From a98149361ded254fcb5b3c41317acae343e62f46 Mon Sep 17 00:00:00 2001 From: ERussel Date: Thu, 28 Sep 2023 15:06:14 +0500 Subject: [PATCH 001/204] base arch --- novawallet.xcodeproj/project.pbxproj | 64 ++++++++++++++++++ .../AssetConversionPallet+KeyMapper.swift | 10 +++ .../AssetConversionPallet+Path.swift | 11 ++++ .../AssetConversionPallet.swift | 27 ++++++++ .../Model/AssetConversion.swift | 31 +++++++++ .../AssetConversionServiceProtocol.swift | 12 ++++ .../AssetHubSwapOperationFactory.swift | 66 +++++++++++++++++++ 7 files changed, 221 insertions(+) create mode 100644 novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet+KeyMapper.swift create mode 100644 novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet+Path.swift create mode 100644 novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift create mode 100644 novawallet/Modules/AssetConversion/Model/AssetConversion.swift create mode 100644 novawallet/Modules/AssetConversion/Service/AssetConversionServiceProtocol.swift create mode 100644 novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 2ac49fc6f5..8488735045 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -39,6 +39,12 @@ 0B2B9C6E2BA2E924D6A54F4B /* CrowdloanListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E78D69E8EBC3EB4D01F8EF /* CrowdloanListInteractor.swift */; }; 0B48B02E973CB304B765BBC9 /* ReferendumDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ABAD23C0039AFA8351C650 /* ReferendumDetailsProtocols.swift */; }; 0B65DAE0327678679CACE0B1 /* GovernanceDelegateInfoViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E894D4633D04AD4415CE1F2 /* GovernanceDelegateInfoViewFactory.swift */; }; + 0C0CB37F2AC540B200EAC516 /* AssetConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB37E2AC540B200EAC516 /* AssetConversion.swift */; }; + 0C0CB3822AC545A800EAC516 /* AssetConversionServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3812AC545A800EAC516 /* AssetConversionServiceProtocol.swift */; }; + 0C0CB3852AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3842AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift */; }; + 0C0CB3882AC5688100EAC516 /* AssetConversionPallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3872AC5688100EAC516 /* AssetConversionPallet.swift */; }; + 0C0CB38A2AC56A1600EAC516 /* AssetConversionPallet+Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3892AC56A1600EAC516 /* AssetConversionPallet+Path.swift */; }; + 0C0CB38E2AC573A900EAC516 /* AssetConversionPallet+KeyMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB38D2AC573A900EAC516 /* AssetConversionPallet+KeyMapper.swift */; }; 0C12A2472AA1D96000C7FA49 /* SubqueryMultistakingTypeFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12A2462AA1D96000C7FA49 /* SubqueryMultistakingTypeFactory.swift */; }; 0C12A2492AA35AE300C7FA49 /* RelaychainStakingValidatorFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12A2482AA35AE300C7FA49 /* RelaychainStakingValidatorFacade.swift */; }; 0C1338102AB832B30036BCD6 /* QRImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13380F2AB832B30036BCD6 /* QRImageViewModel.swift */; }; @@ -3994,6 +4000,12 @@ 0B62C2CBCFF1865A1CA0F1B4 /* LedgerNetworkSelectionProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerNetworkSelectionProtocols.swift; sourceTree = ""; }; 0BA85EE628D7029AC940DFA3 /* NominationPoolBondMoreBasePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreBasePresenter.swift; sourceTree = ""; }; 0C04290B2A67A42A00C3583A /* SubstrateDataModel17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel17.xcdatamodel; sourceTree = ""; }; + 0C0CB37E2AC540B200EAC516 /* AssetConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversion.swift; sourceTree = ""; }; + 0C0CB3812AC545A800EAC516 /* AssetConversionServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionServiceProtocol.swift; sourceTree = ""; }; + 0C0CB3842AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubSwapOperationFactory.swift; sourceTree = ""; }; + 0C0CB3872AC5688100EAC516 /* AssetConversionPallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionPallet.swift; sourceTree = ""; }; + 0C0CB3892AC56A1600EAC516 /* AssetConversionPallet+Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetConversionPallet+Path.swift"; sourceTree = ""; }; + 0C0CB38D2AC573A900EAC516 /* AssetConversionPallet+KeyMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetConversionPallet+KeyMapper.swift"; sourceTree = ""; }; 0C12A2462AA1D96000C7FA49 /* SubqueryMultistakingTypeFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubqueryMultistakingTypeFactory.swift; sourceTree = ""; }; 0C12A2482AA35AE300C7FA49 /* RelaychainStakingValidatorFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaychainStakingValidatorFacade.swift; sourceTree = ""; }; 0C13380F2AB832B30036BCD6 /* QRImageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRImageViewModel.swift; sourceTree = ""; }; @@ -8121,6 +8133,50 @@ path = Amount; sourceTree = ""; }; + 0C0CB37C2AC5408000EAC516 /* AssetConversion */ = { + isa = PBXGroup; + children = ( + 0C0CB3802AC5459200EAC516 /* Service */, + 0C0CB37D2AC5408900EAC516 /* Model */, + ); + path = AssetConversion; + sourceTree = ""; + }; + 0C0CB37D2AC5408900EAC516 /* Model */ = { + isa = PBXGroup; + children = ( + 0C0CB37E2AC540B200EAC516 /* AssetConversion.swift */, + ); + path = Model; + sourceTree = ""; + }; + 0C0CB3802AC5459200EAC516 /* Service */ = { + isa = PBXGroup; + children = ( + 0C0CB3832AC561CA00EAC516 /* AssetHub */, + 0C0CB3812AC545A800EAC516 /* AssetConversionServiceProtocol.swift */, + ); + path = Service; + sourceTree = ""; + }; + 0C0CB3832AC561CA00EAC516 /* AssetHub */ = { + isa = PBXGroup; + children = ( + 0C0CB3842AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift */, + ); + path = AssetHub; + sourceTree = ""; + }; + 0C0CB3862AC5686C00EAC516 /* AssetConversionPallet */ = { + isa = PBXGroup; + children = ( + 0C0CB3872AC5688100EAC516 /* AssetConversionPallet.swift */, + 0C0CB3892AC56A1600EAC516 /* AssetConversionPallet+Path.swift */, + 0C0CB38D2AC573A900EAC516 /* AssetConversionPallet+KeyMapper.swift */, + ); + path = AssetConversionPallet; + sourceTree = ""; + }; 0C13D2F82A7D469E0054BB6F /* Recommendation */ = { isa = PBXGroup; children = ( @@ -10732,6 +10788,7 @@ 8438E1DC24C18F11001BDB13 /* Types */ = { isa = PBXGroup; children = ( + 0C0CB3862AC5686C00EAC516 /* AssetConversionPallet */, 0C7945B92ABB223D001C07CA /* XTokens */, 0C893E6B2A65629E00781503 /* NominationPools */, 8498534D2A1738EC00993977 /* Assets */, @@ -12708,6 +12765,7 @@ 849013D224A9268D008F705E /* Modules */ = { isa = PBXGroup; children = ( + 0C0CB37C2AC5408000EAC516 /* AssetConversion */, 88C5F079297EE429001CCADE /* InAppUpdates */, 845B891A2959608D00EE25B0 /* SecurityLayer */, ABAF6F503B172CEE34E19030 /* MarkdownDescription */, @@ -19146,8 +19204,10 @@ 842876A424AE049B00D91AD8 /* SelectableSubtitleListViewModel.swift in Sources */, AEE5FB1426457A98002B8FDC /* StakingRewardDestSetupInteractor.swift in Sources */, 842D1E7D24D0686D00C30A7A /* TriangularedButton+Inspectable.swift in Sources */, + 0C0CB3882AC5688100EAC516 /* AssetConversionPallet.swift in Sources */, 845D8ED92977F18300F22133 /* GovernanceDelegateStats.swift in Sources */, 84CF00C327F6C1E4004DB322 /* CustomAssetMapper.swift in Sources */, + 0C0CB37F2AC540B200EAC516 /* AssetConversion.swift in Sources */, 880CC0B029E7F194008C7F65 /* EquillibriumLocksUpdater.swift in Sources */, 843910DB253F90C400E3C217 /* NovaWindow.swift in Sources */, 84DD5F4C263DE82C00425ACF /* DataValidationRunner.swift in Sources */, @@ -19644,6 +19704,7 @@ 84CA68D926BE9E7F003B9453 /* SpecVersionSubscription.swift in Sources */, 84C74363251E4C2F009576C6 /* DummySigner.swift in Sources */, 8454C26A2632B8CE00657DAD /* BalanceDepositEvent.swift in Sources */, + 0C0CB38A2AC56A1600EAC516 /* AssetConversionPallet+Path.swift in Sources */, 84DD5F3E263DE5FF00425ACF /* DataValidationProtocols.swift in Sources */, 88F34FD928FFE68B00712BDE /* YourVoteRow.swift in Sources */, 84D8F15F24D8179000AF43E9 /* TitleWithSubtitleViewModel.swift in Sources */, @@ -20775,6 +20836,7 @@ 7D281FEA78E2E5F44990C184 /* AccountImportPresenter.swift in Sources */, 6C56AB4AE63AB2DC73DE98E0 /* AccountImportInteractor.swift in Sources */, F4EAC7972642E0D800FBDDC3 /* ControllerAccountViewModelFactory.swift in Sources */, + 0C0CB3852AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift in Sources */, 775F19512A5811FA009915B6 /* StartStakingParachainInteractor.swift in Sources */, 8828F4F328AD2734009E0B7C /* CrowdloansCalculator.swift in Sources */, 8487583627F06AF300495306 /* QRScannerViewController.swift in Sources */, @@ -21177,6 +21239,7 @@ 0CA307BC2F570941CD22C9AA /* ExportMnemonicConfirmViewFactory.swift in Sources */, 840EAE6929FA935900453C7E /* WalletConnectModelFactory.swift in Sources */, 844CB57A26FA706C00396E13 /* ChainAssetDisplayInfo.swift in Sources */, + 0C0CB38E2AC573A900EAC516 /* AssetConversionPallet+KeyMapper.swift in Sources */, 848DAEF7282274E700D56F55 /* ParachainStakingRemoteSubscriptionService.swift in Sources */, 845B821F26EF8E8900D25C72 /* ManagedMetaAccountModel.swift in Sources */, 88F34FD428FFE64400712BDE /* ReferendumDAppCellView.swift in Sources */, @@ -21482,6 +21545,7 @@ 8498534F2A17390900993977 /* PalletAssets.swift in Sources */, 845B08042918C308005785D3 /* Gov1ActionOperationFactory.swift in Sources */, F0C3DB0CEE1975626B0014A8 /* StakingUnbondConfirmInteractor.swift in Sources */, + 0C0CB3822AC545A800EAC516 /* AssetConversionServiceProtocol.swift in Sources */, 849FA21628A26CB500F83EAA /* CountdownTimerMediator.swift in Sources */, D3B48F82A875E301D749AC0B /* StakingUnbondConfirmViewController.swift in Sources */, 842AEB81292F34B600C61B0C /* RemoteChainExternalApi.swift in Sources */, diff --git a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet+KeyMapper.swift b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet+KeyMapper.swift new file mode 100644 index 0000000000..c38a52feb5 --- /dev/null +++ b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet+KeyMapper.swift @@ -0,0 +1,10 @@ +import Foundation + +extension AssetConversionPallet { + enum PoolKeysMapper { + static func getMapper() -> AnyMapper { + AnyMapper { _ in + } + } + } +} diff --git a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet+Path.swift b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet+Path.swift new file mode 100644 index 0000000000..be192819d5 --- /dev/null +++ b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet+Path.swift @@ -0,0 +1,11 @@ +import Foundation + +extension AssetConversionPallet { + static var poolsPath: StorageCodingPath { + getPoolsPath(for: AssetConversionPallet.name) + } + + static func getPoolsPath(for moduleName: String) -> StorageCodingPath { + StorageCodingPath(moduleName: moduleName, itemName: "Pools") + } +} diff --git a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift new file mode 100644 index 0000000000..b1174889ff --- /dev/null +++ b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift @@ -0,0 +1,27 @@ +import Foundation +import SubstrateSdk + +enum AssetConversionPallet { + static let name = "AssetConversion" + + struct PoolAssetPair { + let asset1: JSON + let asset2: JSON + } + + extension PoolAssetPair: JSONListConvertible { + init(jsonList: [JSON], context _: [CodingUserInfoKey: Any]?) throws { + let expectedFieldsCount = 2 + let actualFieldsCount = jsonList.count + guard expectedFieldsCount == actualFieldsCount else { + throw JSONListConvertibleError.unexpectedNumberOfItems( + expected: expectedFieldsCount, + actual: actualFieldsCount + ) + } + + asset1 = try jsonList[0] + asset2 = try jsonList[1] + } + } +} diff --git a/novawallet/Modules/AssetConversion/Model/AssetConversion.swift b/novawallet/Modules/AssetConversion/Model/AssetConversion.swift new file mode 100644 index 0000000000..cc292d46cc --- /dev/null +++ b/novawallet/Modules/AssetConversion/Model/AssetConversion.swift @@ -0,0 +1,31 @@ +import Foundation +import BigInt + +enum AssetConversion { + enum Direction { + case sell + case buy + } + + struct Args { + let assetIn: ChainAssetId + let assetOut: ChainAssetId + let amount: BigUInt + let direction: Direction + let slippage: Decimal + } + + struct Quote { + let amountIn: BigUInt + let assetIn: ChainAssetId + let amountOut: BigUInt + let assetOut: ChainAssetId + let priceImpact: Decimal + let fee: Fee + } + + struct Fee { + let serviceFee: Decimal + let novaFee: Decimal? + } +} diff --git a/novawallet/Modules/AssetConversion/Service/AssetConversionServiceProtocol.swift b/novawallet/Modules/AssetConversion/Service/AssetConversionServiceProtocol.swift new file mode 100644 index 0000000000..e03fc84a80 --- /dev/null +++ b/novawallet/Modules/AssetConversion/Service/AssetConversionServiceProtocol.swift @@ -0,0 +1,12 @@ +import Foundation +import RobinHood + +protocol AssetConversionOperationFactoryProtocol { + func availableDirections() -> CompoundOperationWrapper<[ChainAssetId: Set]> + func availableDirectionsForAsset(_ chainAssetId: ChainAssetId) -> CompoundOperationWrapper> + func quote(for args: AssetConversion.Args) -> CompoundOperationWrapper +} + +protocol AssetConversionServiceProtocol { + func fetchExtrinsicBuilderClosure(for args: AssetConversion.Args) -> ExtrinsicBuilderClosure +} diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift new file mode 100644 index 0000000000..5d67b8f996 --- /dev/null +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift @@ -0,0 +1,66 @@ +import Foundation +import RobinHood +import SubstrateSdk + +final class AssetHubSwapOperationFactory { + let chain: ChainModel + let runtimeService: RuntimeCodingServiceProtocol + let connection: JSONRPCEngine + + init( + chain: ChainModel, + runtimeService: RuntimeCodingServiceProtocol, + connection: JSONRPCEngine, + operationQueue _: OperationQueue + ) { + self.chain = chain + self.runtimeService = runtimeService + self.connection = connection + } + + private func fetchAllPairsWrapper() -> CompoundOperationWrapper<[AssetConversionPallet.PoolAssetPair]> { + let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() + + let keysFetchOperation = StorageKeysQueryService( + connection: connection, + operationManager: OperationManager(operationQueue: operationQueue), + prefixKeyClosure: { Data() }, + mapper: IdentityMapper() + ).longrunOperation() + + let decodingOperation = StorageKeyDecodingOperation( + path: AssetConversionPallet.poolsPath + ) + + decodingOperation.configurationBlock = { + do { + decodingOperation.codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + decodingOperation.dataList = try keysFetchOperation.extractNoCancellableResultData() + } catch { + decodingOperation.result = .failure(error) + } + } + + decodingOperation.addDependency(codingFactoryOperation) + decodingOperation.addDependency(codingFactoryOperation) + + return CompoundOperationWrapper( + targetOperation: decodingOperation, + dependencies: [codingFactoryOperation, keysFetchOperation] + ) + } +} + +extension AssetHubSwapOperationFactory: AssetConversionOperationFactoryProtocol { + func availableDirections() -> CompoundOperationWrapper<[ChainAssetId: Set]> { + CompoundOperationWrapper.createWithError(CommonError.undefined) + } + + func availableDirectionsForAsset(_: ChainAssetId) -> CompoundOperationWrapper> { + CompoundOperationWrapper.createWithError(CommonError.undefined) + } + + func quote(for _: AssetConversion.Args) -> CompoundOperationWrapper { + CompoundOperationWrapper.createWithError(CommonError.undefined) + } +} From 1e994f496ca00d69b05fb23bf79acbda5dfe12b2 Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 29 Sep 2023 12:03:48 +0500 Subject: [PATCH 002/204] fetch available directions for Asset Hub --- novawallet.xcodeproj/project.pbxproj | 28 ++- .../Common/Model/Xcm/XcmTransferFactory.swift | 14 +- .../AssetConversionPallet+KeyMapper.swift | 10 - .../AssetConversionPallet.swift | 56 ++++- .../Substrate/Types/Assets/PalletAssets.swift | 4 +- .../Common/Substrate/Types/Xcm/V3/XcmV3.swift | 3 + .../Types/Xcm/V3/XcmV3Junction.swift | 178 ++++++++++++++++ .../Types/Xcm/V3/XcmV3Multilocation.swift | 9 + .../Substrate/Types/Xcm/XcmJunction.swift | 192 ++++++++++++++++-- .../Types/Xcm/XcmMultilocation.swift | 2 +- .../Substrate/Types/Xcm/XcmNetworkId.swift | 30 --- .../AssetHubSwapOperationFactory.swift | 115 +++++++++-- 12 files changed, 543 insertions(+), 98 deletions(-) delete mode 100644 novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet+KeyMapper.swift create mode 100644 novawallet/Common/Substrate/Types/Xcm/V3/XcmV3.swift create mode 100644 novawallet/Common/Substrate/Types/Xcm/V3/XcmV3Junction.swift create mode 100644 novawallet/Common/Substrate/Types/Xcm/V3/XcmV3Multilocation.swift delete mode 100644 novawallet/Common/Substrate/Types/Xcm/XcmNetworkId.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 8488735045..730ccb819e 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -44,7 +44,6 @@ 0C0CB3852AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3842AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift */; }; 0C0CB3882AC5688100EAC516 /* AssetConversionPallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3872AC5688100EAC516 /* AssetConversionPallet.swift */; }; 0C0CB38A2AC56A1600EAC516 /* AssetConversionPallet+Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3892AC56A1600EAC516 /* AssetConversionPallet+Path.swift */; }; - 0C0CB38E2AC573A900EAC516 /* AssetConversionPallet+KeyMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB38D2AC573A900EAC516 /* AssetConversionPallet+KeyMapper.swift */; }; 0C12A2472AA1D96000C7FA49 /* SubqueryMultistakingTypeFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12A2462AA1D96000C7FA49 /* SubqueryMultistakingTypeFactory.swift */; }; 0C12A2492AA35AE300C7FA49 /* RelaychainStakingValidatorFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12A2482AA35AE300C7FA49 /* RelaychainStakingValidatorFacade.swift */; }; 0C1338102AB832B30036BCD6 /* QRImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13380F2AB832B30036BCD6 /* QRImageViewModel.swift */; }; @@ -241,6 +240,9 @@ 0CC2E56A2A6E6EBB004092E7 /* LocalStorageProviderObserving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2E5692A6E6EBB004092E7 /* LocalStorageProviderObserving.swift */; }; 0CC4CCF42A67C9C400F63041 /* Multistaking+NominationPools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC4CCF32A67C9C400F63041 /* Multistaking+NominationPools.swift */; }; 0CC6C8D82AAB401200AD8D9B /* CustomValidatorsFullList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC6C8D72AAB401200AD8D9B /* CustomValidatorsFullList.swift */; }; + 0CCA245B2AC6917400AEF23D /* XcmV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA245A2AC6917400AEF23D /* XcmV3.swift */; }; + 0CCA245D2AC6918800AEF23D /* XcmV3Junction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA245C2AC6918800AEF23D /* XcmV3Junction.swift */; }; + 0CCA245F2AC6974200AEF23D /* XcmV3Multilocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA245E2AC6974200AEF23D /* XcmV3Multilocation.swift */; }; 0CCE25212A44306200286709 /* TransactionHistoryPhishingFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCE25202A44306200286709 /* TransactionHistoryPhishingFilter.swift */; }; 0CD1F4D100ED82D137AB9834 /* ParaStkStakeSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2F1EEBF48485F02BF690A4 /* ParaStkStakeSetupViewController.swift */; }; 0CE150502A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE1504F2A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift */; }; @@ -2977,7 +2979,6 @@ 84FB9E1C285C57EF00B42FC0 /* XcmMultilocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FB9E1B285C57EF00B42FC0 /* XcmMultilocation.swift */; }; 84FB9E1E285C58FF00B42FC0 /* Xcm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FB9E1D285C58FF00B42FC0 /* Xcm.swift */; }; 84FB9E20285C5C9E00B42FC0 /* XcmJunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FB9E1F285C5C9E00B42FC0 /* XcmJunction.swift */; }; - 84FB9E22285C5D6200B42FC0 /* XcmNetworkId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FB9E21285C5D6200B42FC0 /* XcmNetworkId.swift */; }; 84FB9E24285C6ACC00B42FC0 /* XcmMultiasset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FB9E23285C6ACC00B42FC0 /* XcmMultiasset.swift */; }; 84FB9E26285C6C5000B42FC0 /* XcmVersionedMultiasset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FB9E25285C6C5000B42FC0 /* XcmVersionedMultiasset.swift */; }; 84FB9E28285C736A00B42FC0 /* XcmTransferFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FB9E27285C736A00B42FC0 /* XcmTransferFactory.swift */; }; @@ -4005,7 +4006,6 @@ 0C0CB3842AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubSwapOperationFactory.swift; sourceTree = ""; }; 0C0CB3872AC5688100EAC516 /* AssetConversionPallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionPallet.swift; sourceTree = ""; }; 0C0CB3892AC56A1600EAC516 /* AssetConversionPallet+Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetConversionPallet+Path.swift"; sourceTree = ""; }; - 0C0CB38D2AC573A900EAC516 /* AssetConversionPallet+KeyMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetConversionPallet+KeyMapper.swift"; sourceTree = ""; }; 0C12A2462AA1D96000C7FA49 /* SubqueryMultistakingTypeFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubqueryMultistakingTypeFactory.swift; sourceTree = ""; }; 0C12A2482AA35AE300C7FA49 /* RelaychainStakingValidatorFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaychainStakingValidatorFacade.swift; sourceTree = ""; }; 0C13380F2AB832B30036BCD6 /* QRImageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRImageViewModel.swift; sourceTree = ""; }; @@ -4209,6 +4209,9 @@ 0CC41A9A168AF0F1FBE5F799 /* Pods_novawalletAll_novawallet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_novawalletAll_novawallet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0CC4CCF32A67C9C400F63041 /* Multistaking+NominationPools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Multistaking+NominationPools.swift"; sourceTree = ""; }; 0CC6C8D72AAB401200AD8D9B /* CustomValidatorsFullList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomValidatorsFullList.swift; sourceTree = ""; }; + 0CCA245A2AC6917400AEF23D /* XcmV3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmV3.swift; sourceTree = ""; }; + 0CCA245C2AC6918800AEF23D /* XcmV3Junction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmV3Junction.swift; sourceTree = ""; }; + 0CCA245E2AC6974200AEF23D /* XcmV3Multilocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmV3Multilocation.swift; sourceTree = ""; }; 0CCE25202A44306200286709 /* TransactionHistoryPhishingFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionHistoryPhishingFilter.swift; sourceTree = ""; }; 0CDFFCC54A504417F4ACE7AA /* NftListInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftListInteractor.swift; sourceTree = ""; }; 0CE1504F2A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsPoolSubscriptionService.swift; sourceTree = ""; }; @@ -6978,7 +6981,6 @@ 84FB9E1B285C57EF00B42FC0 /* XcmMultilocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmMultilocation.swift; sourceTree = ""; }; 84FB9E1D285C58FF00B42FC0 /* Xcm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcm.swift; sourceTree = ""; }; 84FB9E1F285C5C9E00B42FC0 /* XcmJunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmJunction.swift; sourceTree = ""; }; - 84FB9E21285C5D6200B42FC0 /* XcmNetworkId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmNetworkId.swift; sourceTree = ""; }; 84FB9E23285C6ACC00B42FC0 /* XcmMultiasset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmMultiasset.swift; sourceTree = ""; }; 84FB9E25285C6C5000B42FC0 /* XcmVersionedMultiasset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmVersionedMultiasset.swift; sourceTree = ""; }; 84FB9E27285C736A00B42FC0 /* XcmTransferFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmTransferFactory.swift; sourceTree = ""; }; @@ -8172,7 +8174,6 @@ children = ( 0C0CB3872AC5688100EAC516 /* AssetConversionPallet.swift */, 0C0CB3892AC56A1600EAC516 /* AssetConversionPallet+Path.swift */, - 0C0CB38D2AC573A900EAC516 /* AssetConversionPallet+KeyMapper.swift */, ); path = AssetConversionPallet; sourceTree = ""; @@ -8564,6 +8565,16 @@ path = Model; sourceTree = ""; }; + 0CCA24592AC6914100AEF23D /* V3 */ = { + isa = PBXGroup; + children = ( + 0CCA245A2AC6917400AEF23D /* XcmV3.swift */, + 0CCA245C2AC6918800AEF23D /* XcmV3Junction.swift */, + 0CCA245E2AC6974200AEF23D /* XcmV3Multilocation.swift */, + ); + path = V3; + sourceTree = ""; + }; 0CE550B42A4973BA00F0A7AC /* StakingUnbondSetup */ = { isa = PBXGroup; children = ( @@ -16206,12 +16217,12 @@ 84FB9E18285C57C000B42FC0 /* Xcm */ = { isa = PBXGroup; children = ( + 0CCA24592AC6914100AEF23D /* V3 */, 84EBFCE5285E7A6D0006327E /* Message */, 84FB9E19285C57D900B42FC0 /* XcmVersionedMultilocation.swift */, 84FB9E1B285C57EF00B42FC0 /* XcmMultilocation.swift */, 84FB9E1D285C58FF00B42FC0 /* Xcm.swift */, 84FB9E1F285C5C9E00B42FC0 /* XcmJunction.swift */, - 84FB9E21285C5D6200B42FC0 /* XcmNetworkId.swift */, 84FB9E23285C6ACC00B42FC0 /* XcmMultiasset.swift */, 84FB9E25285C6C5000B42FC0 /* XcmVersionedMultiasset.swift */, 84EBFCEF285E84C30006327E /* XcmMultiassetFilter.swift */, @@ -20524,7 +20535,6 @@ 849013D024A9267F008F705E /* R.generated.swift in Sources */, 844CED2529260B6A001A7757 /* EvmQueryContractMessageFactory.swift in Sources */, 8442003828EAA16600C49C4A /* ReferendumsPresenter.swift in Sources */, - 84FB9E22285C5D6200B42FC0 /* XcmNetworkId.swift in Sources */, 84DC3CE52796768A0038E2ED /* SubqueryHistoryContext.swift in Sources */, 848077D22837CAE5003B7C79 /* ParachainStakingErrorPresentable.swift in Sources */, 84F4387025D9BC3900AEDA56 /* EmptyStreamableSource.swift in Sources */, @@ -21239,7 +21249,6 @@ 0CA307BC2F570941CD22C9AA /* ExportMnemonicConfirmViewFactory.swift in Sources */, 840EAE6929FA935900453C7E /* WalletConnectModelFactory.swift in Sources */, 844CB57A26FA706C00396E13 /* ChainAssetDisplayInfo.swift in Sources */, - 0C0CB38E2AC573A900EAC516 /* AssetConversionPallet+KeyMapper.swift in Sources */, 848DAEF7282274E700D56F55 /* ParachainStakingRemoteSubscriptionService.swift in Sources */, 845B821F26EF8E8900D25C72 /* ManagedMetaAccountModel.swift in Sources */, 88F34FD428FFE64400712BDE /* ReferendumDAppCellView.swift in Sources */, @@ -21307,6 +21316,7 @@ C01C5F1C8CB67B0D5CBE9FB1 /* StakingMainPresenter.swift in Sources */, 843F657A265854A700829C14 /* CrowdloanDisplayInfo.swift in Sources */, 0C13D31F2A8227AB0054BB6F /* StartStakingPoolConfirmPresenter.swift in Sources */, + 0CCA245D2AC6918800AEF23D /* XcmV3Junction.swift in Sources */, 8412AF992789AB76008A6C22 /* PolkadotExtensionMetadata.swift in Sources */, 847999AF2888A45700D1BAD2 /* AddAccount+CreateWatchOnlyWireframe.swift in Sources */, 846E5010277998040049B659 /* DAppAuthRequest.swift in Sources */, @@ -21583,6 +21593,7 @@ 845B07EF2915951A005785D3 /* DemocracyVoteThreshold.swift in Sources */, 885551F78A5926D16D5AF0CB /* ControllerAccountPresenter.swift in Sources */, 8442002F28E9AEFB00C49C4A /* VoteWireframe.swift in Sources */, + 0CCA245F2AC6974200AEF23D /* XcmV3Multilocation.swift in Sources */, 88E5E2A5295D8F8E001B1D41 /* DAppGlobalSettingsViewModel.swift in Sources */, 841E553C282D44BA00C8438F /* ParachainStakingAccountSubscriptionService.swift in Sources */, 84C479C129309E58003DF82B /* BasePolkassemblyOperationFactory.swift in Sources */, @@ -21931,6 +21942,7 @@ 0C7C886C2A9622F800DD96A1 /* StakingSelectedEntityViewModel.swift in Sources */, 84C3420B283187D800156569 /* BlockTimeEstimationService.swift in Sources */, EB376E61CD1C39AC148DE80C /* NftListViewController.swift in Sources */, + 0CCA245B2AC6917400AEF23D /* XcmV3.swift in Sources */, 84BAD213293AFCDA00C55C49 /* TokensManageTableViewCell.swift in Sources */, 84C5ADD02811E6FA006D7388 /* LinkCellView.swift in Sources */, EBDDDEE1BAB05E95DC720783 /* NftListViewLayout.swift in Sources */, diff --git a/novawallet/Common/Model/Xcm/XcmTransferFactory.swift b/novawallet/Common/Model/Xcm/XcmTransferFactory.swift index ca394903b3..229bef65e4 100644 --- a/novawallet/Common/Model/Xcm/XcmTransferFactory.swift +++ b/novawallet/Common/Model/Xcm/XcmTransferFactory.swift @@ -91,7 +91,7 @@ extension XcmTransferFactoryProtocol { } final class XcmTransferFactory { - private func extractRelativeJunctions(from path: JSON) throws -> Xcm.Junctions { + private func extractRelativeJunctions(from path: JSON) throws -> Xcm.JunctionsV2 { var junctions: [Xcm.Junction] = [] if let palletInstance = path.palletInstance?.unsignedIntValue { @@ -112,7 +112,7 @@ final class XcmTransferFactory { return Xcm.Junctions(items: junctions) } - private func extractAbsoluteJunctions(from path: JSON) throws -> Xcm.Junctions { + private func extractAbsoluteJunctions(from path: JSON) throws -> Xcm.JunctionsV2 { let commonJunctions = try extractRelativeJunctions(from: path) if let parachainId = path.parachainId?.unsignedIntValue { @@ -150,7 +150,7 @@ final class XcmTransferFactory { reserve: ChainModel ) throws -> Xcm.Multilocation { let parents = extractParents(from: path, type: type, origin: origin, reserve: reserve) - let junctions: Xcm.Junctions + let junctions: Xcm.JunctionsV2 switch type { case .absolute, .concrete: @@ -188,7 +188,7 @@ final class XcmTransferFactory { parents = 0 } - let junctions: Xcm.Junctions + let junctions: Xcm.JunctionsV2 if let parachainId = destination.parachainId { let networkJunction = Xcm.Junction.parachain(parachainId) @@ -212,13 +212,13 @@ final class XcmTransferFactory { parents = 0 } - let junctions: Xcm.Junctions + let junctions: Xcm.JunctionsV2 if let parachainId = reserve.parachainId { let networkJunction = Xcm.Junction.parachain(parachainId) - junctions = Xcm.Junctions(items: [networkJunction]) + junctions = Xcm.JunctionsV2(items: [networkJunction]) } else { - junctions = Xcm.Junctions(items: []) + junctions = Xcm.JunctionsV2(items: []) } return Xcm.Multilocation(parents: parents, interior: junctions) diff --git a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet+KeyMapper.swift b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet+KeyMapper.swift deleted file mode 100644 index c38a52feb5..0000000000 --- a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet+KeyMapper.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -extension AssetConversionPallet { - enum PoolKeysMapper { - static func getMapper() -> AnyMapper { - AnyMapper { _ in - } - } - } -} diff --git a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift index b1174889ff..f10edab12c 100644 --- a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift +++ b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift @@ -1,16 +1,55 @@ import Foundation import SubstrateSdk +import BigInt enum AssetConversionPallet { static let name = "AssetConversion" - struct PoolAssetPair { - let asset1: JSON - let asset2: JSON + enum PoolAsset { + case native + case assets(pallet: UInt8, index: BigUInt) + case foreignNetwork(XcmV3.NetworkId) + case undefined(XcmV3.Multilocation) + + init(multilocation: XcmV3.Multilocation) { + let junctions = multilocation.interior.items + + if multilocation.parents == 0 { + guard !junctions.isEmpty else { + self = .native + return + } + + switch junctions[0] { + case let .palletInstance(pallet): + if + junctions.count == 2, + case let .generalIndex(index) = junctions[1] { + self = .assets(pallet: pallet, index: index) + } else { + self = .undefined(multilocation) + } + default: + self = .undefined(multilocation) + } + } else if multilocation.parents == 2, junctions.count == 1 { + switch junctions[0] { + case let .globalConsensus(network): + self = .foreignNetwork(network) + default: + self = .undefined(multilocation) + } + } else { + self = .undefined(multilocation) + } + } } - extension PoolAssetPair: JSONListConvertible { - init(jsonList: [JSON], context _: [CodingUserInfoKey: Any]?) throws { + struct PoolAssetPair: JSONListConvertible { + let asset1: PoolAsset + let asset2: PoolAsset + + init(jsonList: [JSON], context: [CodingUserInfoKey: Any]?) throws { let expectedFieldsCount = 2 let actualFieldsCount = jsonList.count guard expectedFieldsCount == actualFieldsCount else { @@ -20,8 +59,11 @@ enum AssetConversionPallet { ) } - asset1 = try jsonList[0] - asset2 = try jsonList[1] + let multilocation1 = try jsonList[0].map(to: XcmV3.Multilocation.self, with: context) + let multilocation2 = try jsonList[1].map(to: XcmV3.Multilocation.self, with: context) + + asset1 = PoolAsset(multilocation: multilocation1) + asset2 = PoolAsset(multilocation: multilocation2) } } } diff --git a/novawallet/Common/Substrate/Types/Assets/PalletAssets.swift b/novawallet/Common/Substrate/Types/Assets/PalletAssets.swift index d5e569ea9e..a6364b3f88 100644 --- a/novawallet/Common/Substrate/Types/Assets/PalletAssets.swift +++ b/novawallet/Common/Substrate/Types/Assets/PalletAssets.swift @@ -1,3 +1,5 @@ import Foundation -enum PalletAssets {} +enum PalletAssets { + static let name = "Assets" +} diff --git a/novawallet/Common/Substrate/Types/Xcm/V3/XcmV3.swift b/novawallet/Common/Substrate/Types/Xcm/V3/XcmV3.swift new file mode 100644 index 0000000000..b6988009bc --- /dev/null +++ b/novawallet/Common/Substrate/Types/Xcm/V3/XcmV3.swift @@ -0,0 +1,3 @@ +import Foundation + +enum XcmV3 {} diff --git a/novawallet/Common/Substrate/Types/Xcm/V3/XcmV3Junction.swift b/novawallet/Common/Substrate/Types/Xcm/V3/XcmV3Junction.swift new file mode 100644 index 0000000000..bfd05ab7a4 --- /dev/null +++ b/novawallet/Common/Substrate/Types/Xcm/V3/XcmV3Junction.swift @@ -0,0 +1,178 @@ +import Foundation +import BigInt +import SubstrateSdk + +extension XcmV3 { + enum NetworkId: Codable { + static let byGenesisField = "ByGenesis" + static let polkadotField = "Polkadot" + static let kusamaField = "Kusama" + static let westendField = "Westend" + + case byGenesis(Data) + case polkadot + case kusama + case westend + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + let type = try container.decode(String.self) + + switch type { + case Self.byGenesisField: + let hash = try container.decode(BytesCodable.self).wrappedValue + self = .byGenesis(hash) + case Self.polkadotField: + self = .polkadot + case Self.kusamaField: + self = .kusama + case Self.westendField: + self = .westend + default: + throw DecodingError.dataCorrupted( + .init( + codingPath: decoder.codingPath, + debugDescription: "Unsupported network id: \(type)" + ) + ) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + + switch self { + case let .byGenesis(hash): + try container.encode(Self.byGenesisField) + try container.encode(BytesCodable(wrappedValue: hash)) + case .polkadot: + try container.encode(Self.polkadotField) + try container.encode(JSON.null) + case .kusama: + try container.encode(Self.kusamaField) + try container.encode(JSON.null) + case .westend: + try container.encode(Self.westendField) + try container.encode(JSON.null) + } + } + } + + struct AccountId32Value: Codable { + enum CodingKeys: String, CodingKey { + case network + case accountId = "id" + } + + let network: NetworkId? + @BytesCodable var accountId: AccountId + } + + struct AccountId20Value: Codable { + let network: NetworkId? + @BytesCodable var key: AccountId + } + + struct AccountIndexValue: Codable { + let network: NetworkId? + @StringCodable var index: UInt64 + } + + enum Junction: Codable { + static let parachainField = "Parachain" + static let accountId32Field = "AccountId32" + static let accountIndex64Field = "AccountIndex64" + static let accountKey20Field = "AccountKey20" + static let palletInstanceField = "PalletInstance" + static let generalIndexField = "GeneralIndex" + static let generalKeyField = "GeneralKey" + static let onlyChildKey = "OnlyChild" + static let globalConsensusField = "GlobalConsensus" + + case parachain(_ paraId: ParaId) + case accountId32(AccountId32Value) + case accountIndex64(AccountIndexValue) + case accountKey20(AccountId20Value) + case palletInstance(_ index: UInt8) + case generalIndex(_ index: BigUInt) + case generalKey(_ key: Data) + case onlyChild + case globalConsensus(NetworkId) + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + let type = try container.decode(String.self) + + switch type { + case Self.parachainField: + let paraId = try container.decode(StringScaleMapper.self).value + self = .parachain(paraId) + case Self.accountId32Field: + let accountId = try container.decode(AccountId32Value.self) + self = .accountId32(accountId) + case Self.accountIndex64Field: + let accountIndex = try container.decode(AccountIndexValue.self) + self = .accountIndex64(accountIndex) + case Self.accountKey20Field: + let accountKey = try container.decode(AccountId20Value.self) + self = .accountKey20(accountKey) + case Self.palletInstanceField: + let index = try container.decode(StringScaleMapper.self).value + self = .palletInstance(index) + case Self.generalKeyField: + let key = try container.decode(BytesCodable.self).wrappedValue + self = .generalKey(key) + case Self.onlyChildKey: + self = .onlyChild + case Self.globalConsensusField: + let network = try container.decode(NetworkId.self) + self = .globalConsensus(network) + default: + throw DecodingError.dataCorrupted( + .init( + codingPath: decoder.codingPath, + debugDescription: "Unsupported junction: \(type)" + ) + ) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + + switch self { + case let .parachain(paraId): + try container.encode(Self.parachainField) + try container.encode(StringScaleMapper(value: paraId)) + case let .accountId32(value): + try container.encode(Self.accountId32Field) + try container.encode(value) + case let .accountIndex64(value): + try container.encode(Self.accountIndex64Field) + try container.encode(value) + case let .accountKey20(value): + try container.encode(Self.accountKey20Field) + try container.encode(value) + case let .palletInstance(index): + try container.encode(Self.palletInstanceField) + try container.encode(StringScaleMapper(value: index)) + case let .generalIndex(index): + try container.encode(Self.generalIndexField) + try container.encode(StringScaleMapper(value: index)) + case let .generalKey(key): + try container.encode(Self.generalKeyField) + try container.encode(BytesCodable(wrappedValue: key)) + case .onlyChild: + try container.encode(Self.onlyChildKey) + try container.encode(JSON.null) + case let .globalConsensus(network): + try container.encode(Self.globalConsensusField) + try container.encode(network) + } + } + } + + typealias Junctions = Xcm.Junctions +} diff --git a/novawallet/Common/Substrate/Types/Xcm/V3/XcmV3Multilocation.swift b/novawallet/Common/Substrate/Types/Xcm/V3/XcmV3Multilocation.swift new file mode 100644 index 0000000000..76c02e43a4 --- /dev/null +++ b/novawallet/Common/Substrate/Types/Xcm/V3/XcmV3Multilocation.swift @@ -0,0 +1,9 @@ +import Foundation +import SubstrateSdk + +extension XcmV3 { + struct Multilocation: Codable { + @StringCodable var parents: UInt8 + let interior: XcmV3.Junctions + } +} diff --git a/novawallet/Common/Substrate/Types/Xcm/XcmJunction.swift b/novawallet/Common/Substrate/Types/Xcm/XcmJunction.swift index 334ab8cf12..66315b15d4 100644 --- a/novawallet/Common/Substrate/Types/Xcm/XcmJunction.swift +++ b/novawallet/Common/Substrate/Types/Xcm/XcmJunction.swift @@ -3,7 +3,63 @@ import BigInt import SubstrateSdk extension Xcm { - struct AccountId32Value: Encodable { + enum NetworkId: Codable { + static let anyField = "Any" + static let namedField = "Named" + static let polkadotField = "Polkadot" + static let kusamaField = "Kusama" + + case any + case named(_ data: Data) + case polkadot + case kusama + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + let type = try container.decode(String.self) + + switch type { + case Self.anyField: + self = .any + case Self.namedField: + let data = try container.decode(BytesCodable.self).wrappedValue + self = .named(data) + case Self.polkadotField: + self = .polkadot + case Self.kusamaField: + self = .kusama + default: + throw DecodingError.dataCorrupted( + .init( + codingPath: decoder.codingPath, + debugDescription: "Unsupported network id: \(type)" + ) + ) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + + switch self { + case .any: + try container.encode(Self.anyField) + try container.encode(JSON.null) + case let .named(data): + try container.encode(Self.namedField) + try container.encode(BytesCodable(wrappedValue: data)) + case .polkadot: + try container.encode(Self.polkadotField) + try container.encode(JSON.null) + case .kusama: + try container.encode(Self.kusamaField) + try container.encode(JSON.null) + } + } + } + + struct AccountId32Value: Codable { enum CodingKeys: String, CodingKey { case network case accountId = "id" @@ -13,17 +69,26 @@ extension Xcm { @BytesCodable var accountId: AccountId } - struct AccountId20Value: Encodable { + struct AccountId20Value: Codable { let network: NetworkId @BytesCodable var key: AccountId } - struct AccountIndexValue: Encodable { + struct AccountIndexValue: Codable { let network: NetworkId @StringCodable var index: UInt64 } - enum Junction: Encodable { + enum Junction: Codable { + static let parachainField = "Parachain" + static let accountId32Field = "AccountId32" + static let accountIndex64Field = "AccountIndex64" + static let accountKey20Field = "AccountKey20" + static let palletInstanceField = "PalletInstance" + static let generalIndexField = "GeneralIndex" + static let generalKeyField = "GeneralKey" + static let onlyChildKey = "OnlyChild" + case parachain(_ paraId: ParaId) case accountId32(AccountId32Value) case accountIndex64(AccountIndexValue) @@ -33,48 +98,133 @@ extension Xcm { case generalKey(_ key: Data) case onlyChild + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + let type = try container.decode(String.self) + + switch type { + case Self.parachainField: + let paraId = try container.decode(StringScaleMapper.self).value + self = .parachain(paraId) + case Self.accountId32Field: + let accountId = try container.decode(AccountId32Value.self) + self = .accountId32(accountId) + case Self.accountIndex64Field: + let accountIndex = try container.decode(AccountIndexValue.self) + self = .accountIndex64(accountIndex) + case Self.accountKey20Field: + let accountKey = try container.decode(AccountId20Value.self) + self = .accountKey20(accountKey) + case Self.palletInstanceField: + let index = try container.decode(StringScaleMapper.self).value + self = .palletInstance(index) + case Self.generalKeyField: + let key = try container.decode(BytesCodable.self).wrappedValue + self = .generalKey(key) + case Self.onlyChildKey: + self = .onlyChild + default: + throw DecodingError.dataCorrupted( + .init( + codingPath: decoder.codingPath, + debugDescription: "Unsupported junction: \(type)" + ) + ) + } + } + func encode(to encoder: Encoder) throws { var container = encoder.unkeyedContainer() switch self { case let .parachain(paraId): - try container.encode("Parachain") + try container.encode(Self.parachainField) try container.encode(StringScaleMapper(value: paraId)) case let .accountId32(value): - try container.encode("AccountId32") + try container.encode(Self.accountId32Field) try container.encode(value) case let .accountIndex64(value): - try container.encode("AccountIndex64") + try container.encode(Self.accountIndex64Field) try container.encode(value) case let .accountKey20(value): - try container.encode("AccountKey20") + try container.encode(Self.accountKey20Field) try container.encode(value) case let .palletInstance(index): - try container.encode("PalletInstance") + try container.encode(Self.palletInstanceField) try container.encode(StringScaleMapper(value: index)) case let .generalIndex(index): - try container.encode("GeneralIndex") + try container.encode(Self.generalIndexField) try container.encode(StringScaleMapper(value: index)) case let .generalKey(key): - try container.encode("GeneralKey") + try container.encode(Self.generalKeyField) try container.encode(BytesCodable(wrappedValue: key)) case .onlyChild: - try container.encode("OnlyChild") + try container.encode(Self.onlyChildKey) try container.encode(JSON.null) } } } - struct Junctions: Encodable { - let items: [Xcm.Junction] + enum JunctionsConstants { + static let hereField = "Here" + static let junctionPrefix = "X" + } + + struct Junctions: Codable where J: Codable { + let items: [J] + + init(items: [J]) { + self.items = items + } + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + let type = try container.decode(String.self) + + if type == JunctionsConstants.hereField { + items = [] + } else if + type.count == 2, + type.starts(with: JunctionsConstants.junctionPrefix), + let itemsCount = Int(type.suffix(1)) { + if itemsCount > 1 { + let item = try container.decode(J.self) + items = [item] + } else { + let dict = try container.decode([String: J].self) + + items = try (0 ..< itemsCount).map { index in + guard let junction = dict[String(index)] else { + throw DecodingError.dataCorrupted( + .init( + codingPath: container.codingPath, + debugDescription: "Unsupported junctions: \(dict)" + ) + ) + } + + return junction + } + } + } else { + throw DecodingError.dataCorrupted( + .init( + codingPath: container.codingPath, + debugDescription: "Unsupported junctions format" + ) + ) + } + } func encode(to encoder: Encoder) throws { var container = encoder.unkeyedContainer() if items.isEmpty { - try container.encode("Here") + try container.encode(JunctionsConstants.hereField) } else { - let xLocation = "X\(items.count)" + let xLocation = "\(JunctionsConstants.junctionPrefix)\(items.count)" try container.encode(xLocation) } @@ -83,7 +233,7 @@ extension Xcm { } else if items.count == 1 { try container.encode(items[0]) } else { - var jsonDict: [String: Xcm.Junction] = [:] + var jsonDict: [String: J] = [:] for (index, item) in items.enumerated() { let key = String(index) jsonDict[key] = item @@ -93,18 +243,20 @@ extension Xcm { } } } + + typealias JunctionsV2 = Junctions } extension Xcm.Junctions { - func appending(components: [Xcm.Junction]) -> Xcm.Junctions { + func appending(components: [J]) -> Xcm.Junctions { Xcm.Junctions(items: items + components) } - func prepending(components: [Xcm.Junction]) -> Xcm.Junctions { + func prepending(components: [J]) -> Xcm.Junctions { Xcm.Junctions(items: components + items) } - func lastComponent() -> (Xcm.Junctions, Xcm.Junctions) { + func lastComponent() -> (Xcm.Junctions, Xcm.Junctions) { guard let lastJunction = items.last else { return (self, Xcm.Junctions(items: [])) } diff --git a/novawallet/Common/Substrate/Types/Xcm/XcmMultilocation.swift b/novawallet/Common/Substrate/Types/Xcm/XcmMultilocation.swift index 6041fd8291..476c359c07 100644 --- a/novawallet/Common/Substrate/Types/Xcm/XcmMultilocation.swift +++ b/novawallet/Common/Substrate/Types/Xcm/XcmMultilocation.swift @@ -5,6 +5,6 @@ import BigInt extension Xcm { struct Multilocation: Encodable { @StringCodable var parents: UInt8 - let interior: Xcm.Junctions + let interior: Xcm.JunctionsV2 } } diff --git a/novawallet/Common/Substrate/Types/Xcm/XcmNetworkId.swift b/novawallet/Common/Substrate/Types/Xcm/XcmNetworkId.swift deleted file mode 100644 index d0a96e1c40..0000000000 --- a/novawallet/Common/Substrate/Types/Xcm/XcmNetworkId.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation -import SubstrateSdk - -extension Xcm { - enum NetworkId: Encodable { - case any - case named(_ data: Data) - case polkadot - case kusama - - func encode(to encoder: Encoder) throws { - var container = encoder.unkeyedContainer() - - switch self { - case .any: - try container.encode("Any") - try container.encode(JSON.null) - case let .named(data): - try container.encode("Named") - try container.encode(BytesCodable(wrappedValue: data)) - case .polkadot: - try container.encode("Polkadot") - try container.encode(JSON.null) - case .kusama: - try container.encode("Kusama") - try container.encode(JSON.null) - } - } - } -} diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift index 5d67b8f996..940d0c9d5e 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift @@ -1,31 +1,34 @@ import Foundation import RobinHood import SubstrateSdk +import BigInt final class AssetHubSwapOperationFactory { let chain: ChainModel let runtimeService: RuntimeCodingServiceProtocol let connection: JSONRPCEngine + let operationQueue: OperationQueue init( chain: ChainModel, runtimeService: RuntimeCodingServiceProtocol, connection: JSONRPCEngine, - operationQueue _: OperationQueue + operationQueue: OperationQueue ) { self.chain = chain self.runtimeService = runtimeService self.connection = connection + self.operationQueue = operationQueue } - private func fetchAllPairsWrapper() -> CompoundOperationWrapper<[AssetConversionPallet.PoolAssetPair]> { - let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() - + private func fetchAllPairsWrapper( + dependingOn codingFactoryOperation: BaseOperation + ) -> CompoundOperationWrapper<[AssetConversionPallet.PoolAssetPair]> { let keysFetchOperation = StorageKeysQueryService( connection: connection, operationManager: OperationManager(operationQueue: operationQueue), prefixKeyClosure: { Data() }, - mapper: IdentityMapper() + mapper: AnyMapper(mapper: IdentityMapper()) ).longrunOperation() let decodingOperation = StorageKeyDecodingOperation( @@ -41,23 +44,107 @@ final class AssetHubSwapOperationFactory { } } - decodingOperation.addDependency(codingFactoryOperation) - decodingOperation.addDependency(codingFactoryOperation) + decodingOperation.addDependency(keysFetchOperation) - return CompoundOperationWrapper( - targetOperation: decodingOperation, - dependencies: [codingFactoryOperation, keysFetchOperation] - ) + return CompoundOperationWrapper(targetOperation: decodingOperation, dependencies: [keysFetchOperation]) + } + + private func mapRemotePairsOperation( + for chain: ChainModel, + dependingOn codingFactoryOperation: BaseOperation, + remotePairsOperation: BaseOperation<[AssetConversionPallet.PoolAssetPair]> + ) -> BaseOperation<[ChainAssetId: Set]> { + ClosureOperation<[ChainAssetId: Set]> { + let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + let remotePairs = try remotePairsOperation.extractNoCancellableResultData() + + let optNativeAsset = chain.utilityAsset() + + let initAssetsStore = [BigUInt: (AssetModel, StatemineAssetExtras)]() + let assetsPalletTokens = chain.assets.reduce(into: initAssetsStore) { store, asset in + let optStorageInfo = try? AssetStorageInfo.extract(from: asset, codingFactory: codingFactory) + guard case let .statemine(extras) = optStorageInfo, let assetId = BigUInt(extras.assetId) else { + return + } + + store[assetId] = (asset, extras) + } + + let mappingClosure: (AssetConversionPallet.PoolAsset) -> ChainAssetId? = { remoteAsset in + switch remoteAsset { + case .native: + if let nativeAsset = optNativeAsset { + return ChainAssetId(chainId: chain.chainId, assetId: nativeAsset.assetId) + } else { + return nil + } + case let .assets(pallet, index): + guard let localToken = assetsPalletTokens[index] else { + return nil + } + + let palletName = localToken.1.palletName ?? PalletAssets.name + + guard + let moduleIndex = codingFactory.metadata.getModuleIndex(palletName), + moduleIndex == pallet else { + // only Assets pallet currently supported + return nil + } + + return ChainAssetId(chainId: chain.chainId, assetId: localToken.0.assetId) + default: + return nil + } + } + + let initPairsStore = [ChainAssetId: Set]() + let result = remotePairs.reduce(into: initPairsStore) { store, remotePair in + guard + let asset1 = mappingClosure(remotePair.asset1), + let asset2 = mappingClosure(remotePair.asset2) else { + return + } + + store[asset1] = Set([asset2]).union(store[asset1] ?? []) + store[asset2] = Set([asset1]).union(store[asset2] ?? []) + } + + return result + } } } extension AssetHubSwapOperationFactory: AssetConversionOperationFactoryProtocol { func availableDirections() -> CompoundOperationWrapper<[ChainAssetId: Set]> { - CompoundOperationWrapper.createWithError(CommonError.undefined) + let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() + let fetchRemoteWrapper = fetchAllPairsWrapper(dependingOn: codingFactoryOperation) + let mappingOperation = mapRemotePairsOperation( + for: chain, + dependingOn: codingFactoryOperation, + remotePairsOperation: fetchRemoteWrapper.targetOperation + ) + + fetchRemoteWrapper.addDependency(operations: [codingFactoryOperation]) + mappingOperation.addDependency(fetchRemoteWrapper.targetOperation) + + let dependencies = [codingFactoryOperation] + fetchRemoteWrapper.allOperations + + return CompoundOperationWrapper(targetOperation: mappingOperation, dependencies: dependencies) } - func availableDirectionsForAsset(_: ChainAssetId) -> CompoundOperationWrapper> { - CompoundOperationWrapper.createWithError(CommonError.undefined) + func availableDirectionsForAsset(_ chainAssetId: ChainAssetId) -> CompoundOperationWrapper> { + let allDirectionsWrapper = availableDirections() + + let mappingOperation = ClosureOperation> { + let allChainAssets = try allDirectionsWrapper.targetOperation.extractNoCancellableResultData() + + return allChainAssets[chainAssetId] ?? [] + } + + mappingOperation.addDependency(allDirectionsWrapper.targetOperation) + + return CompoundOperationWrapper(targetOperation: mappingOperation, dependencies: allDirectionsWrapper.allOperations) } func quote(for _: AssetConversion.Args) -> CompoundOperationWrapper { From 17ff785b8080fc6a55b99e275873b0b024701f73 Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 29 Sep 2023 12:30:35 +0500 Subject: [PATCH 003/204] improve xcm tests --- novawalletIntegrationTests/XcmTransfersFeeTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novawalletIntegrationTests/XcmTransfersFeeTests.swift b/novawalletIntegrationTests/XcmTransfersFeeTests.swift index 5dd3159761..6e955893dd 100644 --- a/novawalletIntegrationTests/XcmTransfersFeeTests.swift +++ b/novawalletIntegrationTests/XcmTransfersFeeTests.swift @@ -179,7 +179,7 @@ class XcmTransfersFeeTests: XCTestCase { operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) - _ = semaphore.wait(timeout: .now() + .seconds(60)) + _ = semaphore.wait(timeout: .now() + .seconds(600)) switch partiesResult { case let .success(parties): @@ -236,7 +236,7 @@ class XcmTransfersFeeTests: XCTestCase { } } - _ = semaphore.wait(timeout: .now() + .seconds(60)) + _ = semaphore.wait(timeout: .now() + .seconds(600)) switch feeResult { case let .success(parties): From 57d50841242a21700569816c1b26ee2bae6ef029 Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 29 Sep 2023 14:25:23 +0500 Subject: [PATCH 004/204] add integration tests for directions fetch --- novawallet.xcodeproj/project.pbxproj | 4 ++ novawallet/Common/Model/KnownChainIds.swift | 1 + .../Protocols/JSONListConvertible.swift | 1 + .../AssetConversionPallet.swift | 10 ++- .../Types/Xcm/V3/XcmV3Junction.swift | 3 + .../Substrate/Types/Xcm/XcmJunction.swift | 5 +- .../AssetHubSwapOperationFactory.swift | 27 ++++++- .../AssetHubSwapTests.swift | 72 +++++++++++++++++++ 8 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 novawalletIntegrationTests/AssetHubSwapTests.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 730ccb819e..75408e1d5f 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -243,6 +243,7 @@ 0CCA245B2AC6917400AEF23D /* XcmV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA245A2AC6917400AEF23D /* XcmV3.swift */; }; 0CCA245D2AC6918800AEF23D /* XcmV3Junction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA245C2AC6918800AEF23D /* XcmV3Junction.swift */; }; 0CCA245F2AC6974200AEF23D /* XcmV3Multilocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA245E2AC6974200AEF23D /* XcmV3Multilocation.swift */; }; + 0CCA24652AC6B51200AEF23D /* AssetHubSwapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA24642AC6B51200AEF23D /* AssetHubSwapTests.swift */; }; 0CCE25212A44306200286709 /* TransactionHistoryPhishingFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCE25202A44306200286709 /* TransactionHistoryPhishingFilter.swift */; }; 0CD1F4D100ED82D137AB9834 /* ParaStkStakeSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2F1EEBF48485F02BF690A4 /* ParaStkStakeSetupViewController.swift */; }; 0CE150502A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE1504F2A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift */; }; @@ -4212,6 +4213,7 @@ 0CCA245A2AC6917400AEF23D /* XcmV3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmV3.swift; sourceTree = ""; }; 0CCA245C2AC6918800AEF23D /* XcmV3Junction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmV3Junction.swift; sourceTree = ""; }; 0CCA245E2AC6974200AEF23D /* XcmV3Multilocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmV3Multilocation.swift; sourceTree = ""; }; + 0CCA24642AC6B51200AEF23D /* AssetHubSwapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubSwapTests.swift; sourceTree = ""; }; 0CCE25202A44306200286709 /* TransactionHistoryPhishingFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionHistoryPhishingFilter.swift; sourceTree = ""; }; 0CDFFCC54A504417F4ACE7AA /* NftListInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftListInteractor.swift; sourceTree = ""; }; 0CE1504F2A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsPoolSubscriptionService.swift; sourceTree = ""; }; @@ -10792,6 +10794,7 @@ 0C2F86992A72948100593C01 /* NominationPoolsApyTests.swift */, 0C3205C12A868236002EB914 /* EvmGasPriceIntegrationTests.swift */, 0C59E8EA2AA71C3E001E11F3 /* ExternalAssetBalanceIntegrationTests.swift */, + 0CCA24642AC6B51200AEF23D /* AssetHubSwapTests.swift */, ); path = novawalletIntegrationTests; sourceTree = ""; @@ -19079,6 +19082,7 @@ 8479F31426CD9A0E005D8D24 /* ChainRegistryIntegrationTests.swift in Sources */, 844304672A28F7A500DE36DE /* MultistakingSyncTests.swift in Sources */, 84E8AC7727BBC8E400402635 /* NFTIntegrationTests.swift in Sources */, + 0CCA24652AC6B51200AEF23D /* AssetHubSwapTests.swift in Sources */, 849ABE8A262833C000011A2A /* PayoutRewardsServiceTests.swift in Sources */, 8461CC8526BC1306007460E4 /* MortalEraFactoryTests.swift in Sources */, 840874E02978882700ACFA55 /* Gov2DelegationTests.swift in Sources */, diff --git a/novawallet/Common/Model/KnownChainIds.swift b/novawallet/Common/Model/KnownChainIds.swift index b308e7f7c8..e530b57b21 100644 --- a/novawallet/Common/Model/KnownChainIds.swift +++ b/novawallet/Common/Model/KnownChainIds.swift @@ -25,6 +25,7 @@ enum KnowChainId { static let ethereum = "eip155:1" static let rococo = "a84b46a3e602245284bb9a72c4abd58ee979aa7a5d7f8c4dfdddfaaf0665a4ae" static let westend = "e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e" + static let westmint = "67f9723393ef76214df0118c34bbbd3dbebc8ed46a10973a8c969d48fe7598c9" static var kiltOnEnviroment: String { #if F_DEV diff --git a/novawallet/Common/Protocols/JSONListConvertible.swift b/novawallet/Common/Protocols/JSONListConvertible.swift index 03148f3eb8..b6e908bcf1 100644 --- a/novawallet/Common/Protocols/JSONListConvertible.swift +++ b/novawallet/Common/Protocols/JSONListConvertible.swift @@ -3,6 +3,7 @@ import SubstrateSdk enum JSONListConvertibleError: Error { case unexpectedNumberOfItems(expected: Int, actual: Int) + case unexpectedValue(JSON) } protocol JSONListConvertible { diff --git a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift index f10edab12c..a0d8b251e9 100644 --- a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift +++ b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift @@ -50,7 +50,7 @@ enum AssetConversionPallet { let asset2: PoolAsset init(jsonList: [JSON], context: [CodingUserInfoKey: Any]?) throws { - let expectedFieldsCount = 2 + let expectedFieldsCount = 1 let actualFieldsCount = jsonList.count guard expectedFieldsCount == actualFieldsCount else { throw JSONListConvertibleError.unexpectedNumberOfItems( @@ -59,8 +59,12 @@ enum AssetConversionPallet { ) } - let multilocation1 = try jsonList[0].map(to: XcmV3.Multilocation.self, with: context) - let multilocation2 = try jsonList[1].map(to: XcmV3.Multilocation.self, with: context) + guard let poolId = jsonList[0].arrayValue, poolId.count == 2 else { + throw JSONListConvertibleError.unexpectedValue(jsonList[0]) + } + + let multilocation1 = try poolId[0].map(to: XcmV3.Multilocation.self, with: context) + let multilocation2 = try poolId[1].map(to: XcmV3.Multilocation.self, with: context) asset1 = PoolAsset(multilocation: multilocation1) asset2 = PoolAsset(multilocation: multilocation2) diff --git a/novawallet/Common/Substrate/Types/Xcm/V3/XcmV3Junction.swift b/novawallet/Common/Substrate/Types/Xcm/V3/XcmV3Junction.swift index bfd05ab7a4..0cb24c39e4 100644 --- a/novawallet/Common/Substrate/Types/Xcm/V3/XcmV3Junction.swift +++ b/novawallet/Common/Substrate/Types/Xcm/V3/XcmV3Junction.swift @@ -124,6 +124,9 @@ extension XcmV3 { case Self.generalKeyField: let key = try container.decode(BytesCodable.self).wrappedValue self = .generalKey(key) + case Self.generalIndexField: + let index = try container.decode(StringScaleMapper.self).value + self = .generalIndex(index) case Self.onlyChildKey: self = .onlyChild case Self.globalConsensusField: diff --git a/novawallet/Common/Substrate/Types/Xcm/XcmJunction.swift b/novawallet/Common/Substrate/Types/Xcm/XcmJunction.swift index 66315b15d4..fadc54df65 100644 --- a/novawallet/Common/Substrate/Types/Xcm/XcmJunction.swift +++ b/novawallet/Common/Substrate/Types/Xcm/XcmJunction.swift @@ -119,6 +119,9 @@ extension Xcm { case Self.palletInstanceField: let index = try container.decode(StringScaleMapper.self).value self = .palletInstance(index) + case Self.generalIndexField: + let index = try container.decode(StringScaleMapper.self).value + self = .generalIndex(index) case Self.generalKeyField: let key = try container.decode(BytesCodable.self).wrappedValue self = .generalKey(key) @@ -189,7 +192,7 @@ extension Xcm { type.count == 2, type.starts(with: JunctionsConstants.junctionPrefix), let itemsCount = Int(type.suffix(1)) { - if itemsCount > 1 { + if itemsCount == 1 { let item = try container.decode(J.self) items = [item] } else { diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift index 940d0c9d5e..810f341d6f 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift @@ -24,13 +24,28 @@ final class AssetHubSwapOperationFactory { private func fetchAllPairsWrapper( dependingOn codingFactoryOperation: BaseOperation ) -> CompoundOperationWrapper<[AssetConversionPallet.PoolAssetPair]> { + let prefixEncodingOperation = UnkeyedEncodingOperation( + path: AssetConversionPallet.poolsPath, + storageKeyFactory: StorageKeyFactory() + ) + + prefixEncodingOperation.configurationBlock = { + do { + prefixEncodingOperation.codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + } catch { + prefixEncodingOperation.result = .failure(error) + } + } + let keysFetchOperation = StorageKeysQueryService( connection: connection, operationManager: OperationManager(operationQueue: operationQueue), - prefixKeyClosure: { Data() }, + prefixKeyClosure: { try prefixEncodingOperation.extractNoCancellableResultData() }, mapper: AnyMapper(mapper: IdentityMapper()) ).longrunOperation() + keysFetchOperation.addDependency(prefixEncodingOperation) + let decodingOperation = StorageKeyDecodingOperation( path: AssetConversionPallet.poolsPath ) @@ -46,7 +61,10 @@ final class AssetHubSwapOperationFactory { decodingOperation.addDependency(keysFetchOperation) - return CompoundOperationWrapper(targetOperation: decodingOperation, dependencies: [keysFetchOperation]) + return CompoundOperationWrapper( + targetOperation: decodingOperation, + dependencies: [prefixEncodingOperation, keysFetchOperation] + ) } private func mapRemotePairsOperation( @@ -144,7 +162,10 @@ extension AssetHubSwapOperationFactory: AssetConversionOperationFactoryProtocol mappingOperation.addDependency(allDirectionsWrapper.targetOperation) - return CompoundOperationWrapper(targetOperation: mappingOperation, dependencies: allDirectionsWrapper.allOperations) + return CompoundOperationWrapper( + targetOperation: mappingOperation, + dependencies: allDirectionsWrapper.allOperations + ) } func quote(for _: AssetConversion.Args) -> CompoundOperationWrapper { diff --git a/novawalletIntegrationTests/AssetHubSwapTests.swift b/novawalletIntegrationTests/AssetHubSwapTests.swift new file mode 100644 index 0000000000..e214e95b49 --- /dev/null +++ b/novawalletIntegrationTests/AssetHubSwapTests.swift @@ -0,0 +1,72 @@ +import XCTest +@testable import novawallet + +final class AssetHubSwapTests: XCTestCase { + func testWestmintAllDirections() throws { + let directions = try performAvailableDirectionsFetch( + for: KnowChainId.westmint, + assetId: nil + ) + + Logger.shared.info("Directions: \(directions)") + } + + func testWestmintNativeDirections() throws { + let directions = try performAvailableDirectionsFetch( + for: KnowChainId.westmint, + assetId: 0 + ) + + Logger.shared.info("Directions: \(directions)") + } + + func testWestmintSiriDirections() throws { + let directions = try performAvailableDirectionsFetch( + for: KnowChainId.westmint, + assetId: 1 + ) + + Logger.shared.info("Directions: \(directions)") + } + + private func performAvailableDirectionsFetch( + for chainId: ChainModel.Id, + assetId: AssetModel.Id? + ) throws -> [ChainAssetId: Set] { + let storageFacade = SubstrateStorageTestFacade() + let chainRegistry = ChainRegistryFacade.setupForIntegrationTest(with: storageFacade) + + guard + let chain = chainRegistry.getChain(for: chainId), + let connection = chainRegistry.getConnection(for: chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + throw ChainRegistryError.noChain(chainId) + } + + let operationQueue = OperationQueue() + + let operationFactory = AssetHubSwapOperationFactory( + chain: chain, + runtimeService: runtimeService, + connection: connection, + operationQueue: operationQueue + ) + + if let assetId = assetId { + let chainAssetId = ChainAssetId(chainId: chainId, assetId: assetId) + let wrapper = operationFactory.availableDirectionsForAsset(chainAssetId) + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: true) + + let directions = try wrapper.targetOperation.extractNoCancellableResultData() + + return [chainAssetId: directions] + } else { + let wrapper = operationFactory.availableDirections() + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: true) + + return try wrapper.targetOperation.extractNoCancellableResultData() + } + } +} From ef5712c919632065583ff2fe1c8a7bb98cacc41a Mon Sep 17 00:00:00 2001 From: ERussel Date: Sat, 30 Sep 2023 11:35:39 +0500 Subject: [PATCH 005/204] token conversion --- novawallet.xcodeproj/project.pbxproj | 4 ++ .../AssetHubSwapOperationFactory.swift | 32 ++++++++++++++- .../AssetHub/AssetHubTokensConverter.swift | 39 +++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 75408e1d5f..7d856c4350 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -204,6 +204,7 @@ 0C9C64362A8D67FB004DC078 /* StakingNPoolsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C64352A8D67FB004DC078 /* StakingNPoolsProtocols.swift */; }; 0C9C64382A8D6949004DC078 /* NPoolsStakingSharedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C64372A8D6949004DC078 /* NPoolsStakingSharedState.swift */; }; 0C9C643A2A8DF97E004DC078 /* StakingNPoolsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C64392A8DF97E004DC078 /* StakingNPoolsError.swift */; }; + 0C9D87AE2AC708070095FE8C /* AssetHubTokensConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9D87AD2AC708070095FE8C /* AssetHubTokensConverter.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 */; }; @@ -4173,6 +4174,7 @@ 0C9C64352A8D67FB004DC078 /* StakingNPoolsProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingNPoolsProtocols.swift; sourceTree = ""; }; 0C9C64372A8D6949004DC078 /* NPoolsStakingSharedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsStakingSharedState.swift; sourceTree = ""; }; 0C9C64392A8DF97E004DC078 /* StakingNPoolsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingNPoolsError.swift; sourceTree = ""; }; + 0C9D87AD2AC708070095FE8C /* AssetHubTokensConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubTokensConverter.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 = ""; }; @@ -8167,6 +8169,7 @@ isa = PBXGroup; children = ( 0C0CB3842AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift */, + 0C9D87AD2AC708070095FE8C /* AssetHubTokensConverter.swift */, ); path = AssetHub; sourceTree = ""; @@ -19651,6 +19654,7 @@ 887AFC8E28BCB314002A0422 /* SelectableIconSubtitleView.swift in Sources */, 849A4EF8279ABBDD00AB6709 /* AssetBalance.swift in Sources */, 88A0E10128A284C800A9C940 /* Observable.swift in Sources */, + 0C9D87AE2AC708070095FE8C /* AssetHubTokensConverter.swift in Sources */, 84B8AA7529F8FD2400347A37 /* DAppInteractionFactory.swift in Sources */, 844EFB65265FD61D0090ACB1 /* CrowdloanContributeConfirmViewModel.swift in Sources */, AEE5FAFF26415E0C002B8FDC /* StakingRebondSetupPresenter.swift in Sources */, diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift index 810f341d6f..516c9ad0e3 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift @@ -4,6 +4,9 @@ import SubstrateSdk import BigInt final class AssetHubSwapOperationFactory { + static let sellQuoteApi = "AssetConversionApi_quote_price_exact_tokens_for_tokens" + static let buyQuoteApi = "AssetConversionApi_quote_price_exact_tokens_for_tokens" + let chain: ChainModel let runtimeService: RuntimeCodingServiceProtocol let connection: JSONRPCEngine @@ -20,6 +23,18 @@ final class AssetHubSwapOperationFactory { self.connection = connection self.operationQueue = operationQueue } + + private func convertAssetToMultilocation(_ assetId: AssetModel.Id) -> XcmV3.Multilocation? { + guard let asset = chain.asset(for: assetId) else { + return nil + } + + guard !asset.isUtility else { + return .init(parents: 0, interior: .init(items: [])) + } + + + } private func fetchAllPairsWrapper( dependingOn codingFactoryOperation: BaseOperation @@ -168,7 +183,20 @@ extension AssetHubSwapOperationFactory: AssetConversionOperationFactoryProtocol ) } - func quote(for _: AssetConversion.Args) -> CompoundOperationWrapper { - CompoundOperationWrapper.createWithError(CommonError.undefined) + func quote(for args: AssetConversion.Args) -> CompoundOperationWrapper { + let builtInFunction: String + + switch args.direction { + case .sell: + builtInFunction = Self.sellQuoteApi + case .buy: + builtInFunction = Self.buyQuoteApi + } + + StateCallRpc.Request(builtInFunction: builtInFunction) { container in + + container.encode(args.amount.toHexWithPrefix()) + container.encode(false) + } } } diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift new file mode 100644 index 0000000000..87a1443ad3 --- /dev/null +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift @@ -0,0 +1,39 @@ +import Foundation +import SubstrateSdk +import BigInt + +struct AssetHubToken { + let assetId: ChainAssetId + let extras: StatemineAssetExtras +} + +enum AssetHubTokensConverter { + static func convertToMultilocation( + asset: AssetModel, + codingFactory: RuntimeCoderFactoryProtocol + ) -> XcmV3.Multilocation? { + guard let storageInfo = try? AssetStorageInfo.extract(from: asset, codingFactory: codingFactory) else { + return nil + } + + switch storageInfo { + case .native(let info): + return .init(parents: 0, interior: .init(items: [])) + case let .statemine(extras): + let palletName = extras.palletName ?? PalletAssets.name + + guard + let palletIndex = codingFactory.metadata.getModuleIndex(palletName), + let generalIndex = BigUInt(extras.assetId) else { + return nil + } + + let palletJunction = XcmV3.Junction.palletInstance(palletIndex) + let generalIndexJunction = XcmV3.Junction.generalIndex(generalIndex) + + return .init(parents: 0, interior: [palletJunction, generalIndex]) + default: + return nil + } + } +} From 1c906e0737850c8898a34b4d5f0766738ecfe0d9 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 2 Oct 2023 12:27:45 +0500 Subject: [PATCH 006/204] add quote logic --- novawallet.xcodeproj/project.pbxproj | 4 + .../AssetConversionPallet+Path.swift | 4 + .../AssetConversionPallet.swift | 8 +- .../Model/AssetConversion.swift | 19 +- .../AssetHubSwapOperationFactory.swift | 56 +++--- .../AssetHub/AssetHubSwapQuoteBuilder.swift | 178 ++++++++++++++++++ .../AssetHub/AssetHubTokensConverter.swift | 28 ++- .../AssetHubSwapTests.swift | 88 +++++++++ 8 files changed, 344 insertions(+), 41 deletions(-) create mode 100644 novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapQuoteBuilder.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 7d856c4350..2f6ff5a709 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ 0C21D77C2AB42A3500EB2DBD /* ScreenOpenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C21D77B2AB42A3500EB2DBD /* ScreenOpenService.swift */; }; 0C21D77E2AB42D4100EB2DBD /* UrlHandlingAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C21D77D2AB42D4100EB2DBD /* UrlHandlingAction.swift */; }; 0C21D7802AB432B100EB2DBD /* MainTabBarIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C21D77F2AB432B100EB2DBD /* MainTabBarIndex.swift */; }; + 0C2200692ACA80BB0067BA61 /* AssetHubSwapQuoteBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2200682ACA80BB0067BA61 /* AssetHubSwapQuoteBuilder.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 */; }; @@ -4045,6 +4046,7 @@ 0C21D77B2AB42A3500EB2DBD /* ScreenOpenService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenOpenService.swift; sourceTree = ""; }; 0C21D77D2AB42D4100EB2DBD /* UrlHandlingAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlHandlingAction.swift; sourceTree = ""; }; 0C21D77F2AB432B100EB2DBD /* MainTabBarIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarIndex.swift; sourceTree = ""; }; + 0C2200682ACA80BB0067BA61 /* AssetHubSwapQuoteBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubSwapQuoteBuilder.swift; sourceTree = ""; }; 0C2437D345C3D9B12AEE1E28 /* ParaStkYieldBoostSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostSetupProtocols.swift; sourceTree = ""; }; 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 = ""; }; @@ -8170,6 +8172,7 @@ children = ( 0C0CB3842AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift */, 0C9D87AD2AC708070095FE8C /* AssetHubTokensConverter.swift */, + 0C2200682ACA80BB0067BA61 /* AssetHubSwapQuoteBuilder.swift */, ); path = AssetHub; sourceTree = ""; @@ -21163,6 +21166,7 @@ 847EA1D62A1CA47500F1CBD8 /* SubqueryMultistaking.swift in Sources */, 8485D924277E16C400767243 /* DAppBrowserScriptHandler.swift in Sources */, 8427495528FEB92700B2B70B /* GovernanceLockStateFactory.swift in Sources */, + 0C2200692ACA80BB0067BA61 /* AssetHubSwapQuoteBuilder.swift in Sources */, 0C9680F32A8AC2F2006A411B /* EvmTokenAddResult.swift in Sources */, 842BDB2C278C4FFE00AB4B5A /* DAppBrowserAuthorizedState.swift in Sources */, 849346AF2A1F7F7D00CB75B7 /* MultistakingServices.swift in Sources */, diff --git a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet+Path.swift b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet+Path.swift index be192819d5..c85957ffc5 100644 --- a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet+Path.swift +++ b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet+Path.swift @@ -8,4 +8,8 @@ extension AssetConversionPallet { static func getPoolsPath(for moduleName: String) -> StorageCodingPath { StorageCodingPath(moduleName: moduleName, itemName: "Pools") } + + static func addLiquidityCallPath(for moduleName: String) -> CallCodingPath { + CallCodingPath(moduleName: moduleName, callName: "add_liquidity") + } } diff --git a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift index a0d8b251e9..a1bb537e65 100644 --- a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift +++ b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift @@ -5,11 +5,13 @@ import BigInt enum AssetConversionPallet { static let name = "AssetConversion" + typealias AssetId = XcmV3.Multilocation + enum PoolAsset { case native case assets(pallet: UInt8, index: BigUInt) case foreignNetwork(XcmV3.NetworkId) - case undefined(XcmV3.Multilocation) + case undefined(AssetId) init(multilocation: XcmV3.Multilocation) { let junctions = multilocation.interior.items @@ -63,8 +65,8 @@ enum AssetConversionPallet { throw JSONListConvertibleError.unexpectedValue(jsonList[0]) } - let multilocation1 = try poolId[0].map(to: XcmV3.Multilocation.self, with: context) - let multilocation2 = try poolId[1].map(to: XcmV3.Multilocation.self, with: context) + let multilocation1 = try poolId[0].map(to: AssetId.self, with: context) + let multilocation2 = try poolId[1].map(to: AssetId.self, with: context) asset1 = PoolAsset(multilocation: multilocation1) asset2 = PoolAsset(multilocation: multilocation2) diff --git a/novawallet/Modules/AssetConversion/Model/AssetConversion.swift b/novawallet/Modules/AssetConversion/Model/AssetConversion.swift index cc292d46cc..7ec0892697 100644 --- a/novawallet/Modules/AssetConversion/Model/AssetConversion.swift +++ b/novawallet/Modules/AssetConversion/Model/AssetConversion.swift @@ -20,12 +20,19 @@ enum AssetConversion { let assetIn: ChainAssetId let amountOut: BigUInt let assetOut: ChainAssetId - let priceImpact: Decimal - let fee: Fee - } - struct Fee { - let serviceFee: Decimal - let novaFee: Decimal? + init(args: Args, amount: BigUInt) { + switch args.direction { + case .sell: + amountIn = args.amount + amountOut = amount + case .buy: + amountIn = amount + amountOut = args.amount + } + + assetIn = args.assetIn + assetOut = args.assetOut + } } } diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift index 516c9ad0e3..802ad7a51c 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift @@ -6,7 +6,7 @@ import BigInt final class AssetHubSwapOperationFactory { static let sellQuoteApi = "AssetConversionApi_quote_price_exact_tokens_for_tokens" static let buyQuoteApi = "AssetConversionApi_quote_price_exact_tokens_for_tokens" - + let chain: ChainModel let runtimeService: RuntimeCodingServiceProtocol let connection: JSONRPCEngine @@ -23,18 +23,6 @@ final class AssetHubSwapOperationFactory { self.connection = connection self.operationQueue = operationQueue } - - private func convertAssetToMultilocation(_ assetId: AssetModel.Id) -> XcmV3.Multilocation? { - guard let asset = chain.asset(for: assetId) else { - return nil - } - - guard !asset.isUtility else { - return .init(parents: 0, interior: .init(items: [])) - } - - - } private func fetchAllPairsWrapper( dependingOn codingFactoryOperation: BaseOperation @@ -184,19 +172,37 @@ extension AssetHubSwapOperationFactory: AssetConversionOperationFactoryProtocol } func quote(for args: AssetConversion.Args) -> CompoundOperationWrapper { - let builtInFunction: String - - switch args.direction { - case .sell: - builtInFunction = Self.sellQuoteApi - case .buy: - builtInFunction = Self.buyQuoteApi + let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() + + let request = AssetHubSwapRequestBuilder(chain: chain).build(args: args) { + try codingFactoryOperation.extractNoCancellableResultData() } - - StateCallRpc.Request(builtInFunction: builtInFunction) { container in - - container.encode(args.amount.toHexWithPrefix()) - container.encode(false) + + let quoteOperation = JSONRPCOperation( + engine: connection, + method: StateCallRpc.method + ) + + quoteOperation.parameters = request + + quoteOperation.addDependency(codingFactoryOperation) + + let mappingOperation = ClosureOperation { + let responseString = try quoteOperation.extractNoCancellableResultData() + let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + + let amount = try AssetHubSwapRequestSerializer.deserialize( + quoteResponse: responseString, + codingFactory: codingFactory + ) + + return .init(args: args, amount: amount) } + + mappingOperation.addDependency(quoteOperation) + + let dependencies = [codingFactoryOperation, quoteOperation] + + return CompoundOperationWrapper(targetOperation: mappingOperation, dependencies: dependencies) } } diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapQuoteBuilder.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapQuoteBuilder.swift new file mode 100644 index 0000000000..7a7a96937a --- /dev/null +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapQuoteBuilder.swift @@ -0,0 +1,178 @@ +import Foundation +import SubstrateSdk +import BigInt + +enum AssetHubSwapRequestBuilderError: Error { + case brokenAssetIn(ChainAssetId) + case brokenAssetOut(ChainAssetId) +} + +enum AssetHubSwapRequestSerializerError: Error { + case undefinedAssetType + case undefinedBalanceType + case quoteCalcFailed +} + +enum AssetHubSwapRequestSerializer { + private static func extractAssetType(from codingFactory: RuntimeCoderFactoryProtocol) -> String? { + guard + let call = codingFactory.getCall( + for: AssetConversionPallet.addLiquidityCallPath(for: AssetConversionPallet.name) + ), + !call.arguments.isEmpty else { + return nil + } + + return call.arguments[0].type + } + + private static func extractBalanceType(from codingFactory: RuntimeCoderFactoryProtocol) -> String? { + guard + let call = codingFactory.getCall( + for: AssetConversionPallet.addLiquidityCallPath(for: AssetConversionPallet.name) + ), + call.arguments.count > 2 else { + return nil + } + + return call.arguments[2].type + } + + static func serialize( + asset: AssetConversionPallet.AssetId, + into encoder: inout DynamicScaleEncoding, + codingFactory: RuntimeCoderFactoryProtocol + ) throws { + guard let assetType = extractAssetType(from: codingFactory) else { + throw AssetHubSwapRequestSerializerError.undefinedAssetType + } + + try encoder.append(asset, ofType: assetType, with: codingFactory.createRuntimeJsonContext().toRawContext()) + } + + static func serialize( + amount: BigUInt, + into encoder: inout DynamicScaleEncoding, + codingFactory: RuntimeCoderFactoryProtocol + ) throws { + guard let balanceType = extractBalanceType(from: codingFactory) else { + throw AssetHubSwapRequestSerializerError.undefinedBalanceType + } + + try encoder.append( + StringScaleMapper(value: amount), + ofType: balanceType, + with: codingFactory.createRuntimeJsonContext().toRawContext() + ) + } + + static func deserialize(quoteResponse: String, codingFactory: RuntimeCoderFactoryProtocol) throws -> BigUInt { + guard let balanceType = extractBalanceType(from: codingFactory) else { + throw AssetHubSwapRequestSerializerError.undefinedBalanceType + } + + let data = try Data(hexString: quoteResponse) + + let decoder = try codingFactory.createDecoder(from: data) + + let json: JSON = try decoder.readOption(type: balanceType) + + guard json != .null else { + throw AssetHubSwapRequestSerializerError.quoteCalcFailed + } + + return try json.map( + to: StringScaleMapper.self, + with: codingFactory.createRuntimeJsonContext().toRawContext() + ).value + } +} + +final class AssetHubSwapRequestBuilder { + static let sellQuoteApi = "AssetConversionApi_quote_price_exact_tokens_for_tokens" + static let buyQuoteApi = "AssetConversionApi_quote_price_tokens_for_exact_tokens" + + let chain: ChainModel + + init(chain: ChainModel) { + self.chain = chain + } + + private func createRequest( + for chain: ChainModel, + args: AssetConversion.Args, + builtInFunction: String, + codingClosure: @escaping () throws -> RuntimeCoderFactoryProtocol, + includesFee: Bool + ) -> StateCallRpc.Request { + StateCallRpc.Request(builtInFunction: builtInFunction) { container in + let codingFactory = try codingClosure() + + guard + let remoteAssetIn = AssetHubTokensConverter.converToMultilocation( + chainAssetId: args.assetIn, + chain: chain, + codingFactory: codingFactory + ) else { + throw AssetHubSwapRequestBuilderError.brokenAssetIn(args.assetIn) + } + + guard + let remoteAssetOut = AssetHubTokensConverter.converToMultilocation( + chainAssetId: args.assetOut, + chain: chain, + codingFactory: codingFactory + ) else { + throw AssetHubSwapRequestBuilderError.brokenAssetIn(args.assetOut) + } + + var encoder = codingFactory.createEncoder() + + try AssetHubSwapRequestSerializer.serialize( + asset: remoteAssetIn, + into: &encoder, + codingFactory: codingFactory + ) + + try AssetHubSwapRequestSerializer.serialize( + asset: remoteAssetOut, + into: &encoder, + codingFactory: codingFactory + ) + + try AssetHubSwapRequestSerializer.serialize( + amount: args.amount, + into: &encoder, + codingFactory: codingFactory + ) + + try encoder.appendBool(json: .boolValue(includesFee)) + + let data = try encoder.encode() + + try container.encode(data.toHex(includePrefix: true)) + } + } + + func build( + args: AssetConversion.Args, + codingClosure: @escaping () throws -> RuntimeCoderFactoryProtocol + ) -> StateCallRpc.Request { + let builtInFunction: String + + switch args.direction { + case .sell: + builtInFunction = Self.sellQuoteApi + case .buy: + builtInFunction = Self.buyQuoteApi + } + + return createRequest( + for: chain, + args: args, + builtInFunction: builtInFunction, + codingClosure: codingClosure, + includesFee: true + ) + } +} diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift index 87a1443ad3..c893147a68 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift @@ -8,30 +8,44 @@ struct AssetHubToken { } enum AssetHubTokensConverter { + static func converToMultilocation( + chainAssetId: ChainAssetId, + chain: ChainModel, + codingFactory: RuntimeCoderFactoryProtocol + ) -> AssetConversionPallet.AssetId? { + guard + chain.chainId == chainAssetId.chainId, + let localAsset = chain.asset(for: chainAssetId.assetId) else { + return nil + } + + return convertToMultilocation(asset: localAsset, codingFactory: codingFactory) + } + static func convertToMultilocation( asset: AssetModel, codingFactory: RuntimeCoderFactoryProtocol - ) -> XcmV3.Multilocation? { + ) -> AssetConversionPallet.AssetId? { guard let storageInfo = try? AssetStorageInfo.extract(from: asset, codingFactory: codingFactory) else { return nil } - + switch storageInfo { - case .native(let info): + case let .native(info): return .init(parents: 0, interior: .init(items: [])) case let .statemine(extras): let palletName = extras.palletName ?? PalletAssets.name - + guard let palletIndex = codingFactory.metadata.getModuleIndex(palletName), let generalIndex = BigUInt(extras.assetId) else { return nil } - + let palletJunction = XcmV3.Junction.palletInstance(palletIndex) let generalIndexJunction = XcmV3.Junction.generalIndex(generalIndex) - - return .init(parents: 0, interior: [palletJunction, generalIndex]) + + return .init(parents: 0, interior: .init(items: [palletJunction, generalIndexJunction])) default: return nil } diff --git a/novawalletIntegrationTests/AssetHubSwapTests.swift b/novawalletIntegrationTests/AssetHubSwapTests.swift index e214e95b49..a070e204d2 100644 --- a/novawalletIntegrationTests/AssetHubSwapTests.swift +++ b/novawalletIntegrationTests/AssetHubSwapTests.swift @@ -1,5 +1,6 @@ import XCTest @testable import novawallet +import BigInt final class AssetHubSwapTests: XCTestCase { func testWestmintAllDirections() throws { @@ -29,6 +30,52 @@ final class AssetHubSwapTests: XCTestCase { Logger.shared.info("Directions: \(directions)") } + func testQuoteForWestmintSiriSell() throws { + let quote = try fetchQuote( + for: KnowChainId.westmint, + assetIn: 0, + assetOut: 1, + direction: .sell + ) + + Logger.shared.info("Quote: \(quote)") + } + + func testQuoteForWestmintSiriBuy() throws { + let quote = try fetchQuote( + for: KnowChainId.westmint, + assetIn: 0, + assetOut: 1, + direction: .buy, + amount: 1_000_000 + ) + + Logger.shared.info("Quote: \(quote)") + } + + func testQuoteForSiriWestmintSell() throws { + let quote = try fetchQuote( + for: KnowChainId.westmint, + assetIn: 1, + assetOut: 0, + direction: .sell + ) + + Logger.shared.info("Quote: \(quote)") + } + + func testQuoteForSiriWestmintBuy() throws { + let quote = try fetchQuote( + for: KnowChainId.westmint, + assetIn: 1, + assetOut: 0, + direction: .buy, + amount: 1_000_000 + ) + + Logger.shared.info("Quote: \(quote)") + } + private func performAvailableDirectionsFetch( for chainId: ChainModel.Id, assetId: AssetModel.Id? @@ -69,4 +116,45 @@ final class AssetHubSwapTests: XCTestCase { return try wrapper.targetOperation.extractNoCancellableResultData() } } + + private func fetchQuote( + for chainId: ChainModel.Id, + assetIn: AssetModel.Id, + assetOut: AssetModel.Id, + direction: AssetConversion.Direction, + amount: BigUInt = 1_000_000_000_000 + ) throws -> AssetConversion.Quote { + let storageFacade = SubstrateStorageTestFacade() + let chainRegistry = ChainRegistryFacade.setupForIntegrationTest(with: storageFacade) + + guard + let chain = chainRegistry.getChain(for: chainId), + let connection = chainRegistry.getConnection(for: chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + throw ChainRegistryError.noChain(chainId) + } + + let operationQueue = OperationQueue() + + let operationFactory = AssetHubSwapOperationFactory( + chain: chain, + runtimeService: runtimeService, + connection: connection, + operationQueue: operationQueue + ) + + let args = AssetConversion.Args( + assetIn: .init(chainId: chainId, assetId: assetIn), + assetOut: .init(chainId: chainId, assetId: assetOut), + amount: amount, + direction: direction, + slippage: 0 + ) + + let quoteWrapper = operationFactory.quote(for: args) + + operationQueue.addOperations(quoteWrapper.allOperations, waitUntilFinished: true) + + return try quoteWrapper.targetOperation.extractNoCancellableResultData() + } } From e95123032dda29b2a25d24387a3ecb1df47fd54f Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 2 Oct 2023 18:34:37 +0500 Subject: [PATCH 007/204] implement extrinsic service --- novawallet.xcodeproj/project.pbxproj | 32 +++++++- novawallet/Common/Model/BigRational.swift | 11 +++ .../AssetConversionPallet+Call.swift | 45 ++++++++++++ .../Model/AssetConversion.swift | 15 +++- .../AssetConversionExtrinsicService.swift | 13 ++++ ... => AssetConversionOperationFactory.swift} | 8 +- .../AssetHub/AssetHubExtrinsicService.swift | 73 +++++++++++++++++++ .../AssetHubSwapOperationFactory.swift | 2 +- .../AssetHub/AssetHubSwapQuoteBuilder.swift | 31 +++----- .../AssetHub/AssetHubTokensConverter.swift | 4 +- .../AssetHubSwapTests.swift | 5 +- 11 files changed, 202 insertions(+), 37 deletions(-) create mode 100644 novawallet/Common/Model/BigRational.swift create mode 100644 novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Call.swift create mode 100644 novawallet/Modules/AssetConversion/Service/AssetConversionExtrinsicService.swift rename novawallet/Modules/AssetConversion/Service/{AssetConversionServiceProtocol.swift => AssetConversionOperationFactory.swift} (55%) create mode 100644 novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubExtrinsicService.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 2f6ff5a709..d013196201 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -40,7 +40,7 @@ 0B48B02E973CB304B765BBC9 /* ReferendumDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ABAD23C0039AFA8351C650 /* ReferendumDetailsProtocols.swift */; }; 0B65DAE0327678679CACE0B1 /* GovernanceDelegateInfoViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E894D4633D04AD4415CE1F2 /* GovernanceDelegateInfoViewFactory.swift */; }; 0C0CB37F2AC540B200EAC516 /* AssetConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB37E2AC540B200EAC516 /* AssetConversion.swift */; }; - 0C0CB3822AC545A800EAC516 /* AssetConversionServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3812AC545A800EAC516 /* AssetConversionServiceProtocol.swift */; }; + 0C0CB3822AC545A800EAC516 /* AssetConversionExtrinsicService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3812AC545A800EAC516 /* AssetConversionExtrinsicService.swift */; }; 0C0CB3852AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3842AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift */; }; 0C0CB3882AC5688100EAC516 /* AssetConversionPallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3872AC5688100EAC516 /* AssetConversionPallet.swift */; }; 0C0CB38A2AC56A1600EAC516 /* AssetConversionPallet+Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3892AC56A1600EAC516 /* AssetConversionPallet+Path.swift */; }; @@ -82,6 +82,7 @@ 0C21D77E2AB42D4100EB2DBD /* UrlHandlingAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C21D77D2AB42D4100EB2DBD /* UrlHandlingAction.swift */; }; 0C21D7802AB432B100EB2DBD /* MainTabBarIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C21D77F2AB432B100EB2DBD /* MainTabBarIndex.swift */; }; 0C2200692ACA80BB0067BA61 /* AssetHubSwapQuoteBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2200682ACA80BB0067BA61 /* AssetHubSwapQuoteBuilder.swift */; }; + 0C22006E2ACAAC2F0067BA61 /* AssetConversionPallet+Call.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C22006D2ACAAC2F0067BA61 /* AssetConversionPallet+Call.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 */; }; @@ -248,6 +249,9 @@ 0CCA24652AC6B51200AEF23D /* AssetHubSwapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA24642AC6B51200AEF23D /* AssetHubSwapTests.swift */; }; 0CCE25212A44306200286709 /* TransactionHistoryPhishingFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCE25202A44306200286709 /* TransactionHistoryPhishingFilter.swift */; }; 0CD1F4D100ED82D137AB9834 /* ParaStkStakeSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2F1EEBF48485F02BF690A4 /* ParaStkStakeSetupViewController.swift */; }; + 0CD352932ACAD7A500B3E446 /* AssetHubExtrinsicService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD352922ACAD7A500B3E446 /* AssetHubExtrinsicService.swift */; }; + 0CD352952ACAF59900B3E446 /* BigRational.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD352942ACAF59900B3E446 /* BigRational.swift */; }; + 0CD352972ACAFADA00B3E446 /* AssetConversionOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD352962ACAFADA00B3E446 /* AssetConversionOperationFactory.swift */; }; 0CE150502A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE1504F2A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift */; }; 0CE150542A70EA2200B61CC1 /* NominationPoolsSyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE150532A70EA2200B61CC1 /* NominationPoolsSyncTests.swift */; }; 0CE550B32A49658700F0A7AC /* StakingDuration+Localizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE550B22A49658700F0A7AC /* StakingDuration+Localizable.swift */; }; @@ -4005,7 +4009,7 @@ 0BA85EE628D7029AC940DFA3 /* NominationPoolBondMoreBasePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreBasePresenter.swift; sourceTree = ""; }; 0C04290B2A67A42A00C3583A /* SubstrateDataModel17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel17.xcdatamodel; sourceTree = ""; }; 0C0CB37E2AC540B200EAC516 /* AssetConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversion.swift; sourceTree = ""; }; - 0C0CB3812AC545A800EAC516 /* AssetConversionServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionServiceProtocol.swift; sourceTree = ""; }; + 0C0CB3812AC545A800EAC516 /* AssetConversionExtrinsicService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionExtrinsicService.swift; sourceTree = ""; }; 0C0CB3842AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubSwapOperationFactory.swift; sourceTree = ""; }; 0C0CB3872AC5688100EAC516 /* AssetConversionPallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionPallet.swift; sourceTree = ""; }; 0C0CB3892AC56A1600EAC516 /* AssetConversionPallet+Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetConversionPallet+Path.swift"; sourceTree = ""; }; @@ -4047,6 +4051,7 @@ 0C21D77D2AB42D4100EB2DBD /* UrlHandlingAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlHandlingAction.swift; sourceTree = ""; }; 0C21D77F2AB432B100EB2DBD /* MainTabBarIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarIndex.swift; sourceTree = ""; }; 0C2200682ACA80BB0067BA61 /* AssetHubSwapQuoteBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubSwapQuoteBuilder.swift; sourceTree = ""; }; + 0C22006D2ACAAC2F0067BA61 /* AssetConversionPallet+Call.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetConversionPallet+Call.swift"; sourceTree = ""; }; 0C2437D345C3D9B12AEE1E28 /* ParaStkYieldBoostSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostSetupProtocols.swift; sourceTree = ""; }; 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 = ""; }; @@ -4219,6 +4224,9 @@ 0CCA245E2AC6974200AEF23D /* XcmV3Multilocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmV3Multilocation.swift; sourceTree = ""; }; 0CCA24642AC6B51200AEF23D /* AssetHubSwapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubSwapTests.swift; sourceTree = ""; }; 0CCE25202A44306200286709 /* TransactionHistoryPhishingFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionHistoryPhishingFilter.swift; sourceTree = ""; }; + 0CD352922ACAD7A500B3E446 /* AssetHubExtrinsicService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubExtrinsicService.swift; sourceTree = ""; }; + 0CD352942ACAF59900B3E446 /* BigRational.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BigRational.swift; sourceTree = ""; }; + 0CD352962ACAFADA00B3E446 /* AssetConversionOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionOperationFactory.swift; sourceTree = ""; }; 0CDFFCC54A504417F4ACE7AA /* NftListInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftListInteractor.swift; sourceTree = ""; }; 0CE1504F2A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsPoolSubscriptionService.swift; sourceTree = ""; }; 0CE150532A70EA2200B61CC1 /* NominationPoolsSyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsSyncTests.swift; sourceTree = ""; }; @@ -8162,7 +8170,8 @@ isa = PBXGroup; children = ( 0C0CB3832AC561CA00EAC516 /* AssetHub */, - 0C0CB3812AC545A800EAC516 /* AssetConversionServiceProtocol.swift */, + 0C0CB3812AC545A800EAC516 /* AssetConversionExtrinsicService.swift */, + 0CD352962ACAFADA00B3E446 /* AssetConversionOperationFactory.swift */, ); path = Service; sourceTree = ""; @@ -8173,6 +8182,7 @@ 0C0CB3842AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift */, 0C9D87AD2AC708070095FE8C /* AssetHubTokensConverter.swift */, 0C2200682ACA80BB0067BA61 /* AssetHubSwapQuoteBuilder.swift */, + 0CD352922ACAD7A500B3E446 /* AssetHubExtrinsicService.swift */, ); path = AssetHub; sourceTree = ""; @@ -8247,6 +8257,14 @@ path = Model; sourceTree = ""; }; + 0C22006C2ACAAC0F0067BA61 /* AssetConversionPallet */ = { + isa = PBXGroup; + children = ( + 0C22006D2ACAAC2F0067BA61 /* AssetConversionPallet+Call.swift */, + ); + path = AssetConversionPallet; + sourceTree = ""; + }; 0C2F86872A723E4200593C01 /* NominationPools */ = { isa = PBXGroup; children = ( @@ -11529,6 +11547,7 @@ 845BB8C725E45D0600E5FCDC /* Calls */ = { isa = PBXGroup; children = ( + 0C22006C2ACAAC0F0067BA61 /* AssetConversionPallet */, 0C7945BC2ABB22AA001C07CA /* XTokens */, 0C13D3052A7FB9170054BB6F /* NominationPools */, 843ADAC12A38FC4C003AE2B5 /* Staking */, @@ -12950,6 +12969,7 @@ 8455F1A32A1F606B003F072D /* OnchainStorage.swift */, 0C3205EB2A8A122D002EB914 /* FeeOutputModel.swift */, 0C59E8D02AA5FAC5001E11F3 /* PooledAssetBalance.swift */, + 0CD352942ACAF59900B3E446 /* BigRational.swift */, ); path = Model; sourceTree = ""; @@ -19339,6 +19359,7 @@ 842D1E8624D1A90400C30A7A /* NSPredicate+Validation.swift in Sources */, 8887814028B7AAB700E7290F /* RoundedIconTitleView.swift in Sources */, 8860F3E2289D4FFD00C0BF86 /* SectionProtocol.swift in Sources */, + 0C22006E2ACAAC2F0067BA61 /* AssetConversionPallet+Call.swift in Sources */, F4FDA0F826A57626003D753B /* BabeEraOperationFactory.swift in Sources */, 84AE7AB927D3F96300495267 /* RMRKV1Collection.swift in Sources */, 84AE7AAF27D38B1800495267 /* DrawableIconViewModel.swift in Sources */, @@ -21567,7 +21588,7 @@ 8498534F2A17390900993977 /* PalletAssets.swift in Sources */, 845B08042918C308005785D3 /* Gov1ActionOperationFactory.swift in Sources */, F0C3DB0CEE1975626B0014A8 /* StakingUnbondConfirmInteractor.swift in Sources */, - 0C0CB3822AC545A800EAC516 /* AssetConversionServiceProtocol.swift in Sources */, + 0C0CB3822AC545A800EAC516 /* AssetConversionExtrinsicService.swift in Sources */, 849FA21628A26CB500F83EAA /* CountdownTimerMediator.swift in Sources */, D3B48F82A875E301D749AC0B /* StakingUnbondConfirmViewController.swift in Sources */, 842AEB81292F34B600C61B0C /* RemoteChainExternalApi.swift in Sources */, @@ -21722,6 +21743,7 @@ 8407715628CB7D6B007DBD24 /* ParaStkYieldBoostScheduleInteractor.swift in Sources */, 84E2ABCA2992891300A5D3C1 /* GovernanceDelegateInteractor.swift in Sources */, 84D1ABDC27E1B4FE0073C631 /* ChainAssetViewModel.swift in Sources */, + 0CD352952ACAF59900B3E446 /* BigRational.swift in Sources */, AEE0C442272A9AF7009F9AD5 /* BaseChainAccountConfirmInteractor.swift in Sources */, 848CCB462832EF3400A1FD00 /* GeneralLocalStorageSubscriber.swift in Sources */, 84735E7B2881A57700BADC1B /* AssetsSearchFlowLayout.swift in Sources */, @@ -21918,6 +21940,7 @@ 9BADFCBF3AF5186094DB8D67 /* DAppTxDetailsInteractor.swift in Sources */, B409644ED1E20062A3EA0316 /* DAppTxDetailsViewController.swift in Sources */, DAD46B2B29A446C19A6ABF2D /* DAppTxDetailsViewLayout.swift in Sources */, + 0CD352972ACAFADA00B3E446 /* AssetConversionOperationFactory.swift in Sources */, 88AC186328CA3F0000892A9B /* GenericCollectionViewLayout.swift in Sources */, A97F32D057BFEFBCC478A09C /* DAppTxDetailsViewFactory.swift in Sources */, D567BAAF620EDB9F4975C800 /* DAppAuthConfirmProtocols.swift in Sources */, @@ -22022,6 +22045,7 @@ 433A3C2B0D1E4BA5974D681B /* DAppAddFavoriteWireframe.swift in Sources */, F5CA222FA684AAC8B556E667 /* DAppAddFavoritePresenter.swift in Sources */, AC904E313DC15AE40C927946 /* DAppAddFavoriteInteractor.swift in Sources */, + 0CD352932ACAD7A500B3E446 /* AssetHubExtrinsicService.swift in Sources */, 006BEDBD2F98FF54DB993D8C /* DAppAddFavoriteViewController.swift in Sources */, 84FFE45D28620833002432BB /* XcmTransferResolutionService.swift in Sources */, D3F199376DAEBF380C5FFD9D /* DAppAddFavoriteViewLayout.swift in Sources */, diff --git a/novawallet/Common/Model/BigRational.swift b/novawallet/Common/Model/BigRational.swift new file mode 100644 index 0000000000..3db92ab7aa --- /dev/null +++ b/novawallet/Common/Model/BigRational.swift @@ -0,0 +1,11 @@ +import Foundation +import BigInt + +struct BigRational { + let numerator: BigUInt + let denominator: BigUInt + + func mul(value: BigUInt) -> BigUInt { + value * numerator / denominator + } +} diff --git a/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Call.swift b/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Call.swift new file mode 100644 index 0000000000..58ce2ad652 --- /dev/null +++ b/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Call.swift @@ -0,0 +1,45 @@ +import Foundation +import SubstrateSdk +import BigInt + +extension AssetConversionPallet { + struct SwapExactTokensForTokensCall: Codable { + enum CodingKeys: String, CodingKey { + case path + case amountIn = "amount_in" + case amountOutMin = "amount_out_min" + case sendTo = "send_to" + case keepAlive = "keep_alive" + } + + let path: [AssetConversionPallet.AssetId] + @StringCodable var amountIn: BigUInt + @StringCodable var amountOutMin: BigUInt + @BytesCodable var sendTo: AccountId + let keepAlive: Bool + + func runtimeCall(for module: String) -> RuntimeCall { + RuntimeCall(moduleName: module, callName: "swap_exact_tokens_for_tokens", args: self) + } + } + + struct SwapTokensForExactTokensCall: Codable { + enum CodingKeys: String, CodingKey { + case path + case amountOut = "amount_out" + case amountInMax = "amount_in_max" + case sendTo = "send_to" + case keepAlive = "keep_alive" + } + + let path: [AssetConversionPallet.AssetId] + @StringCodable var amountOut: BigUInt + @StringCodable var amountInMax: BigUInt + @BytesCodable var sendTo: AccountId + let keepAlive: Bool + + func runtimeCall(for module: String) -> RuntimeCall { + RuntimeCall(moduleName: module, callName: "swap_tokens_for_exact_tokens", args: self) + } + } +} diff --git a/novawallet/Modules/AssetConversion/Model/AssetConversion.swift b/novawallet/Modules/AssetConversion/Model/AssetConversion.swift index 7ec0892697..e7071b4156 100644 --- a/novawallet/Modules/AssetConversion/Model/AssetConversion.swift +++ b/novawallet/Modules/AssetConversion/Model/AssetConversion.swift @@ -7,12 +7,11 @@ enum AssetConversion { case buy } - struct Args { + struct QuoteArgs { let assetIn: ChainAssetId let assetOut: ChainAssetId let amount: BigUInt let direction: Direction - let slippage: Decimal } struct Quote { @@ -21,7 +20,7 @@ enum AssetConversion { let amountOut: BigUInt let assetOut: ChainAssetId - init(args: Args, amount: BigUInt) { + init(args: QuoteArgs, amount: BigUInt) { switch args.direction { case .sell: amountIn = args.amount @@ -35,4 +34,14 @@ enum AssetConversion { assetOut = args.assetOut } } + + struct CallArgs { + let assetIn: ChainAssetId + let amountIn: BigUInt + let assetOut: ChainAssetId + let amountOut: BigUInt + let receiver: AccountId + let direction: Direction + let slippage: BigRational + } } diff --git a/novawallet/Modules/AssetConversion/Service/AssetConversionExtrinsicService.swift b/novawallet/Modules/AssetConversion/Service/AssetConversionExtrinsicService.swift new file mode 100644 index 0000000000..e1f0846c42 --- /dev/null +++ b/novawallet/Modules/AssetConversion/Service/AssetConversionExtrinsicService.swift @@ -0,0 +1,13 @@ +import Foundation +import RobinHood + +protocol AssetConversionExtrinsicServiceProtocol { + func fetchExtrinsicBuilderClosure( + for args: AssetConversion.CallArgs, + codingFactory: RuntimeCoderFactoryProtocol + ) -> ExtrinsicBuilderClosure +} + +enum AssetConversionExtrinsicServiceError: Error { + case remoteAssetNotFound(ChainAssetId) +} diff --git a/novawallet/Modules/AssetConversion/Service/AssetConversionServiceProtocol.swift b/novawallet/Modules/AssetConversion/Service/AssetConversionOperationFactory.swift similarity index 55% rename from novawallet/Modules/AssetConversion/Service/AssetConversionServiceProtocol.swift rename to novawallet/Modules/AssetConversion/Service/AssetConversionOperationFactory.swift index e03fc84a80..dbff0fddb9 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetConversionServiceProtocol.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetConversionOperationFactory.swift @@ -4,9 +4,11 @@ import RobinHood protocol AssetConversionOperationFactoryProtocol { func availableDirections() -> CompoundOperationWrapper<[ChainAssetId: Set]> func availableDirectionsForAsset(_ chainAssetId: ChainAssetId) -> CompoundOperationWrapper> - func quote(for args: AssetConversion.Args) -> CompoundOperationWrapper + func quote(for args: AssetConversion.QuoteArgs) -> CompoundOperationWrapper } -protocol AssetConversionServiceProtocol { - func fetchExtrinsicBuilderClosure(for args: AssetConversion.Args) -> ExtrinsicBuilderClosure +enum AssetConversionOperationError: Error { + case remoteAssetNotFound(ChainAssetId) + case runtimeError(String) + case quoteCalcFailed } diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubExtrinsicService.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubExtrinsicService.swift new file mode 100644 index 0000000000..246bd1dc29 --- /dev/null +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubExtrinsicService.swift @@ -0,0 +1,73 @@ +import Foundation +import SubstrateSdk + +final class AssetHubExtrinsicService { + let chain: ChainModel + + init(chain: ChainModel) { + self.chain = chain + } + + private func fetchExtrinsicBuilderClosure( + for args: AssetConversion.CallArgs, + codingFactory: RuntimeCoderFactoryProtocol, + chain: ChainModel + ) -> ExtrinsicBuilderClosure { + { builder in + + guard + let remoteAssetIn = AssetHubTokensConverter.convertToMultilocation( + chainAssetId: args.assetIn, + chain: chain, + codingFactory: codingFactory + ) else { + throw AssetConversionExtrinsicServiceError.remoteAssetNotFound(args.assetIn) + } + + guard + let remoteAssetOut = AssetHubTokensConverter.convertToMultilocation( + chainAssetId: args.assetOut, + chain: chain, + codingFactory: codingFactory + ) else { + throw AssetConversionExtrinsicServiceError.remoteAssetNotFound(args.assetOut) + } + + switch args.direction { + case .sell: + let amountOutMin = args.amountOut - args.slippage.mul(value: args.amountOut) + + let call = AssetConversionPallet.SwapExactTokensForTokensCall( + path: [remoteAssetIn, remoteAssetOut], + amountIn: args.amountIn, + amountOutMin: amountOutMin, + sendTo: args.receiver, + keepAlive: false + ) + + return try builder.adding(call: call.runtimeCall(for: AssetConversionPallet.name)) + case .buy: + let amountInMax = args.amountIn + args.slippage.mul(value: args.amountIn) + + let call = AssetConversionPallet.SwapTokensForExactTokensCall( + path: [remoteAssetIn, remoteAssetOut], + amountOut: args.amountOut, + amountInMax: amountInMax, + sendTo: args.receiver, + keepAlive: false + ) + + return try builder.adding(call: call.runtimeCall(for: AssetConversionPallet.name)) + } + } + } +} + +extension AssetHubExtrinsicService: AssetConversionExtrinsicServiceProtocol { + func fetchExtrinsicBuilderClosure( + for args: AssetConversion.CallArgs, + codingFactory: RuntimeCoderFactoryProtocol + ) -> ExtrinsicBuilderClosure { + fetchExtrinsicBuilderClosure(for: args, codingFactory: codingFactory, chain: chain) + } +} diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift index 802ad7a51c..712f04b1a4 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift @@ -171,7 +171,7 @@ extension AssetHubSwapOperationFactory: AssetConversionOperationFactoryProtocol ) } - func quote(for args: AssetConversion.Args) -> CompoundOperationWrapper { + func quote(for args: AssetConversion.QuoteArgs) -> CompoundOperationWrapper { let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() let request = AssetHubSwapRequestBuilder(chain: chain).build(args: args) { diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapQuoteBuilder.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapQuoteBuilder.swift index 7a7a96937a..b209408c64 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapQuoteBuilder.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapQuoteBuilder.swift @@ -2,17 +2,6 @@ import Foundation import SubstrateSdk import BigInt -enum AssetHubSwapRequestBuilderError: Error { - case brokenAssetIn(ChainAssetId) - case brokenAssetOut(ChainAssetId) -} - -enum AssetHubSwapRequestSerializerError: Error { - case undefinedAssetType - case undefinedBalanceType - case quoteCalcFailed -} - enum AssetHubSwapRequestSerializer { private static func extractAssetType(from codingFactory: RuntimeCoderFactoryProtocol) -> String? { guard @@ -44,7 +33,7 @@ enum AssetHubSwapRequestSerializer { codingFactory: RuntimeCoderFactoryProtocol ) throws { guard let assetType = extractAssetType(from: codingFactory) else { - throw AssetHubSwapRequestSerializerError.undefinedAssetType + throw AssetConversionOperationError.runtimeError("undefined asset type") } try encoder.append(asset, ofType: assetType, with: codingFactory.createRuntimeJsonContext().toRawContext()) @@ -56,7 +45,7 @@ enum AssetHubSwapRequestSerializer { codingFactory: RuntimeCoderFactoryProtocol ) throws { guard let balanceType = extractBalanceType(from: codingFactory) else { - throw AssetHubSwapRequestSerializerError.undefinedBalanceType + throw AssetConversionOperationError.runtimeError("undefined balance type") } try encoder.append( @@ -68,7 +57,7 @@ enum AssetHubSwapRequestSerializer { static func deserialize(quoteResponse: String, codingFactory: RuntimeCoderFactoryProtocol) throws -> BigUInt { guard let balanceType = extractBalanceType(from: codingFactory) else { - throw AssetHubSwapRequestSerializerError.undefinedBalanceType + throw AssetConversionOperationError.runtimeError("undefined balance type") } let data = try Data(hexString: quoteResponse) @@ -78,7 +67,7 @@ enum AssetHubSwapRequestSerializer { let json: JSON = try decoder.readOption(type: balanceType) guard json != .null else { - throw AssetHubSwapRequestSerializerError.quoteCalcFailed + throw AssetConversionOperationError.quoteCalcFailed } return try json.map( @@ -100,7 +89,7 @@ final class AssetHubSwapRequestBuilder { private func createRequest( for chain: ChainModel, - args: AssetConversion.Args, + args: AssetConversion.QuoteArgs, builtInFunction: String, codingClosure: @escaping () throws -> RuntimeCoderFactoryProtocol, includesFee: Bool @@ -109,21 +98,21 @@ final class AssetHubSwapRequestBuilder { let codingFactory = try codingClosure() guard - let remoteAssetIn = AssetHubTokensConverter.converToMultilocation( + let remoteAssetIn = AssetHubTokensConverter.convertToMultilocation( chainAssetId: args.assetIn, chain: chain, codingFactory: codingFactory ) else { - throw AssetHubSwapRequestBuilderError.brokenAssetIn(args.assetIn) + throw AssetConversionOperationError.remoteAssetNotFound(args.assetIn) } guard - let remoteAssetOut = AssetHubTokensConverter.converToMultilocation( + let remoteAssetOut = AssetHubTokensConverter.convertToMultilocation( chainAssetId: args.assetOut, chain: chain, codingFactory: codingFactory ) else { - throw AssetHubSwapRequestBuilderError.brokenAssetIn(args.assetOut) + throw AssetConversionOperationError.remoteAssetNotFound(args.assetOut) } var encoder = codingFactory.createEncoder() @@ -155,7 +144,7 @@ final class AssetHubSwapRequestBuilder { } func build( - args: AssetConversion.Args, + args: AssetConversion.QuoteArgs, codingClosure: @escaping () throws -> RuntimeCoderFactoryProtocol ) -> StateCallRpc.Request { let builtInFunction: String diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift index c893147a68..d386f85770 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift @@ -8,7 +8,7 @@ struct AssetHubToken { } enum AssetHubTokensConverter { - static func converToMultilocation( + static func convertToMultilocation( chainAssetId: ChainAssetId, chain: ChainModel, codingFactory: RuntimeCoderFactoryProtocol @@ -31,7 +31,7 @@ enum AssetHubTokensConverter { } switch storageInfo { - case let .native(info): + case .native: return .init(parents: 0, interior: .init(items: [])) case let .statemine(extras): let palletName = extras.palletName ?? PalletAssets.name diff --git a/novawalletIntegrationTests/AssetHubSwapTests.swift b/novawalletIntegrationTests/AssetHubSwapTests.swift index a070e204d2..bcd2eec859 100644 --- a/novawalletIntegrationTests/AssetHubSwapTests.swift +++ b/novawalletIntegrationTests/AssetHubSwapTests.swift @@ -143,12 +143,11 @@ final class AssetHubSwapTests: XCTestCase { operationQueue: operationQueue ) - let args = AssetConversion.Args( + let args = AssetConversion.QuoteArgs( assetIn: .init(chainId: chainId, assetId: assetIn), assetOut: .init(chainId: chainId, assetId: assetOut), amount: amount, - direction: direction, - slippage: 0 + direction: direction ) let quoteWrapper = operationFactory.quote(for: args) From ace7bac7fe4bca5481c289779dbb1a4852dec783 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 2 Oct 2023 19:05:41 +0500 Subject: [PATCH 008/204] add fee test --- novawallet.xcodeproj/project.pbxproj | 2 + novawallet/Common/Model/BigRational.swift | 6 ++ .../AssetHubSwapTests.swift | 84 +++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 11d02565d4..add234c2d3 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -252,6 +252,7 @@ 0CD352932ACAD7A500B3E446 /* AssetHubExtrinsicService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD352922ACAD7A500B3E446 /* AssetHubExtrinsicService.swift */; }; 0CD352952ACAF59900B3E446 /* BigRational.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD352942ACAF59900B3E446 /* BigRational.swift */; }; 0CD352972ACAFADA00B3E446 /* AssetConversionOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD352962ACAFADA00B3E446 /* AssetConversionOperationFactory.swift */; }; + 0CD352982ACB01FD00B3E446 /* AccountGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845B822426EFE03E00D25C72 /* AccountGenerator.swift */; }; 0CE150502A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE1504F2A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift */; }; 0CE150542A70EA2200B61CC1 /* NominationPoolsSyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE150532A70EA2200B61CC1 /* NominationPoolsSyncTests.swift */; }; 0CE550B32A49658700F0A7AC /* StakingDuration+Localizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE550B22A49658700F0A7AC /* StakingDuration+Localizable.swift */; }; @@ -19124,6 +19125,7 @@ 849E16E42714D1BD0065B305 /* EthereumBaseIntegrationTests.swift in Sources */, 84BB3CE9267C9ADE00676FFE /* CrowdloanTests.swift in Sources */, 8479F31426CD9A0E005D8D24 /* ChainRegistryIntegrationTests.swift in Sources */, + 0CD352982ACB01FD00B3E446 /* AccountGenerator.swift in Sources */, 844304672A28F7A500DE36DE /* MultistakingSyncTests.swift in Sources */, 84E8AC7727BBC8E400402635 /* NFTIntegrationTests.swift in Sources */, 0CCA24652AC6B51200AEF23D /* AssetHubSwapTests.swift in Sources */, diff --git a/novawallet/Common/Model/BigRational.swift b/novawallet/Common/Model/BigRational.swift index 3db92ab7aa..c561509cb4 100644 --- a/novawallet/Common/Model/BigRational.swift +++ b/novawallet/Common/Model/BigRational.swift @@ -9,3 +9,9 @@ struct BigRational { value * numerator / denominator } } + +extension BigRational { + static func percent(of numerator: BigUInt) -> BigRational { + .init(numerator: numerator, denominator: 100) + } +} diff --git a/novawalletIntegrationTests/AssetHubSwapTests.swift b/novawalletIntegrationTests/AssetHubSwapTests.swift index bcd2eec859..d64e6dc1b0 100644 --- a/novawalletIntegrationTests/AssetHubSwapTests.swift +++ b/novawalletIntegrationTests/AssetHubSwapTests.swift @@ -1,6 +1,7 @@ import XCTest @testable import novawallet import BigInt +import RobinHood final class AssetHubSwapTests: XCTestCase { func testWestmintAllDirections() throws { @@ -76,6 +77,32 @@ final class AssetHubSwapTests: XCTestCase { Logger.shared.info("Quote: \(quote)") } + func testFeeForWestmintSiriSell() throws { + let amountIn: BigUInt = 1_000_000_000_000 + + let quote = try fetchQuote( + for: KnowChainId.westmint, + assetIn: 1, + assetOut: 0, + direction: .sell, + amount: amountIn + ) + + let callArgs = AssetConversion.CallArgs( + assetIn: quote.assetIn, + amountIn: quote.amountIn, + assetOut: quote.assetOut, + amountOut: quote.amountOut, + receiver: AccountId.zeroAccountId(of: 32), + direction: .sell, + slippage: .percent(of: 1) + ) + + let fee = try fetchNetworkFee(for: callArgs) + + Logger.shared.info("Fee: \(String(fee))") + } + private func performAvailableDirectionsFetch( for chainId: ChainModel.Id, assetId: AssetModel.Id? @@ -156,4 +183,61 @@ final class AssetHubSwapTests: XCTestCase { return try quoteWrapper.targetOperation.extractNoCancellableResultData() } + + private func fetchNetworkFee(for args: AssetConversion.CallArgs) throws -> BigUInt { + let storageFacade = SubstrateStorageTestFacade() + let chainRegistry = ChainRegistryFacade.setupForIntegrationTest(with: storageFacade) + + let chainId = args.assetIn.chainId + + let wallet = AccountGenerator.generateMetaAccount(generatingChainAccounts: 1) + + guard + let chain = chainRegistry.getChain(for: chainId), + let connection = chainRegistry.getConnection(for: chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chainId), + let accountResponse = wallet.fetch(for: chain.accountRequest()) else { + throw ChainRegistryError.noChain(chainId) + } + + let operationQueue = OperationQueue() + + let extrinsicService = ExtrinsicServiceFactory( + runtimeRegistry: runtimeService, + engine: connection, + operationManager: OperationManager(operationQueue: operationQueue) + ).createService(account: accountResponse, chain: chain) + + let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() + + operationQueue.addOperations([codingFactoryOperation], waitUntilFinished: true) + + let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + + let builderClosure = AssetHubExtrinsicService(chain: chain).fetchExtrinsicBuilderClosure( + for: args, + codingFactory: codingFactory + ) + + var feeResult: FeeExtrinsicResult? + + let expectation = XCTestExpectation() + + extrinsicService.estimateFee(builderClosure, runningIn: .main) { result in + feeResult = result + + expectation.fulfill() + } + + wait(for: [expectation], timeout: 600) + + switch feeResult { + case let .success(dispatchInfo): + return BigUInt(dispatchInfo.fee) ?? 0 + case let .failure(error): + throw error + case .none: + throw CommonError.undefined + } + } } From d699ade2ac17d15083c2db3eb1b8d081a9d3d88e Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Mon, 2 Oct 2023 22:17:42 +0300 Subject: [PATCH 009/204] add control --- Podfile.lock | 2 +- novawallet.xcodeproj/project.pbxproj | 52 +++ .../iconAddSwapAmount.imageset/Contents.json | 12 + .../container-token.pdf | Bin 0 -> 8656 bytes .../AmountInputView/SwapAmountInputView.swift | 393 ++++++++++++++++++ .../AssetList/AssetListPresenter.swift | 4 + .../AssetList/AssetListProtocols.swift | 3 + .../AssetList/AssetListViewController.swift | 9 + .../AssetList/AssetListWireframe.swift | 12 + .../View/AssetListTotalBalanceCell.swift | 10 + .../SwapSetup/SwapSetupInteractor.swift | 7 + .../SwapSetup/SwapSetupPresenter.swift | 76 ++++ .../SwapSetup/SwapSetupProtocols.swift | 21 + .../SwapSetup/SwapSetupViewController.swift | 103 +++++ .../SwapSetup/SwapSetupViewFactory.swift | 31 ++ .../SwapSetup/SwapSetupViewLayout.swift | 57 +++ .../SwapSetup/SwapSetupWireframe.swift | 3 + novawallet/en.lproj/Localizable.strings | 1 + novawallet/ru.lproj/Localizable.strings | 1 + .../Modules/SwapSetup/SwapSetupTests.swift | 16 + 20 files changed, 812 insertions(+), 1 deletion(-) create mode 100644 novawallet/Assets.xcassets/iconAddSwapAmount.imageset/Contents.json create mode 100644 novawallet/Assets.xcassets/iconAddSwapAmount.imageset/container-token.pdf create mode 100644 novawallet/Common/View/AmountInputView/SwapAmountInputView.swift create mode 100644 novawallet/Modules/SwapSetup/SwapSetupInteractor.swift create mode 100644 novawallet/Modules/SwapSetup/SwapSetupPresenter.swift create mode 100644 novawallet/Modules/SwapSetup/SwapSetupProtocols.swift create mode 100644 novawallet/Modules/SwapSetup/SwapSetupViewController.swift create mode 100644 novawallet/Modules/SwapSetup/SwapSetupViewFactory.swift create mode 100644 novawallet/Modules/SwapSetup/SwapSetupViewLayout.swift create mode 100644 novawallet/Modules/SwapSetup/SwapSetupWireframe.swift create mode 100644 novawalletTests/Modules/SwapSetup/SwapSetupTests.swift diff --git a/Podfile.lock b/Podfile.lock index c4ad16509c..7d69090481 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -305,4 +305,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 9da63967acbaf0ea90ab5cf6e0cef78c59d3668f -COCOAPODS: 1.12.1 +COCOAPODS: 1.13.0 diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 2ac49fc6f5..1e34a70884 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -415,6 +415,7 @@ 3441DDC002503A0DC9A8A925 /* ReferendumSearchViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B179EA3EF793684717BA9D68 /* ReferendumSearchViewFactory.swift */; }; 347BBBBCC84CA155006FDCDB /* GovernanceSelectTracksViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3593D7650F5126266ED9FE84 /* GovernanceSelectTracksViewLayout.swift */; }; 34D6FF85BEA25EFD1D15D460 /* InAppUpdatesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15E09DD01C1CC61EA5CDED9C /* InAppUpdatesInteractor.swift */; }; + 350B8A18C9C91DF07D2E53C5 /* SwapSetupViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD43373A9159E0A592077BB /* SwapSetupViewLayout.swift */; }; 355476A5AECD2FFE4ED3DE39 /* MessageSheetViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A719A9FC28373296AB195CB /* MessageSheetViewLayout.swift */; }; 3592E885646B3ED9F2717412 /* GovernanceRevokeDelegationTracksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CAF58F7D0659E89B66B75E4 /* GovernanceRevokeDelegationTracksViewController.swift */; }; 35F9157CAA182493B2F0E1D3 /* ParaStkRedeemInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53B5C1C920BFDA8F5C9C89D9 /* ParaStkRedeemInteractor.swift */; }; @@ -452,6 +453,7 @@ 3E480EEAF501AEB5D543506D /* UsernameSetupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172B3E9BE51A339D7A09BDA3 /* UsernameSetupPresenter.swift */; }; 3E6215E91AE1C1F78246A43C /* ParaStkUnstakeViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611AD2A7BEEEBA634F56163D /* ParaStkUnstakeViewLayout.swift */; }; 3EAB85420EDDDE7D5B03A1CF /* GovernanceUnavailableTracksWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374AF5133A7FD39B961B9C84 /* GovernanceUnavailableTracksWireframe.swift */; }; + 3EE29545824B68594751769C /* SwapSetupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB494F0B16C9588325CF0D84 /* SwapSetupPresenter.swift */; }; 3F3AE7490C59A0CE0BF2D7A7 /* DAppWalletAuthViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A5B4E6779AA27F10713C6 /* DAppWalletAuthViewFactory.swift */; }; 3F7F10D0E1BDE09CBE64BD2D /* CrowdloanYourContributionsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC155A29FA8777D90A46913D /* CrowdloanYourContributionsViewFactory.swift */; }; 3FC436AED4098456EDEAF484 /* MessageSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A2F34329579E12A2836E77 /* MessageSheetViewController.swift */; }; @@ -527,6 +529,7 @@ 542588DA751A44C993BC1F27 /* ParaStkYourCollatorsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8C13B77688FFF0FFBBB6612 /* ParaStkYourCollatorsWireframe.swift */; }; 5443122935BBFDD55AE9E6FD /* ParitySignerAddressesProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4F3C080F3D5C1E64475903 /* ParitySignerAddressesProtocols.swift */; }; 544C8EB3D71227FAF2FD4658 /* GovernanceRemoveVotesConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29FE1BF422468BECDCDEE63 /* GovernanceRemoveVotesConfirmPresenter.swift */; }; + 54813408A51B4AEBA3EED0A5 /* SwapSetupInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C585109AC3A2580AB1253C31 /* SwapSetupInteractor.swift */; }; 54983C354F7EDCD8014C8371 /* WalletConnectSessionDetailsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE103341935B2A4B8C32B966 /* WalletConnectSessionDetailsViewFactory.swift */; }; 54D334605E9A7C71A4873CFC /* ParaStkRedeemWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 578743E9101B334BFBE44CB6 /* ParaStkRedeemWireframe.swift */; }; 5510625BDA756B939ED7C586 /* AddDelegationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D44AF5C59681B54ECD7658 /* AddDelegationPresenter.swift */; }; @@ -550,6 +553,7 @@ 59A0AF440ABAAA459EF7D993 /* GovernanceYourDelegationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4462DCD832DB73AA78D44C /* GovernanceYourDelegationsViewController.swift */; }; 59D03D8CB4AE60DE53130729 /* NPoolsRedeemViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EF6FA61F2B7E3B2ADD3200 /* NPoolsRedeemViewLayout.swift */; }; 5AC2A8AD94278DFA4B68A718 /* NominationPoolSearchInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44AA632DE49B746BC38B959F /* NominationPoolSearchInteractor.swift */; }; + 5B02D050E6E067906A05B46B /* SwapSetupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7F910E56C2CC7AA5224BD21 /* SwapSetupWireframe.swift */; }; 5B54978244C37502DD592486 /* NftListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A092ADC09DA0429548EBC08 /* NftListPresenter.swift */; }; 5B652F1E0040F68F835A2F1D /* AssetDetailsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEE05ECCFE3DD11A2EAAF495 /* AssetDetailsViewLayout.swift */; }; 5C796EF8ED29F564B5D1126B /* CrowdloanContributionConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F75722D2F921FD1C2D4105D /* CrowdloanContributionConfirmViewController.swift */; }; @@ -590,6 +594,7 @@ 65C06FCE82EEC0B476DB1CEF /* DAppBrowserProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E38ABE379CA48E63328C4 /* DAppBrowserProtocols.swift */; }; 65CD159259A06EC3E92FD4B0 /* AssetDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 998CDEAB9F149770B27F5317 /* AssetDetailsProtocols.swift */; }; 663DB041307C59E939BF0BE2 /* ParitySignerAddConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44809BCF44D7329266A60A9D /* ParitySignerAddConfirmInteractor.swift */; }; + 66531C7E2E0E99C89A89A35A /* SwapSetupViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A6BCE8F8DB7576E1E5B5974 /* SwapSetupViewFactory.swift */; }; 671C5788468FE8445A46C09F /* AdvancedWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 597A3C3F2937333D0EC7ABD5 /* AdvancedWalletViewController.swift */; }; 67684F7576ED0252C1050CA5 /* OperationDetailsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D569738955713647612599 /* OperationDetailsViewLayout.swift */; }; 676B1511C4A34528C668751D /* GovernanceRevokeDelegationConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B263D5668F1C91E2CF61D9 /* GovernanceRevokeDelegationConfirmWireframe.swift */; }; @@ -664,6 +669,7 @@ 772B1C7D2A93837800A19061 /* StartStakingCustomValidatorListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 772B1C7C2A93837800A19061 /* StartStakingCustomValidatorListWireframe.swift */; }; 7731E9C42A14DA3F0085B5FF /* BorderedActionControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7731E9C32A14DA3F0085B5FF /* BorderedActionControlView.swift */; }; 7738FB6A2A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7738FB692A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift */; }; + 774091F92ACB1F4B00172516 /* SwapAmountInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774091F82ACB1F4B00172516 /* SwapAmountInputView.swift */; }; 774A481129F8BFB70094635B /* OperationAuthPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774A481029F8BFB70094635B /* OperationAuthPresentable.swift */; }; 7756927D2A20B88200220756 /* TokenOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7756927C2A20B88200220756 /* TokenOperation.swift */; }; 775F194D2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775F194C2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift */; }; @@ -3022,6 +3028,7 @@ 85A093F6055DDD2E2E9253F2 /* ControllerAccountProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F829E7F8B39EE7D977001510 /* ControllerAccountProtocols.swift */; }; 86EB789787B731691B36C827 /* OnChainTransferSetupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1A2F7E5E278FDCC89FE097 /* OnChainTransferSetupPresenter.swift */; }; 873FAB6E5CAD1FD4D02737D0 /* NominationPoolBondMoreSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C9473AB7D1FE4A27403078 /* NominationPoolBondMoreSetupViewController.swift */; }; + 8786222ADF4643BE7A6FBBEB /* SwapSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBEB2BC52266AF493D310834 /* SwapSetupViewController.swift */; }; 879D493C025963619CFADF4F /* GovernanceUnlockSetupProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4642DD5186EFA940518CCB4 /* GovernanceUnlockSetupProtocols.swift */; }; 87F7556E02F6F5BB6F1B1AEA /* ParitySignerTxQrViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A5DCA28ABF42D342BBDF9A /* ParitySignerTxQrViewLayout.swift */; }; 880059D828EEBC0200E87B9B /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880059D728EEBC0200E87B9B /* SliderView.swift */; }; @@ -3333,6 +3340,7 @@ 91A1286763617DE022BD495F /* LedgerInstructionsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B08ACC71BE679A48A7B66E /* LedgerInstructionsPresenter.swift */; }; 921E4891E85C0DC6FDD8A0D0 /* CrowdloanContributionConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1366336078BCA34EFB4C6FF9 /* CrowdloanContributionConfirmInteractor.swift */; }; 924BADB89E7FA2DC54BF1A02 /* NPoolsClaimRewardsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5F1F933F624B01855AA3BA5 /* NPoolsClaimRewardsInteractor.swift */; }; + 92984DFE797C52644C084377 /* SwapSetupProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53775773F2060B4B7F6D62DA /* SwapSetupProtocols.swift */; }; 93434E8E407A6C63D8862A21 /* AssetSelectionProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DECC58C93DB18E79A03B5A0 /* AssetSelectionProtocols.swift */; }; 934F229F4E5A588D5AF2A093 /* TokensAddSelectNetworkViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E800814C025B38C87CC282D /* TokensAddSelectNetworkViewFactory.swift */; }; 9358E048B1AA0F71F519101E /* GovernanceDelegateConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27FB20961F2F221A96624A6 /* GovernanceDelegateConfirmInteractor.swift */; }; @@ -3660,6 +3668,7 @@ D264B2A8A516396051016CAB /* AssetReceivePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3934C46625930FA8D171D3E7 /* AssetReceivePresenter.swift */; }; D274117F06B12F955073D35B /* DelegationReferendumVotersViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7059B3F1E8DC94D36733B4C7 /* DelegationReferendumVotersViewLayout.swift */; }; D344C6DAC1F8BB6152BA8DD0 /* RecommendedValidatorListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C6573C52692E4A56E35FF9 /* RecommendedValidatorListProtocols.swift */; }; + D37305C9017F215D98002AC8 /* SwapSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0F09EB970EC5D7C942BFDB /* SwapSetupTests.swift */; }; D3B48F82A875E301D749AC0B /* StakingUnbondConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674162035C7D9F226FA9964 /* StakingUnbondConfirmViewController.swift */; }; D3B74ED2525DE12423722DE2 /* AssetReceiveInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B243F7A096241F329224A18E /* AssetReceiveInteractor.swift */; }; D3F199376DAEBF380C5FFD9D /* DAppAddFavoriteViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A759A20A4A39B3B0E2A735 /* DAppAddFavoriteViewLayout.swift */; }; @@ -4394,6 +4403,7 @@ 397F057FD5B16A58E5F30F07 /* NPoolsUnstakeConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeConfirmWireframe.swift; sourceTree = ""; }; 39907750D40A8DD7FE1288C8 /* CreateWatchOnlyViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateWatchOnlyViewController.swift; sourceTree = ""; }; 399700B22225DD916DFACAF9 /* DelegateVotedReferendaViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegateVotedReferendaViewFactory.swift; sourceTree = ""; }; + 3A0F09EB970EC5D7C942BFDB /* SwapSetupTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupTests.swift; sourceTree = ""; }; 3A46EE888D60C1538A0A3EFC /* NftDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsProtocols.swift; sourceTree = ""; }; 3A7235097E09C94005B091B4 /* CommonDelegationTracksPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommonDelegationTracksPresenter.swift; sourceTree = ""; }; 3A76BDAB14EEA1C4E23B884E /* ParitySignerTxQrViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxQrViewController.swift; sourceTree = ""; }; @@ -4484,6 +4494,7 @@ 53235E51143C6E93303E30FE /* DAppSearchViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppSearchViewController.swift; sourceTree = ""; }; 534173384708B3CF8F47E4DA /* GovernanceDelegateConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateConfirmPresenter.swift; sourceTree = ""; }; 536D1CA47A5753B9E0389BEA /* ReferendumSearchProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumSearchProtocols.swift; sourceTree = ""; }; + 53775773F2060B4B7F6D62DA /* SwapSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupProtocols.swift; sourceTree = ""; }; 537CCA1F2667A51731C56C88 /* CreateWatchOnlyProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateWatchOnlyProtocols.swift; sourceTree = ""; }; 53A058D4A585F253CBF2968D /* ReferendumVoteSetupViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVoteSetupViewController.swift; sourceTree = ""; }; 53B5C1C920BFDA8F5C9C89D9 /* ParaStkRedeemInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkRedeemInteractor.swift; sourceTree = ""; }; @@ -4567,6 +4578,7 @@ 6A3105383F2825940D0105D5 /* ReferendumVoteSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVoteSetupViewLayout.swift; sourceTree = ""; }; 6A4009AE7EF95E9CE1EB88B8 /* NominationPoolBondMoreBaseWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreBaseWireframe.swift; sourceTree = ""; }; 6A695CA303926DFB5D54E309 /* LedgerAccountConfirmationViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerAccountConfirmationViewLayout.swift; sourceTree = ""; }; + 6A6BCE8F8DB7576E1E5B5974 /* SwapSetupViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupViewFactory.swift; sourceTree = ""; }; 6A7302440137F083F7AEC64E /* NPoolsClaimRewardsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsClaimRewardsViewLayout.swift; sourceTree = ""; }; 6A825B6368073B06F32D7C8F /* StakingMainViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingMainViewFactory.swift; sourceTree = ""; }; 6AD8B98AB03AAF06AA891695 /* TransferConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferConfirmViewLayout.swift; sourceTree = ""; }; @@ -4624,6 +4636,7 @@ 772B1C7C2A93837800A19061 /* StartStakingCustomValidatorListWireframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartStakingCustomValidatorListWireframe.swift; sourceTree = ""; }; 7731E9C32A14DA3F0085B5FF /* BorderedActionControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedActionControlView.swift; sourceTree = ""; }; 7738FB692A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingRelaychainInteractor.swift; sourceTree = ""; }; + 774091F82ACB1F4B00172516 /* SwapAmountInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAmountInputView.swift; sourceTree = ""; }; 774A481029F8BFB70094635B /* OperationAuthPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationAuthPresentable.swift; sourceTree = ""; }; 7756927C2A20B88200220756 /* TokenOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenOperation.swift; sourceTree = ""; }; 775F194C2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoRelaychainPresenter.swift; sourceTree = ""; }; @@ -7594,6 +7607,8 @@ BAB2478DE3AF0885A3ED7ED8 /* StakingRedeemPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRedeemPresenter.swift; sourceTree = ""; }; BAF9ED27CF12B7DA8B1378CF /* MarkdownDescriptionViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MarkdownDescriptionViewFactory.swift; sourceTree = ""; }; BB1A1934B76A5DCC65855EE1 /* TransactionHistoryWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransactionHistoryWireframe.swift; sourceTree = ""; }; + BB494F0B16C9588325CF0D84 /* SwapSetupPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupPresenter.swift; sourceTree = ""; }; + BBEB2BC52266AF493D310834 /* SwapSetupViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupViewController.swift; sourceTree = ""; }; BC15D0B7B9F29E97FCECC1D2 /* AssetsSettingsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetsSettingsViewLayout.swift; sourceTree = ""; }; BC216C4DBF86A9F3ADB3AECF /* ParitySignerAddConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerAddConfirmWireframe.swift; sourceTree = ""; }; BC767DE76479F70A8FA3292A /* StakingMoreOptionsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionsViewLayout.swift; sourceTree = ""; }; @@ -7623,8 +7638,10 @@ C4E807E9E12A130C50E8FFDF /* StakingDashboardViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingDashboardViewFactory.swift; sourceTree = ""; }; C503100478AB56E903598A78 /* ReferralCrowdloanPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferralCrowdloanPresenter.swift; sourceTree = ""; }; C52D6675524DB913210F0459 /* DAppSettingsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppSettingsPresenter.swift; sourceTree = ""; }; + C585109AC3A2580AB1253C31 /* SwapSetupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupInteractor.swift; sourceTree = ""; }; C5E9D289393AA2CC1E34C2F4 /* AssetDetailsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetDetailsWireframe.swift; sourceTree = ""; }; C74A2166B054240BD5D925B6 /* UsernameSetupViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UsernameSetupViewFactory.swift; sourceTree = ""; }; + C7F910E56C2CC7AA5224BD21 /* SwapSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupWireframe.swift; sourceTree = ""; }; C80D934D47929D2331111AD7 /* ReferendumFullDetailsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumFullDetailsWireframe.swift; sourceTree = ""; }; C92B3D5B314FB3EAE65FA471 /* StartStakingInfoProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingInfoProtocols.swift; sourceTree = ""; }; C96C3B5ABF4A8124848EFD17 /* ControllerAccountConfirmationWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountConfirmationWireframe.swift; sourceTree = ""; }; @@ -7645,6 +7662,7 @@ CD6B5B187E83839481846C7E /* NftDetailsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsInteractor.swift; sourceTree = ""; }; CD7A6C62EC06FC3B2693FB43 /* GovernanceDelegateSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateSetupViewLayout.swift; sourceTree = ""; }; CDB47990BC7A594E663DAC00 /* ReferendumVoteConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVoteConfirmPresenter.swift; sourceTree = ""; }; + CDD43373A9159E0A592077BB /* SwapSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupViewLayout.swift; sourceTree = ""; }; CE98454DC77EAA01301B9BBF /* ParaStkCollatorFiltersProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkCollatorFiltersProtocols.swift; sourceTree = ""; }; CF389223A781CA2088C7A4DD /* NPoolsClaimRewardsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsClaimRewardsWireframe.swift; sourceTree = ""; }; CF7A019F89C6CD418AEEE79C /* YourWalletsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourWalletsPresenter.swift; sourceTree = ""; }; @@ -8727,6 +8745,14 @@ path = ParaStkCollatorInfo; sourceTree = ""; }; + 1ED929902157CCBAD0BD894E /* SwapSetup */ = { + isa = PBXGroup; + children = ( + 3A0F09EB970EC5D7C942BFDB /* SwapSetupTests.swift */, + ); + path = SwapSetup; + sourceTree = ""; + }; 249BACDEE5CB2B1ECDF470D9 /* AccountExportPassword */ = { isa = PBXGroup; children = ( @@ -8778,6 +8804,20 @@ path = AccountManagement; sourceTree = ""; }; + 29BD7DA0076BA8BC3411221A /* SwapSetup */ = { + isa = PBXGroup; + children = ( + 53775773F2060B4B7F6D62DA /* SwapSetupProtocols.swift */, + C7F910E56C2CC7AA5224BD21 /* SwapSetupWireframe.swift */, + BB494F0B16C9588325CF0D84 /* SwapSetupPresenter.swift */, + C585109AC3A2580AB1253C31 /* SwapSetupInteractor.swift */, + BBEB2BC52266AF493D310834 /* SwapSetupViewController.swift */, + CDD43373A9159E0A592077BB /* SwapSetupViewLayout.swift */, + 6A6BCE8F8DB7576E1E5B5974 /* SwapSetupViewFactory.swift */, + ); + path = SwapSetup; + sourceTree = ""; + }; 2A1FB6759F2E8A05A1894287 /* GovernanceUnavailableTracks */ = { isa = PBXGroup; children = ( @@ -9804,6 +9844,7 @@ 8401620F25E144DA0087A5F3 /* AmountInputView */ = { isa = PBXGroup; children = ( + 774091F82ACB1F4B00172516 /* SwapAmountInputView.swift */, 840DCBF025DFEE4800D45C6A /* AmountInputView.swift */, 840DCBF525E0059D00D45C6A /* AmountInputView+Inspectable.swift */, 8401620A25E144D50087A5F3 /* AmountInputAccessoryView.swift */, @@ -12753,6 +12794,7 @@ 9D97DD4BC9672502D2E2A625 /* TokensManage */, EC1A579A3747EB16688DAEBF /* AssetReceive */, C9850B4B70AEFEABB96269FF /* TransactionHistory */, + 29BD7DA0076BA8BC3411221A /* SwapSetup */, ); path = Modules; sourceTree = ""; @@ -14238,6 +14280,7 @@ 84B7C705289BFA79001A3566 /* AccountManagement */, 84B7C708289BFA79001A3566 /* WalletList */, 84B7C70A289BFA79001A3566 /* ControllerAccount */, + 1ED929902157CCBAD0BD894E /* SwapSetup */, ); path = Modules; sourceTree = ""; @@ -19176,6 +19219,7 @@ 84FBED052927B1CA00FBEB83 /* EvmEventParser.swift in Sources */, 84A1742428ED3CF70096F943 /* ReferendumLocal.swift in Sources */, 841AB7922993D01A00A362E8 /* GovernanceDelegateConfirmInteractorError.swift in Sources */, + 774091F92ACB1F4B00172516 /* SwapAmountInputView.swift in Sources */, 7728E5912A1324A2007901E0 /* ReferendumsSearchManager.swift in Sources */, 843C49DB24DF373000B71DDA /* AccountImportRequest.swift in Sources */, 843910C1253F36F300E3C217 /* BaseStorageChildSubscription.swift in Sources */, @@ -22725,6 +22769,13 @@ EB877554208E91A80985F1E5 /* NPoolsRedeemViewController.swift in Sources */, 59D03D8CB4AE60DE53130729 /* NPoolsRedeemViewLayout.swift in Sources */, F3719F7C2AD0B75FC271DCE9 /* NPoolsRedeemViewFactory.swift in Sources */, + 92984DFE797C52644C084377 /* SwapSetupProtocols.swift in Sources */, + 5B02D050E6E067906A05B46B /* SwapSetupWireframe.swift in Sources */, + 3EE29545824B68594751769C /* SwapSetupPresenter.swift in Sources */, + 54813408A51B4AEBA3EED0A5 /* SwapSetupInteractor.swift in Sources */, + 8786222ADF4643BE7A6FBBEB /* SwapSetupViewController.swift in Sources */, + 350B8A18C9C91DF07D2E53C5 /* SwapSetupViewLayout.swift in Sources */, + 66531C7E2E0E99C89A89A35A /* SwapSetupViewFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -22885,6 +22936,7 @@ 84B7C748289BFA79001A3566 /* WalletListTests.swift in Sources */, 84B7C720289BFA79001A3566 /* ReferralCrowdloanTests.swift in Sources */, F4897BB126AED13D0075F291 /* EraCountdownOperationFactoryStub.swift in Sources */, + D37305C9017F215D98002AC8 /* SwapSetupTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/novawallet/Assets.xcassets/iconAddSwapAmount.imageset/Contents.json b/novawallet/Assets.xcassets/iconAddSwapAmount.imageset/Contents.json new file mode 100644 index 0000000000..9d63318b20 --- /dev/null +++ b/novawallet/Assets.xcassets/iconAddSwapAmount.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "container-token.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconAddSwapAmount.imageset/container-token.pdf b/novawallet/Assets.xcassets/iconAddSwapAmount.imageset/container-token.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8dd529ed0bf8069a53efbea2929081df47dff651 GIT binary patch literal 8656 zcmeHMTW{Pp7Jk>S;7fqCKt`gj6i@_c?8I1XgSz$t8=&ZeA}jWEXGZRfrbxEvf8Xzr z)HxiFoKY@sn+Xt8pU89P;o;Gf%kx*y^rDI)trFG0{XJ6ZhaXh@Q@6Pt?ot?x`Br{HVwcoVEvfupc!+oc6tb6`E+6~*TSx0{< zu-B(IL?q+{av-7ObwYnYDUvAQUe?Kuen7rG+6*brUl`w5? zZrtKi^SxLz=d<8E|8KQ?ryP^KDvM&S27dBv4IZCAt-)jSKdOPL$|}uH`e43mt#!ze ze@ETs#s>#R6u=P$t&8L$OX$5tGe(2>&S%B<7?@unb`wK;XCj0S<8vaq>M}8Tsa!OA zGP9%QAdo0Fo+XQ_uHh9=Fn3#eir}R4u;l|yKI#s#v2#b7*6fagQF6zN7ybJ%CvZl| zCq|uD1KI{AK&2Ca9-YKlwTn-`k_^e;`+w1$L?Nof;{kd$ri*574W!Z4z$#Fd9 zTSZ&MXZw?@!U^%|jS)^iIUX%(4}kP@RC@V-z`lq1hwZ*||9!HNaq66u)J}+f=zWs7 zv7J3Gox|z*a~yDfA87>9OTpvma4PsD12;p(eZvjaD z1eMYFwhZ2eobRGBh7@cWUuv~$H_^Lj6HT0q6c{cQXI0LrxMEboMBIpI8>>O{RpW{S zXDX7v<@Uqees%HU;ijFDoFuhVb^k53k0u*KZ+w3A@3^;YZku}&eP(K(@^6fT?BaT8 zhR&w&m#O{JHgq8=Z6!HJmH4Hrhpt~YyZ;1d0XOcMO&*+&^n{fD2mX-*Z@=WE z9OF1VBjk*c^W^{U^rVyWmv5DXYNNa8-`~R+6m993MO(74xhX-GDmC3(F{n(E)u}B` zaPcbKbu$b+LF+}DC3$XChGk`46u8hIuVAQ+gbXT2PkKdlRhDX#;nK$_V&1St@zjA~VHR1t3UhnZA@YUm?B99mrXuOksz&|t)YBbaF3(srNV zC8&rxj%@jTv_@8h77?B#ze_N`U|-F;=xDx5$Ou;*ZjEunlc`a?uMz9ID3T1yT;t4rSg^`tx8gjrOS)wcAWc5Hp+Uf=iI^Cdek<>~e(6^&Ps6`fm1#|c#oCmL_ zxVDn1969ob(S>)B=SzOmiMA&70(zaGGwUG`D@<43Ba6Wb)?g`|$wpcs1nbgdXr&7X zb+b&Tkk7iMMjsyDg;NdW^A;kzAbK+c2YW|XxRQrqPWI8bp^TSqEVbtY1rCGGJT*SvVmyAtghhkPOnM^bJ48cyRnG6pyC~_|Z7Gaq0p+uO!YX zi3Ne~n`rjN{YES5vt98e_Tkn33$N(7l+!MocPXcS?~kkdyXN$@c-$D1cka2j?6GeR zV&msH99E&JeCL|7r?b(lo3Xza$1v`Bk89jSMzR#Wr%K^2AEhWG{kYlm1HRUbUzS`e z`?9(1oPpxk-OaN3vTsy2HoSZA@n(Y$6kuu1Frt^#eIJw_yDOo&2y<$X!2ev@k4xHq zzZ}0%f$E-?dznRv9gvWgw1J5hsR=d$XHe6_A~vj@O?Tf z6mb8e?Lj|B)| u?mP8#dGnp~YJAoAa4C7rMoE0V{BK9rhd*~ivmLBIOq%QH$&;5qzxp3l@&K&> literal 0 HcmV?d00001 diff --git a/novawallet/Common/View/AmountInputView/SwapAmountInputView.swift b/novawallet/Common/View/AmountInputView/SwapAmountInputView.swift new file mode 100644 index 0000000000..4a7f01a729 --- /dev/null +++ b/novawallet/Common/View/AmountInputView/SwapAmountInputView.swift @@ -0,0 +1,393 @@ +import UIKit +import SoraUI + +final class SwapSymbolView: GenericPairValueView, IconDetailsView> { + var symbolLabel: UILabel { fView.fView.detailsLabel } + var disclosureImageView: UIImageView { fView.fView.imageView } + var hubNameView: UILabel { sView.detailsLabel } + var hubImageView: UIImageView { sView.imageView } + + private var imageViewModel: ImageViewModelProtocol? + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure() { + fView.makeHorizontal() + fView.fView.spacing = 0 + fView.fView.iconWidth = 20 + fView.fView.mode = .detailsIcon + + sView.spacing = 8 + sView.iconWidth = 16 + sView.mode = .iconDetails + + spacing = 4 + makeVertical() + + symbolLabel.apply(style: .semiboldBodyPrimary) + hubNameView.apply(style: .footnoteSecondary) + } + + override var intrinsicContentSize: CGSize { + let symbolWidth = symbolLabel.intrinsicContentSize.width + fView.fView.iconWidth + let hubWidth = sView.iconWidth + sView.spacing + hubNameView.intrinsicContentSize.width + let width: CGFloat = max(symbolWidth, hubWidth) + let symbolHeight = max(symbolLabel.intrinsicContentSize.height, fView.fView.iconWidth) + let hubHeight = max(hubNameView.intrinsicContentSize.height, sView.iconWidth) + let height = symbolHeight + spacing + hubHeight + return .init( + width: width, + height: height + ) + } + + func bind(symbol: String, network: String, icon: ImageViewModelProtocol?) { + symbolLabel.text = symbol + imageViewModel?.cancel(on: hubImageView) + imageViewModel = icon + icon?.loadImage( + on: hubImageView, + targetSize: .init( + width: sView.iconWidth, + height: sView.iconWidth + ), + animated: true + ) + sView.hidesIcon = icon == nil + hubNameView.text = network + disclosureImageView.image = R.image.iconSmallArrow()?.tinted(with: R.color.colorIconSecondary()!) + invalidateIntrinsicContentSize() + } +} + +final class SwapAmountInputView: BackgroundedContentControl { + var iconView: AssetIconView { lazyIconViewOrCreateIfNeeded() } + private var lazyIconView: AssetIconView? + + let symbolHubMultiValueView = SwapSymbolView() + var iconViewContentInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) { + didSet { + iconView.contentInsets = iconViewContentInsets + setNeedsLayout() + } + } + + let textField: UITextField = .create { + $0.font = .title2 + $0.textColor = R.color.colorTextPrimary() + $0.tintColor = R.color.colorTextPrimary() + $0.textAlignment = .right + $0.attributedPlaceholder = NSAttributedString( + string: "0", + attributes: [ + .foregroundColor: R.color.colorHintText()!, + .font: UIFont.title2 + ] + ) + $0.keyboardType = .decimalPad + } + + let priceLabel = UILabel( + style: .footnoteSecondary, + textAlignment: .right, + numberOfLines: 1 + ) + + var roundedBackgroundView: RoundedView? { + backgroundView as? RoundedView + } + + var horizontalSpacing: CGFloat = 8 { + didSet { + setNeedsLayout() + } + } + + var iconRadius: CGFloat = 16 { + didSet { + lazyIconView?.backgroundView.cornerRadius = iconRadius + + setNeedsLayout() + } + } + + override var intrinsicContentSize: CGSize { + let rightContentHeight = max( + textField.intrinsicContentSize.height, + priceLabel.intrinsicContentSize.height + ) + + let leftContentHeight = max( + lazyIconView?.intrinsicContentSize.height ?? 0.0, + symbolHubMultiValueView.intrinsicContentSize.height + ) + + let contentHeight = max(leftContentHeight, rightContentHeight) + + let height = contentInsets.top + contentHeight + contentInsets.bottom + + return CGSize(width: UIView.noIntrinsicMetric, height: height) + } + + private(set) var inputViewModel: AmountInputViewModelProtocol? + + var completed: Bool { + if let inputViewModel = inputViewModel { + return inputViewModel.isValid + } else { + return false + } + } + + var hasValidNumber: Bool { + inputViewModel?.decimalAmount != nil + } + + override public init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Layout + + override func layoutSubviews() { + super.layoutSubviews() + + contentView?.frame = bounds + + layoutContent() + } + + private func layoutContent() { + let availableWidth = bounds.width - contentInsets.left - contentInsets.right + let symbolSize = symbolHubMultiValueView.intrinsicContentSize + if let iconView = lazyIconView { + iconView.frame = CGRect( + x: bounds.minX + contentInsets.left, + y: bounds.midY - iconRadius, + width: 2.0 * iconRadius, + height: 2.0 * iconRadius + ) + + symbolHubMultiValueView.frame = CGRect( + x: iconView.frame.maxX + horizontalSpacing, + y: bounds.midY - symbolSize.height / 2.0, + width: min(availableWidth, symbolSize.width), + height: symbolSize.height + ) + } else { + symbolHubMultiValueView.frame = CGRect( + x: contentInsets.left, + y: bounds.midY - symbolSize.height / 2.0, + width: min(availableWidth, symbolSize.width), + height: symbolSize.height + ) + } + + let estimatedFieldWidth = bounds.maxX - contentInsets.right + - symbolHubMultiValueView.frame.maxX - horizontalSpacing + let fieldWidth = max(estimatedFieldWidth, 0.0) + + let hasPriceLabel = !priceLabel.text.isNilOrEmpty + let fieldHeight = textField.intrinsicContentSize.height + + let textFieldY: CGFloat + + if hasPriceLabel { + let fieldBaselineOffset = 4.0 + textFieldY = bounds.midY - fieldHeight + fieldBaselineOffset + } else { + textFieldY = bounds.midY - fieldHeight / 2.0 + } + + textField.frame = CGRect( + x: bounds.maxX - contentInsets.right - fieldWidth, + y: textFieldY, + width: fieldWidth, + height: fieldHeight + ) + + priceLabel.frame = CGRect( + x: bounds.maxX - contentInsets.right - fieldWidth, + y: textField.frame.maxY, + width: fieldWidth, + height: priceLabel.intrinsicContentSize.height + ) + } + + // MARK: Configure + + private func configure() { + backgroundColor = UIColor.clear + + configureBackgroundViewIfNeeded() + configureContentViewIfNeeded() + configureLocalHandlers() + configureTextFieldHandlers() + } + + private func configureBackgroundViewIfNeeded() { + if backgroundView == nil { + let roundedView = RoundedView() + roundedView.apply(style: .strokeOnEditing) + roundedView.isUserInteractionEnabled = false + backgroundView = roundedView + } + } + + private func configureLocalHandlers() { + addTarget(self, action: #selector(actionTouchUpInside), for: .touchUpInside) + } + + private func configureTextFieldHandlers() { + textField.delegate = self + + textField.addTarget( + self, + action: #selector(actionEditingDidBeginEnd), + for: .editingDidBegin + ) + + textField.addTarget( + self, + action: #selector(actionEditingDidBeginEnd), + for: .editingDidEnd + ) + } + + private func configureContentViewIfNeeded() { + if contentView == nil { + let contentView = UIView() + contentView.backgroundColor = .clear + contentView.isUserInteractionEnabled = false + self.contentView = contentView + } + + contentView?.addSubview(symbolHubMultiValueView) + contentView?.addSubview(priceLabel) + addSubview(textField) + + contentInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 16) + } + + private func lazyIconViewOrCreateIfNeeded() -> AssetIconView { + if let iconView = lazyIconView { + return iconView + } + + let size = 2 * iconRadius + let initFrame = CGRect(origin: .zero, size: .init(width: size, height: size)) + let imageView = AssetIconView(frame: initFrame) + imageView.contentInsets = iconViewContentInsets + imageView.backgroundView.cornerRadius = iconRadius + contentView?.addSubview(imageView) + + lazyIconView = imageView + + if superview != nil { + setNeedsLayout() + } + + return imageView + } + + // MARK: Action + + @objc private func actionEditingDidBeginEnd() { + roundedBackgroundView?.strokeWidth = textField.isFirstResponder ? 0.5 : 0.0 + } + + @objc private func actionTouchUpInside() { + if !textField.isHidden { + textField.becomeFirstResponder() + } + } +} + +extension SwapAmountInputView: UITextFieldDelegate { + func textField( + _: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + inputViewModel?.didReceiveReplacement(string, for: range) ?? false + } +} + +extension SwapAmountInputView: AmountInputViewModelObserver { + func amountInputDidChange() { + textField.text = inputViewModel?.displayAmount + + sendActions(for: .editingChanged) + } +} + +extension SwapAmountInputView { + func bind(assetViewModel: SwapsAssetViewModel) { + let width = 2 * iconRadius - iconView.contentInsets.left - iconView.contentInsets.right + let height = 2 * iconRadius - iconView.contentInsets.top - iconView.contentInsets.bottom + iconViewContentInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) + let size = CGSize(width: width, height: height) + iconView.bind(viewModel: assetViewModel.imageViewModel, size: size) + + symbolHubMultiValueView.bind( + symbol: assetViewModel.symbol, + network: assetViewModel.hub.name, + icon: assetViewModel.hub.icon + ) + setNeedsLayout() + } + + func bind(emptyViewModel: EmptySwapsAssetViewModel) { + let size = CGSize(width: 2 * iconRadius, height: 2 * iconRadius) + iconView.bind(viewModel: emptyViewModel.imageViewModel, size: size) + iconViewContentInsets = .zero + symbolHubMultiValueView.bind( + symbol: emptyViewModel.title, + network: emptyViewModel.subtitle, + icon: nil + ) + textField.isHidden = true + setNeedsLayout() + } + + func bind(inputViewModel: AmountInputViewModelProtocol) { + textField.isHidden = false + self.inputViewModel?.observable.remove(observer: self) + inputViewModel.observable.add(observer: self) + + self.inputViewModel = inputViewModel + textField.text = inputViewModel.displayAmount + } + + func bind(priceViewModel: String?) { + priceLabel.text = priceViewModel + + setNeedsLayout() + } +} + +struct SwapsAssetViewModel { + let symbol: String + let imageViewModel: ImageViewModelProtocol? + let hub: NetworkViewModel +} + +struct EmptySwapsAssetViewModel { + let imageViewModel: ImageViewModelProtocol? + let title: String + let subtitle: String +} diff --git a/novawallet/Modules/AssetList/AssetListPresenter.swift b/novawallet/Modules/AssetList/AssetListPresenter.swift index d4260a4dd5..9cb7466717 100644 --- a/novawallet/Modules/AssetList/AssetListPresenter.swift +++ b/novawallet/Modules/AssetList/AssetListPresenter.swift @@ -429,6 +429,10 @@ extension AssetListPresenter: AssetListPresenterProtocol { wireframe.showBuyTokens(from: view) } + func swap() { + wireframe.showSwapTokens(from: view) + } + func presentWalletConnect() { if walletConnectSessionsCount > 0 { wireframe.showWalletConnect(from: view) diff --git a/novawallet/Modules/AssetList/AssetListProtocols.swift b/novawallet/Modules/AssetList/AssetListProtocols.swift index 3794b0e1b0..02aff21019 100644 --- a/novawallet/Modules/AssetList/AssetListProtocols.swift +++ b/novawallet/Modules/AssetList/AssetListProtocols.swift @@ -23,6 +23,7 @@ protocol AssetListPresenterProtocol: AnyObject { func send() func receive() func buy() + func swap() func presentWalletConnect() } @@ -73,6 +74,8 @@ protocol AssetListWireframeProtocol: AnyObject, WalletSwitchPresentable, AlertPr ) func showBuyTokens(from view: AssetListViewProtocol?) + + func showSwapTokens(from view: AssetListViewProtocol?) } typealias WalletConnectSessionsError = WalletConnectSessionsInteractorError diff --git a/novawallet/Modules/AssetList/AssetListViewController.swift b/novawallet/Modules/AssetList/AssetListViewController.swift index d166d3367a..2a3a996dfa 100644 --- a/novawallet/Modules/AssetList/AssetListViewController.swift +++ b/novawallet/Modules/AssetList/AssetListViewController.swift @@ -122,6 +122,10 @@ final class AssetListViewController: UIViewController, ViewHolder { @objc private func actionBuy() { presenter.buy() } + + @objc private func actionSwap() { + presenter.swap() + } } extension AssetListViewController: UICollectionViewDelegateFlowLayout { @@ -267,6 +271,11 @@ extension AssetListViewController: UICollectionViewDataSource { action: #selector(actionBuy), for: .touchUpInside ) + totalBalanceCell.swapButton.addTarget( + self, + action: #selector(actionSwap), + for: .touchUpInside + ) if let viewModel = headerViewModel { totalBalanceCell.bind(viewModel: viewModel) } diff --git a/novawallet/Modules/AssetList/AssetListWireframe.swift b/novawallet/Modules/AssetList/AssetListWireframe.swift index 4839d46da8..3d1ebef65f 100644 --- a/novawallet/Modules/AssetList/AssetListWireframe.swift +++ b/novawallet/Modules/AssetList/AssetListWireframe.swift @@ -127,6 +127,18 @@ final class AssetListWireframe: AssetListWireframeProtocol { view?.controller.present(navigationController, animated: true, completion: nil) } + func showSwapTokens(from view: AssetListViewProtocol?) { + guard let swapTokensView = SwapSetupViewFactory.createView() else { + return + } + + let navigationController = NovaNavigationController( + rootViewController: swapTokensView.controller + ) + + view?.controller.present(navigationController, animated: true, completion: nil) + } + func showNfts(from view: AssetListViewProtocol?) { guard let nftListView = NftListViewFactory.createView() else { return diff --git a/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift b/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift index 8a8300567f..93546d03ab 100644 --- a/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift +++ b/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift @@ -51,6 +51,12 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { R.string.localizable.walletAssetReceive(preferredLanguages: locale.rLanguages), icon: R.image.iconReceive() ) + lazy var swapButton = createActionButton( + title: R.string.localizable.walletAssetsSwap( + preferredLanguages: locale.rLanguages + ), + icon: R.image.iconActionChange() + ) lazy var buyButton = createActionButton( title: R.string.localizable.walletAssetBuy( preferredLanguages: locale.rLanguages @@ -63,6 +69,7 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { [ sendButton, receiveButton, + swapButton, buyButton ] ) @@ -206,6 +213,9 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { buyButton.imageWithTitleView?.title = R.string.localizable.walletAssetBuy( preferredLanguages: locale.rLanguages ) + swapButton.imageWithTitleView?.title = R.string.localizable.walletAssetsSwap( + preferredLanguages: locale.rLanguages + ) } private func setupLayout() { diff --git a/novawallet/Modules/SwapSetup/SwapSetupInteractor.swift b/novawallet/Modules/SwapSetup/SwapSetupInteractor.swift new file mode 100644 index 0000000000..0b4035f0f2 --- /dev/null +++ b/novawallet/Modules/SwapSetup/SwapSetupInteractor.swift @@ -0,0 +1,7 @@ +import UIKit + +final class SwapSetupInteractor { + weak var presenter: SwapSetupInteractorOutputProtocol? +} + +extension SwapSetupInteractor: SwapSetupInteractorInputProtocol {} diff --git a/novawallet/Modules/SwapSetup/SwapSetupPresenter.swift b/novawallet/Modules/SwapSetup/SwapSetupPresenter.swift new file mode 100644 index 0000000000..aed0b0077a --- /dev/null +++ b/novawallet/Modules/SwapSetup/SwapSetupPresenter.swift @@ -0,0 +1,76 @@ +import Foundation +import SoraFoundation + +final class SwapSetupPresenter { + weak var view: SwapSetupViewProtocol? + let wireframe: SwapSetupWireframeProtocol + let interactor: SwapSetupInteractorInputProtocol + let balanceViewModelFactory: BalanceViewModelFactoryFacadeProtocol + + init( + interactor: SwapSetupInteractorInputProtocol, + wireframe: SwapSetupWireframeProtocol, + balanceViewModelFactory: BalanceViewModelFactoryFacadeProtocol, + localizationManager _: LocalizationManagerProtocol + ) { + self.interactor = interactor + self.wireframe = wireframe + self.balanceViewModelFactory = balanceViewModelFactory + } +} + +extension SwapSetupPresenter: SwapSetupPresenterProtocol { + func setup() { + view?.didReceiveButtonState(title: "Enter amount", enabled: false) + view?.didReceiveInputChainAsset(payViewModel: dotModel()) + view?.didReceiveAmount(payInputViewModel: amount()) + view?.didReceiveAmountInputPrice(payViewModel: "$0") + view?.didReceiveInputChainAsset(receiveViewModel: nil) + } + + func dotModel() -> SwapsAssetViewModel { + let dotImage = RemoteImageViewModel(url: URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/chains/white/Polkadot.svg")!) + let hubImage = RemoteImageViewModel(url: URL(string: "https://parachains.info/images/parachains/1688559044_assethub.svg")!) + return SwapsAssetViewModel( + symbol: "DOT", + imageViewModel: dotImage, + hub: .init( + name: "Polkadot Asset Hub", + icon: hubImage + ) + ) + } + + func amount() -> AmountInputViewModelProtocol { + let targetAssetInfo = AssetBalanceDisplayInfo( + displayPrecision: 2, + assetPrecision: 10, + symbol: "DOT", + symbolValueSeparator: "", + symbolPosition: .suffix, + icon: nil + ) + return balanceViewModelFactory.createBalanceInputViewModel( + targetAssetInfo: targetAssetInfo, + amount: 0 + ).value(for: selectedLocale) + } + + func selectPayToken() { + print("SELECT PAY TOKEN") + } + + func selectReceiveToken() { + print("SELECT RECEIVE TOKEN") + } +} + +extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol {} + +extension SwapSetupPresenter: Localizable { + func applyLocalization() { + if view?.isSetup == true { + setup() + } + } +} diff --git a/novawallet/Modules/SwapSetup/SwapSetupProtocols.swift b/novawallet/Modules/SwapSetup/SwapSetupProtocols.swift new file mode 100644 index 0000000000..85960f7091 --- /dev/null +++ b/novawallet/Modules/SwapSetup/SwapSetupProtocols.swift @@ -0,0 +1,21 @@ +protocol SwapSetupViewProtocol: ControllerBackedProtocol { + func didReceiveButtonState(title: String, enabled: Bool) + func didReceiveInputChainAsset(payViewModel viewModel: SwapsAssetViewModel?) + func didReceiveAmount(payInputViewModel inputViewModel: AmountInputViewModelProtocol) + func didReceiveAmountInputPrice(payViewModel: String?) + func didReceiveInputChainAsset(receiveViewModel viewModel: SwapsAssetViewModel?) + func didReceiveAmount(receiveInputViewModel inputViewModel: AmountInputViewModelProtocol) + func didReceiveAmountInputPrice(receiveViewModel: String?) +} + +protocol SwapSetupPresenterProtocol: AnyObject { + func setup() + func selectPayToken() + func selectReceiveToken() +} + +protocol SwapSetupInteractorInputProtocol: AnyObject {} + +protocol SwapSetupInteractorOutputProtocol: AnyObject {} + +protocol SwapSetupWireframeProtocol: AnyObject {} diff --git a/novawallet/Modules/SwapSetup/SwapSetupViewController.swift b/novawallet/Modules/SwapSetup/SwapSetupViewController.swift new file mode 100644 index 0000000000..450da9eea4 --- /dev/null +++ b/novawallet/Modules/SwapSetup/SwapSetupViewController.swift @@ -0,0 +1,103 @@ +import UIKit + +final class SwapSetupViewController: UIViewController, ViewHolder { + typealias RootViewType = SwapSetupViewLayout + + let presenter: SwapSetupPresenterProtocol + + init(presenter: SwapSetupPresenterProtocol) { + self.presenter = presenter + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = SwapSetupViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupHandlers() + presenter.setup() + } + + private func setupHandlers() { + rootView.payAmountInputView.symbolHubMultiValueView.addGestureRecognizer( + UITapGestureRecognizer( + target: self, + action: #selector(selectPayTokenAction) + )) + rootView.receiveAmountInputView.symbolHubMultiValueView.addGestureRecognizer( + UITapGestureRecognizer( + target: self, + action: #selector(selectReceiveTokenAction) + )) + } + + private func emptyPayViewModel() -> EmptySwapsAssetViewModel { + EmptySwapsAssetViewModel( + imageViewModel: StaticImageViewModel(image: R.image.iconAddSwapAmount()!), + title: "Pay", + subtitle: "Select a token" + ) + } + + private func emptyReceiveViewModel() -> EmptySwapsAssetViewModel { + EmptySwapsAssetViewModel( + imageViewModel: StaticImageViewModel(image: R.image.iconAddSwapAmount()!), + title: "Receive", + subtitle: "Select a token" + ) + } + + @objc private func selectPayTokenAction() { + presenter.selectPayToken() + } + + @objc private func selectReceiveTokenAction() { + presenter.selectReceiveToken() + } +} + +extension SwapSetupViewController: SwapSetupViewProtocol { + func didReceiveButtonState(title: String, enabled: Bool) { + rootView.actionButton.applyState(title: title, enabled: enabled) + } + + func didReceiveInputChainAsset(payViewModel viewModel: SwapsAssetViewModel?) { + if let viewModel = viewModel { + rootView.payAmountInputView.bind(assetViewModel: viewModel) + } else { + rootView.payAmountInputView.bind(emptyViewModel: emptyPayViewModel()) + } + } + + func didReceiveAmount(payInputViewModel inputViewModel: AmountInputViewModelProtocol) { + rootView.payAmountInputView.bind(inputViewModel: inputViewModel) + } + + func didReceiveAmountInputPrice(payViewModel viewModel: String?) { + rootView.payAmountInputView.bind(priceViewModel: viewModel) + } + + func didReceiveInputChainAsset(receiveViewModel viewModel: SwapsAssetViewModel?) { + if let viewModel = viewModel { + rootView.receiveAmountInputView.bind(assetViewModel: viewModel) + } else { + rootView.receiveAmountInputView.bind(emptyViewModel: emptyReceiveViewModel()) + } + } + + func didReceiveAmount(receiveInputViewModel inputViewModel: AmountInputViewModelProtocol) { + rootView.receiveAmountInputView.bind(inputViewModel: inputViewModel) + } + + func didReceiveAmountInputPrice(receiveViewModel viewModel: String?) { + rootView.receiveAmountInputView.bind(priceViewModel: viewModel) + } +} diff --git a/novawallet/Modules/SwapSetup/SwapSetupViewFactory.swift b/novawallet/Modules/SwapSetup/SwapSetupViewFactory.swift new file mode 100644 index 0000000000..6cad9401fe --- /dev/null +++ b/novawallet/Modules/SwapSetup/SwapSetupViewFactory.swift @@ -0,0 +1,31 @@ +import Foundation +import SoraFoundation + +struct SwapSetupViewFactory { + static func createView() -> SwapSetupViewProtocol? { + guard + let currencyManager = CurrencyManager.shared else { + return nil + } + + let balanceViewModelFactory = BalanceViewModelFactoryFacade( + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager)) + + let interactor = SwapSetupInteractor() + let wireframe = SwapSetupWireframe() + + let presenter = SwapSetupPresenter( + interactor: interactor, + wireframe: wireframe, + balanceViewModelFactory: balanceViewModelFactory, + localizationManager: LocalizationManager.shared + ) + + let view = SwapSetupViewController(presenter: presenter) + + presenter.view = view + interactor.presenter = presenter + + return view + } +} diff --git a/novawallet/Modules/SwapSetup/SwapSetupViewLayout.swift b/novawallet/Modules/SwapSetup/SwapSetupViewLayout.swift new file mode 100644 index 0000000000..2568d71cd8 --- /dev/null +++ b/novawallet/Modules/SwapSetup/SwapSetupViewLayout.swift @@ -0,0 +1,57 @@ +import UIKit + +final class SwapSetupViewLayout: ScrollableContainerLayoutView { + let payAmountView: TitleHorizontalMultiValueView = .create { + $0.titleView.apply(style: .footnoteSecondary) + $0.detailsTitleLabel.apply(style: .footnoteSecondary) + $0.detailsValueLabel.apply(style: .footnotePrimary) + } + + let payAmountInputView = SwapAmountInputView() + + let receiveAmountView: TitleHorizontalMultiValueView = .create { + $0.titleView.apply(style: .footnoteSecondary) + $0.detailsTitleLabel.apply(style: .footnoteSecondary) + $0.detailsValueLabel.apply(style: .footnotePrimary) + } + + let receiveAmountInputView = SwapAmountInputView() + + let actionButton: TriangularedButton = .create { + $0.applyDefaultStyle() + } + + override func setupStyle() { + backgroundColor = R.color.colorSecondaryScreenBackground() + } + + override func setupLayout() { + super.setupLayout() + + stackView.layoutMargins = UIEdgeInsets(top: 12, left: 16, bottom: 0, right: 16) + + addSubview(actionButton) + actionButton.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) + make.bottom.equalTo(safeAreaLayoutGuide).inset(UIConstants.actionBottomInset) + make.height.equalTo(UIConstants.actionHeight) + } + + addArrangedSubview(payAmountView, spacingAfter: 8) + payAmountView.snp.makeConstraints { + $0.height.equalTo(18) + } + addArrangedSubview(payAmountInputView, spacingAfter: 24) + payAmountInputView.snp.makeConstraints { + $0.height.equalTo(64) + } + addArrangedSubview(receiveAmountView, spacingAfter: 8) + receiveAmountView.snp.makeConstraints { + $0.height.equalTo(18) + } + addArrangedSubview(receiveAmountInputView) + receiveAmountInputView.snp.makeConstraints { + $0.height.equalTo(64) + } + } +} diff --git a/novawallet/Modules/SwapSetup/SwapSetupWireframe.swift b/novawallet/Modules/SwapSetup/SwapSetupWireframe.swift new file mode 100644 index 0000000000..d397c0756f --- /dev/null +++ b/novawallet/Modules/SwapSetup/SwapSetupWireframe.swift @@ -0,0 +1,3 @@ +import Foundation + +final class SwapSetupWireframe: SwapSetupWireframeProtocol {} diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index c5b13b8f45..fbaffbcb47 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1376,3 +1376,4 @@ "common.back" = "Back"; "asset.operation.send.empty.state.message" = "You don’t have tokens to send.\nBuy or Receive tokens to your\naccount."; "governance.referendums.status.deciding" = "Deciding"; +"wallet.assets.swap" = "Swap"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 5c00bc04f7..4ee839b904 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1376,3 +1376,4 @@ "common.back" = "Назад"; "asset.operation.send.empty.state.message" = "У вас нет токенов для отправки.\nКупите или получите токены\nна свой аккаунт."; "governance.referendums.status.deciding" = "На рассмотрении"; +"wallet.assets.swap" = "Обмен"; diff --git a/novawalletTests/Modules/SwapSetup/SwapSetupTests.swift b/novawalletTests/Modules/SwapSetup/SwapSetupTests.swift new file mode 100644 index 0000000000..65c8446bda --- /dev/null +++ b/novawalletTests/Modules/SwapSetup/SwapSetupTests.swift @@ -0,0 +1,16 @@ +import XCTest + +class SwapSetupTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + XCTFail("Did you forget to add tests?") + } +} From 205eb6721a0a282850c831be8f1b9f31ac0980bc Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Mon, 2 Oct 2023 22:49:51 +0300 Subject: [PATCH 010/204] add swap button, add titles --- .../iconActionSwap.imageset/Contents.json | 12 ++++++++++++ .../container-transaction-type.pdf | Bin 0 -> 3770 bytes .../Modules/SwapSetup/SwapSetupPresenter.swift | 2 ++ .../Modules/SwapSetup/SwapSetupProtocols.swift | 2 ++ .../SwapSetup/SwapSetupViewController.swift | 8 ++++++++ .../Modules/SwapSetup/SwapSetupViewLayout.swift | 16 +++++++++++++++- 6 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 novawallet/Assets.xcassets/iconActionSwap.imageset/Contents.json create mode 100644 novawallet/Assets.xcassets/iconActionSwap.imageset/container-transaction-type.pdf diff --git a/novawallet/Assets.xcassets/iconActionSwap.imageset/Contents.json b/novawallet/Assets.xcassets/iconActionSwap.imageset/Contents.json new file mode 100644 index 0000000000..96847b1a57 --- /dev/null +++ b/novawallet/Assets.xcassets/iconActionSwap.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "container-transaction-type.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconActionSwap.imageset/container-transaction-type.pdf b/novawallet/Assets.xcassets/iconActionSwap.imageset/container-transaction-type.pdf new file mode 100644 index 0000000000000000000000000000000000000000..53e799c5e24cec49b702cc85ecb4e0fed4bbcd74 GIT binary patch literal 3770 zcmc&%O>f*r487}D%q2i_h;}$%oB;v@v7H!28^pDH2oUtJSu2jy^{%^;i=@B4k0WU$ zd!42(P*ewd^fV-gkK~a1(jNNKhG@VQdz#S69gAiwwDvP>^{4t=}jroCD2n$7C#o$m(+gR1k}4_!9;db_(G zu(PJ!-tJaSn@zm>(w&@l%dY9)O#Zi8DqY8@@u*SPWO{$KctwwwJ2)u17l()2FX}yv zib)tP3u{JbTKx(~aH_W3HOuR2aoqfIwQ9~z4%N~5{B6}%zgKdy%eJCXOHus1tjf1o zQ%cE`B3+X+$vL&UotNIxm`jv;Nss>`X`CUSu*--l5=49JM9xQYma;CRHBJO9 z1j9jRU4l7QsSaeE75#9=IA@~b+F47%14Ha}VrF25qKTr!v9WLth}IxSj9ZpZTw6|* zZGtwYm;`e+L=yeNvrRY@z~Cu3tpxHuL4@lRgR!EY3?*Y#gvAs=!3w7!PNEH1^d3|R z8uf+WH&ECL7U$Z{zNZJ-j&!GJKRZT233C947gZ~9$A zrC}h!aOe<>*`bWvD#EW=j=ECGW%hG z|3UVX!#RDR0D294&<5!b5N;Tjw08(|h>AP{%mG;l0g#rAKvoiy0D&zN6Wj)fTqM+> z=yL)3kfI(0C1M8>VNXjy@*^3oI;2dfx0_X&!+E$WlUhfv^`|5agJo0`M-uEsU<9%O)L=u>+w}6AXgc0~z>?bqZFa zXnQ&nF0U$~xMPF^nN6#+_ar5Z1Xrd8F&W5o59_eQ8t2#7@qbFNKiG)t8NOWvXC zu@AB_A|EAmxNdRF2&$bI>shCuNCI(*ShIlpsX3KK)odS*SBpWhp z3;dl9?*69^KKAGTu7^&o5<|_ND(mo4HVHBUq7*|J&LieZ> zgx4WyC*_mGgdszUo(Wx7lcEWoRu6rh^q?=7A!I_)iwZg%&Z|k$1AY}~e#ipKF8u7(Q$drM%7_R%~RbwRGTFCG_Ktn8vEhz{LQlc zEerH>uDsrvO84i+9$B0ysj^}G{noAlUtsQixxV?d{J!e3G9J`84CBRbxnR45y- z1dJ;)riKBYM}{6VmM%c;@fNN6f%`}l4p9olG)BCc>6e?$wrkaU8Hn)=E}bqfn*n&! zTwE<5J6emT={JI;#rVVIXuIi}4J--+BX}ymKRShRG($6<1nNDItMkXJ3mjF62WM(L z@zM4cvb1;p7RXGKwA+~~@vtoxC(n9P%FI&@V@*+byrw&8`x9 ze>euYPuuNv^GrRxx_H)8yg1u#!LQJl Date: Tue, 3 Oct 2023 15:05:12 +0300 Subject: [PATCH 011/204] refactor control --- novawallet.xcodeproj/project.pbxproj | 22 +- .../Swap/SwapAmountInput.swift | 152 +++++++ .../Swap/SwapAmountInputView.swift | 149 +++++++ .../Swap/SwapAssetControl.swift | 165 ++++++++ .../AmountInputView/Swap/SwapSymbolView.swift | 68 +++ .../AmountInputView/SwapAmountInputView.swift | 393 ------------------ .../SwapSetup/SwapSetupViewController.swift | 20 +- 7 files changed, 565 insertions(+), 404 deletions(-) create mode 100644 novawallet/Common/View/AmountInputView/Swap/SwapAmountInput.swift create mode 100644 novawallet/Common/View/AmountInputView/Swap/SwapAmountInputView.swift create mode 100644 novawallet/Common/View/AmountInputView/Swap/SwapAssetControl.swift create mode 100644 novawallet/Common/View/AmountInputView/Swap/SwapSymbolView.swift delete mode 100644 novawallet/Common/View/AmountInputView/SwapAmountInputView.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 1e34a70884..738a2d47e7 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -670,6 +670,9 @@ 7731E9C42A14DA3F0085B5FF /* BorderedActionControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7731E9C32A14DA3F0085B5FF /* BorderedActionControlView.swift */; }; 7738FB6A2A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7738FB692A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift */; }; 774091F92ACB1F4B00172516 /* SwapAmountInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774091F82ACB1F4B00172516 /* SwapAmountInputView.swift */; }; + 774091FC2ACC053000172516 /* SwapSymbolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774091FB2ACC053000172516 /* SwapSymbolView.swift */; }; + 774091FE2ACC054B00172516 /* SwapAssetControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774091FD2ACC054B00172516 /* SwapAssetControl.swift */; }; + 774092002ACC1BE400172516 /* SwapAmountInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774091FF2ACC1BE400172516 /* SwapAmountInput.swift */; }; 774A481129F8BFB70094635B /* OperationAuthPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774A481029F8BFB70094635B /* OperationAuthPresentable.swift */; }; 7756927D2A20B88200220756 /* TokenOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7756927C2A20B88200220756 /* TokenOperation.swift */; }; 775F194D2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775F194C2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift */; }; @@ -4637,6 +4640,9 @@ 7731E9C32A14DA3F0085B5FF /* BorderedActionControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedActionControlView.swift; sourceTree = ""; }; 7738FB692A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingRelaychainInteractor.swift; sourceTree = ""; }; 774091F82ACB1F4B00172516 /* SwapAmountInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAmountInputView.swift; sourceTree = ""; }; + 774091FB2ACC053000172516 /* SwapSymbolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapSymbolView.swift; sourceTree = ""; }; + 774091FD2ACC054B00172516 /* SwapAssetControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetControl.swift; sourceTree = ""; }; + 774091FF2ACC1BE400172516 /* SwapAmountInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAmountInput.swift; sourceTree = ""; }; 774A481029F8BFB70094635B /* OperationAuthPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationAuthPresentable.swift; sourceTree = ""; }; 7756927C2A20B88200220756 /* TokenOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenOperation.swift; sourceTree = ""; }; 775F194C2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoRelaychainPresenter.swift; sourceTree = ""; }; @@ -9398,6 +9404,17 @@ path = RelaychainStaking; sourceTree = ""; }; + 774091FA2ACC052400172516 /* Swap */ = { + isa = PBXGroup; + children = ( + 774091F82ACB1F4B00172516 /* SwapAmountInputView.swift */, + 774091FB2ACC053000172516 /* SwapSymbolView.swift */, + 774091FD2ACC054B00172516 /* SwapAssetControl.swift */, + 774091FF2ACC1BE400172516 /* SwapAmountInput.swift */, + ); + path = Swap; + sourceTree = ""; + }; 775692822A24CA5100220756 /* AssetOperation */ = { isa = PBXGroup; children = ( @@ -9844,7 +9861,7 @@ 8401620F25E144DA0087A5F3 /* AmountInputView */ = { isa = PBXGroup; children = ( - 774091F82ACB1F4B00172516 /* SwapAmountInputView.swift */, + 774091FA2ACC052400172516 /* Swap */, 840DCBF025DFEE4800D45C6A /* AmountInputView.swift */, 840DCBF525E0059D00D45C6A /* AmountInputView+Inspectable.swift */, 8401620A25E144D50087A5F3 /* AmountInputAccessoryView.swift */, @@ -19233,6 +19250,7 @@ 848CCB4C2833979700A1FD00 /* ParaStkStateViewModelFactory+Alert.swift in Sources */, 8444407528AA628300446D22 /* LedgerAccount.swift in Sources */, 84C74361251E4B5E009576C6 /* FeeType.swift in Sources */, + 774091FE2ACC054B00172516 /* SwapAssetControl.swift in Sources */, 844DAAE128AD106B008E11DA /* UInt+Serialization.swift in Sources */, 84981EE429D3352600948306 /* TransactionHistoryFetching.swift in Sources */, 8428765224ADDE0200D91AD8 /* SettingsPresenter.swift in Sources */, @@ -20266,6 +20284,7 @@ 840D92A1278D8D6F0007B979 /* DAppBrowserStateError.swift in Sources */, 84FC190B29B7DB9F00BCCAA5 /* ExtrinsicServiceTypes.swift in Sources */, 849707A128F3E0AC00DD0A02 /* ReferendumVoterLocal.swift in Sources */, + 774091FC2ACC053000172516 /* SwapSymbolView.swift in Sources */, 84754C882510BAFE00854599 /* ModalAlertFactory.swift in Sources */, 8430AACC2602249B005B1066 /* InitialStakingState.swift in Sources */, 0C56B4FB2A4B0C320030F9C9 /* AssetListBaseBuilder.swift in Sources */, @@ -22136,6 +22155,7 @@ 84770F25291F72D700852A33 /* ReferendumVotingInitData.swift in Sources */, 84E0C51E29CA40DA000B65C8 /* OperationContractCallModel.swift in Sources */, 846A835D28B8D09600D92892 /* LedgerMessageSheetViewFactory.swift in Sources */, + 774092002ACC1BE400172516 /* SwapAmountInput.swift in Sources */, 60FFEE5B386E82D70333BE80 /* CreateWatchOnlyPresenter.swift in Sources */, 842E9EA02A2DC23900759972 /* StakingDashboardBuilderProtocol.swift in Sources */, 454D41CC5C7CC2FDAB778026 /* CreateWatchOnlyInteractor.swift in Sources */, diff --git a/novawallet/Common/View/AmountInputView/Swap/SwapAmountInput.swift b/novawallet/Common/View/AmountInputView/Swap/SwapAmountInput.swift new file mode 100644 index 0000000000..6668a2ffeb --- /dev/null +++ b/novawallet/Common/View/AmountInputView/Swap/SwapAmountInput.swift @@ -0,0 +1,152 @@ +import SoraUI + +final class SwapAmountInput: BackgroundedContentControl { + let textField: UITextField = .create { + $0.font = .title2 + $0.textColor = R.color.colorTextPrimary() + $0.tintColor = R.color.colorTextPrimary() + $0.textAlignment = .right + $0.attributedPlaceholder = NSAttributedString( + string: "0", + attributes: [ + .foregroundColor: R.color.colorHintText()!, + .font: UIFont.title2 + ] + ) + $0.keyboardType = .decimalPad + } + + let priceLabel = UILabel( + style: .footnoteSecondary, + textAlignment: .right, + numberOfLines: 1 + ) + + let spacing: CGFloat = 4 + + override var intrinsicContentSize: CGSize { + let contentHeight = textField.intrinsicContentSize.height + spacing + priceLabel.intrinsicContentSize.height + + let height = contentInsets.top + contentHeight + contentInsets.bottom + + return CGSize(width: UIView.noIntrinsicMetric, height: height) + } + + private(set) var inputViewModel: AmountInputViewModelProtocol? + + var completed: Bool { + inputViewModel?.isValid == true + } + + var hasValidNumber: Bool { + inputViewModel?.decimalAmount != nil + } + + override public init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + contentView?.frame = bounds + + layoutContent() + } + + private func layoutContent() { + let availableWidth = bounds.width - contentInsets.left - contentInsets.right + let textFieldWidth = max(availableWidth, 0) + + let textFieldHeight: CGFloat = textField.intrinsicContentSize.height + let textFieldY: CGFloat + + if !priceLabel.text.isNilOrEmpty { + textFieldY = bounds.midY - textFieldHeight + spacing + } else { + textFieldY = bounds.midY - textFieldHeight / 2 + } + + textField.frame = CGRect( + x: bounds.maxX - contentInsets.right - textFieldWidth, + y: textFieldY, + width: textFieldWidth, + height: textFieldHeight + ) + + priceLabel.frame = CGRect( + x: bounds.maxX - contentInsets.right - textFieldWidth, + y: textField.frame.maxY, + width: textFieldWidth, + height: priceLabel.intrinsicContentSize.height + ) + } + + private func configure() { + backgroundColor = UIColor.clear + + configureContentViewIfNeeded() + configureLocalHandlers() + } + + private func configureLocalHandlers() { + addTarget(self, action: #selector(actionTouchUpInside), for: .touchUpInside) + } + + private func configureContentViewIfNeeded() { + if contentView == nil { + let contentView = UIView() + contentView.backgroundColor = .clear + contentView.isUserInteractionEnabled = false + self.contentView = contentView + } + + contentView?.addSubview(priceLabel) + addSubview(textField) + } + + @objc private func actionTouchUpInside() { + textField.becomeFirstResponder() + } +} + +extension SwapAmountInput: UITextFieldDelegate { + func textField( + _: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + inputViewModel?.didReceiveReplacement(string, for: range) ?? false + } +} + +extension SwapAmountInput: AmountInputViewModelObserver { + func amountInputDidChange() { + textField.text = inputViewModel?.displayAmount + + sendActions(for: .editingChanged) + } +} + +extension SwapAmountInput { + func bind(inputViewModel: AmountInputViewModelProtocol) { + textField.isHidden = false + self.inputViewModel?.observable.remove(observer: self) + inputViewModel.observable.add(observer: self) + + self.inputViewModel = inputViewModel + textField.text = inputViewModel.displayAmount + } + + func bind(priceViewModel: String?) { + priceLabel.text = priceViewModel + + setNeedsLayout() + } +} diff --git a/novawallet/Common/View/AmountInputView/Swap/SwapAmountInputView.swift b/novawallet/Common/View/AmountInputView/Swap/SwapAmountInputView.swift new file mode 100644 index 0000000000..a9bbce8792 --- /dev/null +++ b/novawallet/Common/View/AmountInputView/Swap/SwapAmountInputView.swift @@ -0,0 +1,149 @@ +import SoraUI + +final class SwapAmountInputView: RoundedView { + let swapAssetControl = SwapAssetControl() + let textInputView = SwapAmountInput() + + var contentInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 16) { + didSet { + setNeedsLayout() + } + } + + var horizontalSpacing: CGFloat = 8 { + didSet { + setNeedsLayout() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var intrinsicContentSize: CGSize { + let leftContentHeight = swapAssetControl.intrinsicContentSize.height + let rightContentHeight = textInputView.intrinsicContentSize.height + + let contentHeight = max(leftContentHeight, rightContentHeight) + + let height = contentInsets.top + contentHeight + contentInsets.bottom + + return CGSize(width: UIView.noIntrinsicMetric, height: height) + } + + // MARK: Layout + + override func layoutSubviews() { + super.layoutSubviews() + + layoutContent() + } + + private func layoutContent() { + let availableWidth = bounds.width - contentInsets.left - contentInsets.right + let swapAssetControlSize = swapAssetControl.intrinsicContentSize + + guard !textInputView.isHidden else { + swapAssetControl.frame = CGRect( + x: contentInsets.left, + y: bounds.midY - swapAssetControlSize.height / 2.0, + width: availableWidth, + height: swapAssetControlSize.height + ) + return + } + swapAssetControl.frame = CGRect( + x: contentInsets.left, + y: bounds.midY - swapAssetControlSize.height / 2.0, + width: min(min(availableWidth, swapAssetControlSize.width), 0.7 * availableWidth), + height: swapAssetControlSize.height + ) + + let estimatedInputViewWidth = bounds.maxX - contentInsets.right + - swapAssetControl.frame.maxX - horizontalSpacing + let inputWidth = max(estimatedInputViewWidth, 0) + let inputSize = textInputView.intrinsicContentSize + + textInputView.frame = CGRect( + x: bounds.maxX - contentInsets.right - inputWidth, + y: bounds.midY - inputSize.height / 2.0, + width: inputWidth, + height: inputSize.height + ) + } + + // MARK: Configure + + override func configure() { + super.configure() + + backgroundColor = UIColor.clear + + configureBackgroundViewIfNeeded() + configureContentViewIfNeeded() + configureInputViewHandlers() + } + + private func configureBackgroundViewIfNeeded() { + apply(style: .strokeOnEditing) + } + + private func configureContentViewIfNeeded() { + addSubview(swapAssetControl) + addSubview(textInputView) + + swapAssetControl.contentInsets = .zero + textInputView.contentInsets = .zero + } + + private func configureInputViewHandlers() { + textInputView.textField.addTarget( + self, + action: #selector(actionEditingDidBeginEnd), + for: .editingDidBegin + ) + + textInputView.textField.addTarget( + self, + action: #selector(actionEditingDidBeginEnd), + for: .editingDidEnd + ) + } + + // MARK: Action + + @objc private func actionEditingDidBeginEnd() { + strokeWidth = textInputView.isFirstResponder ? 0.5 : 0.0 + } +} + +extension SwapAmountInputView { + func bind(assetViewModel: SwapsAssetViewModel) { + swapAssetControl.bind(assetViewModel: assetViewModel) + swapAssetControl.invalidateIntrinsicContentSize() + setNeedsLayout() + } + + func bind(emptyViewModel: EmptySwapsAssetViewModel) { + swapAssetControl.bind(emptyViewModel: emptyViewModel) + textInputView.isHidden = true + swapAssetControl.invalidateIntrinsicContentSize() + setNeedsLayout() + } + + func bind(inputViewModel: AmountInputViewModelProtocol) { + textInputView.isHidden = false + textInputView.bind(inputViewModel: inputViewModel) + } + + func bind(priceViewModel: String?) { + textInputView.bind(priceViewModel: priceViewModel) + setNeedsLayout() + } +} diff --git a/novawallet/Common/View/AmountInputView/Swap/SwapAssetControl.swift b/novawallet/Common/View/AmountInputView/Swap/SwapAssetControl.swift new file mode 100644 index 0000000000..7da05373f5 --- /dev/null +++ b/novawallet/Common/View/AmountInputView/Swap/SwapAssetControl.swift @@ -0,0 +1,165 @@ +import UIKit +import SoraUI + +final class SwapAssetControl: BackgroundedContentControl { + var iconView: AssetIconView { lazyIconViewOrCreateIfNeeded() } + private var lazyIconView: AssetIconView? + + let symbolHubMultiValueView = SwapSymbolView() + + var iconViewContentInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) { + didSet { + lazyIconView?.contentInsets = iconViewContentInsets + setNeedsLayout() + } + } + + var horizontalSpacing: CGFloat = 8 { + didSet { + setNeedsLayout() + } + } + + var iconRadius: CGFloat = 16 { + didSet { + lazyIconView?.backgroundView.cornerRadius = iconRadius + + setNeedsLayout() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var intrinsicContentSize: CGSize { + let contentHeight = max( + lazyIconView?.intrinsicContentSize.height ?? 0.0, + symbolHubMultiValueView.intrinsicContentSize.height + ) + + let contentWidth = (lazyIconView?.intrinsicContentSize.width ?? 0.0) + horizontalSpacing + symbolHubMultiValueView.intrinsicContentSize.width + + let height = contentInsets.top + contentHeight + contentInsets.bottom + let width = contentInsets.left + contentWidth + contentInsets.right + + return CGSize(width: width, height: height) + } + + override func layoutSubviews() { + super.layoutSubviews() + + contentView?.frame = bounds + + layoutContent() + } + + private func layoutContent() { + let availableWidth = bounds.width - contentInsets.left - contentInsets.right + let symbolSize = symbolHubMultiValueView.intrinsicContentSize + + if let iconView = lazyIconView { + iconView.frame = CGRect( + x: bounds.minX + contentInsets.left, + y: bounds.midY - iconRadius, + width: 2.0 * iconRadius, + height: 2.0 * iconRadius + ) + + symbolHubMultiValueView.frame = CGRect( + x: iconView.frame.maxX + horizontalSpacing, + y: bounds.midY - symbolSize.height / 2.0, + width: min(availableWidth, symbolSize.width), + height: symbolSize.height + ) + } else { + symbolHubMultiValueView.frame = CGRect( + x: contentInsets.left, + y: bounds.midY - symbolSize.height / 2.0, + width: min(availableWidth, symbolSize.width), + height: symbolSize.height + ) + } + } + + private func configure() { + backgroundColor = UIColor.clear + + if contentView == nil { + let contentView = UIView() + contentView.backgroundColor = .clear + contentView.isUserInteractionEnabled = false + self.contentView = contentView + } + + contentView?.addSubview(symbolHubMultiValueView) + } + + private func lazyIconViewOrCreateIfNeeded() -> AssetIconView { + if let iconView = lazyIconView { + return iconView + } + + let size = 2 * iconRadius + let initFrame = CGRect(origin: .zero, size: .init(width: size, height: size)) + let imageView = AssetIconView(frame: initFrame) + imageView.contentInsets = iconViewContentInsets + imageView.backgroundView.cornerRadius = iconRadius + contentView?.addSubview(imageView) + + lazyIconView = imageView + + if superview != nil { + setNeedsLayout() + } + + return imageView + } +} + +struct SwapsAssetViewModel { + let symbol: String + let imageViewModel: ImageViewModelProtocol? + let hub: NetworkViewModel +} + +struct EmptySwapsAssetViewModel { + let imageViewModel: ImageViewModelProtocol? + let title: String + let subtitle: String +} + +extension SwapAssetControl { + func bind(assetViewModel: SwapsAssetViewModel) { + let width = 2 * iconRadius - iconView.contentInsets.left - iconView.contentInsets.right + let height = 2 * iconRadius - iconView.contentInsets.top - iconView.contentInsets.bottom + iconViewContentInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) + let size = CGSize(width: width, height: height) + iconView.bind(viewModel: assetViewModel.imageViewModel, size: size) + + symbolHubMultiValueView.bind( + symbol: assetViewModel.symbol, + network: assetViewModel.hub.name, + icon: assetViewModel.hub.icon + ) + invalidateIntrinsicContentSize() + } + + func bind(emptyViewModel: EmptySwapsAssetViewModel) { + let size = CGSize(width: 2 * iconRadius, height: 2 * iconRadius) + iconView.bind(viewModel: emptyViewModel.imageViewModel, size: size) + iconViewContentInsets = .zero + symbolHubMultiValueView.bind( + symbol: emptyViewModel.title, + network: emptyViewModel.subtitle, + icon: nil + ) + invalidateIntrinsicContentSize() + } +} diff --git a/novawallet/Common/View/AmountInputView/Swap/SwapSymbolView.swift b/novawallet/Common/View/AmountInputView/Swap/SwapSymbolView.swift new file mode 100644 index 0000000000..88966920b4 --- /dev/null +++ b/novawallet/Common/View/AmountInputView/Swap/SwapSymbolView.swift @@ -0,0 +1,68 @@ +import UIKit + +final class SwapSymbolView: GenericPairValueView, IconDetailsView> { + var symbolLabel: UILabel { fView.fView.detailsLabel } + var disclosureImageView: UIImageView { fView.fView.imageView } + var hubNameView: UILabel { sView.detailsLabel } + var hubImageView: UIImageView { sView.imageView } + + private var imageViewModel: ImageViewModelProtocol? + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure() { + fView.makeHorizontal() + fView.fView.spacing = 0 + fView.fView.iconWidth = 20 + fView.fView.mode = .detailsIcon + + sView.spacing = 8 + sView.iconWidth = 16 + sView.mode = .iconDetails + + spacing = 4 + makeVertical() + + symbolLabel.apply(style: .semiboldBodyPrimary) + hubNameView.apply(style: .footnoteSecondary) + } + + override var intrinsicContentSize: CGSize { + let symbolWidth = symbolLabel.intrinsicContentSize.width + fView.fView.iconWidth + let hubWidth = sView.iconWidth + sView.spacing + hubNameView.intrinsicContentSize.width + let width: CGFloat = max(symbolWidth, hubWidth) + let symbolHeight = max(symbolLabel.intrinsicContentSize.height, fView.fView.iconWidth) + let hubHeight = max(hubNameView.intrinsicContentSize.height, sView.iconWidth) + let height = symbolHeight + spacing + hubHeight + return .init( + width: width, + height: height + ) + } + + func bind(symbol: String, network: String, icon: ImageViewModelProtocol?) { + symbolLabel.text = symbol + imageViewModel?.cancel(on: hubImageView) + imageViewModel = icon + icon?.loadImage( + on: hubImageView, + targetSize: .init( + width: sView.iconWidth, + height: sView.iconWidth + ), + animated: true + ) + sView.hidesIcon = icon == nil + hubNameView.text = network + disclosureImageView.image = R.image.iconSmallArrow()?.tinted(with: R.color.colorIconSecondary()!) + invalidateIntrinsicContentSize() + } +} diff --git a/novawallet/Common/View/AmountInputView/SwapAmountInputView.swift b/novawallet/Common/View/AmountInputView/SwapAmountInputView.swift deleted file mode 100644 index 4a7f01a729..0000000000 --- a/novawallet/Common/View/AmountInputView/SwapAmountInputView.swift +++ /dev/null @@ -1,393 +0,0 @@ -import UIKit -import SoraUI - -final class SwapSymbolView: GenericPairValueView, IconDetailsView> { - var symbolLabel: UILabel { fView.fView.detailsLabel } - var disclosureImageView: UIImageView { fView.fView.imageView } - var hubNameView: UILabel { sView.detailsLabel } - var hubImageView: UIImageView { sView.imageView } - - private var imageViewModel: ImageViewModelProtocol? - - override init(frame: CGRect) { - super.init(frame: frame) - configure() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func configure() { - fView.makeHorizontal() - fView.fView.spacing = 0 - fView.fView.iconWidth = 20 - fView.fView.mode = .detailsIcon - - sView.spacing = 8 - sView.iconWidth = 16 - sView.mode = .iconDetails - - spacing = 4 - makeVertical() - - symbolLabel.apply(style: .semiboldBodyPrimary) - hubNameView.apply(style: .footnoteSecondary) - } - - override var intrinsicContentSize: CGSize { - let symbolWidth = symbolLabel.intrinsicContentSize.width + fView.fView.iconWidth - let hubWidth = sView.iconWidth + sView.spacing + hubNameView.intrinsicContentSize.width - let width: CGFloat = max(symbolWidth, hubWidth) - let symbolHeight = max(symbolLabel.intrinsicContentSize.height, fView.fView.iconWidth) - let hubHeight = max(hubNameView.intrinsicContentSize.height, sView.iconWidth) - let height = symbolHeight + spacing + hubHeight - return .init( - width: width, - height: height - ) - } - - func bind(symbol: String, network: String, icon: ImageViewModelProtocol?) { - symbolLabel.text = symbol - imageViewModel?.cancel(on: hubImageView) - imageViewModel = icon - icon?.loadImage( - on: hubImageView, - targetSize: .init( - width: sView.iconWidth, - height: sView.iconWidth - ), - animated: true - ) - sView.hidesIcon = icon == nil - hubNameView.text = network - disclosureImageView.image = R.image.iconSmallArrow()?.tinted(with: R.color.colorIconSecondary()!) - invalidateIntrinsicContentSize() - } -} - -final class SwapAmountInputView: BackgroundedContentControl { - var iconView: AssetIconView { lazyIconViewOrCreateIfNeeded() } - private var lazyIconView: AssetIconView? - - let symbolHubMultiValueView = SwapSymbolView() - var iconViewContentInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) { - didSet { - iconView.contentInsets = iconViewContentInsets - setNeedsLayout() - } - } - - let textField: UITextField = .create { - $0.font = .title2 - $0.textColor = R.color.colorTextPrimary() - $0.tintColor = R.color.colorTextPrimary() - $0.textAlignment = .right - $0.attributedPlaceholder = NSAttributedString( - string: "0", - attributes: [ - .foregroundColor: R.color.colorHintText()!, - .font: UIFont.title2 - ] - ) - $0.keyboardType = .decimalPad - } - - let priceLabel = UILabel( - style: .footnoteSecondary, - textAlignment: .right, - numberOfLines: 1 - ) - - var roundedBackgroundView: RoundedView? { - backgroundView as? RoundedView - } - - var horizontalSpacing: CGFloat = 8 { - didSet { - setNeedsLayout() - } - } - - var iconRadius: CGFloat = 16 { - didSet { - lazyIconView?.backgroundView.cornerRadius = iconRadius - - setNeedsLayout() - } - } - - override var intrinsicContentSize: CGSize { - let rightContentHeight = max( - textField.intrinsicContentSize.height, - priceLabel.intrinsicContentSize.height - ) - - let leftContentHeight = max( - lazyIconView?.intrinsicContentSize.height ?? 0.0, - symbolHubMultiValueView.intrinsicContentSize.height - ) - - let contentHeight = max(leftContentHeight, rightContentHeight) - - let height = contentInsets.top + contentHeight + contentInsets.bottom - - return CGSize(width: UIView.noIntrinsicMetric, height: height) - } - - private(set) var inputViewModel: AmountInputViewModelProtocol? - - var completed: Bool { - if let inputViewModel = inputViewModel { - return inputViewModel.isValid - } else { - return false - } - } - - var hasValidNumber: Bool { - inputViewModel?.decimalAmount != nil - } - - override public init(frame: CGRect) { - super.init(frame: frame) - configure() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: Layout - - override func layoutSubviews() { - super.layoutSubviews() - - contentView?.frame = bounds - - layoutContent() - } - - private func layoutContent() { - let availableWidth = bounds.width - contentInsets.left - contentInsets.right - let symbolSize = symbolHubMultiValueView.intrinsicContentSize - if let iconView = lazyIconView { - iconView.frame = CGRect( - x: bounds.minX + contentInsets.left, - y: bounds.midY - iconRadius, - width: 2.0 * iconRadius, - height: 2.0 * iconRadius - ) - - symbolHubMultiValueView.frame = CGRect( - x: iconView.frame.maxX + horizontalSpacing, - y: bounds.midY - symbolSize.height / 2.0, - width: min(availableWidth, symbolSize.width), - height: symbolSize.height - ) - } else { - symbolHubMultiValueView.frame = CGRect( - x: contentInsets.left, - y: bounds.midY - symbolSize.height / 2.0, - width: min(availableWidth, symbolSize.width), - height: symbolSize.height - ) - } - - let estimatedFieldWidth = bounds.maxX - contentInsets.right - - symbolHubMultiValueView.frame.maxX - horizontalSpacing - let fieldWidth = max(estimatedFieldWidth, 0.0) - - let hasPriceLabel = !priceLabel.text.isNilOrEmpty - let fieldHeight = textField.intrinsicContentSize.height - - let textFieldY: CGFloat - - if hasPriceLabel { - let fieldBaselineOffset = 4.0 - textFieldY = bounds.midY - fieldHeight + fieldBaselineOffset - } else { - textFieldY = bounds.midY - fieldHeight / 2.0 - } - - textField.frame = CGRect( - x: bounds.maxX - contentInsets.right - fieldWidth, - y: textFieldY, - width: fieldWidth, - height: fieldHeight - ) - - priceLabel.frame = CGRect( - x: bounds.maxX - contentInsets.right - fieldWidth, - y: textField.frame.maxY, - width: fieldWidth, - height: priceLabel.intrinsicContentSize.height - ) - } - - // MARK: Configure - - private func configure() { - backgroundColor = UIColor.clear - - configureBackgroundViewIfNeeded() - configureContentViewIfNeeded() - configureLocalHandlers() - configureTextFieldHandlers() - } - - private func configureBackgroundViewIfNeeded() { - if backgroundView == nil { - let roundedView = RoundedView() - roundedView.apply(style: .strokeOnEditing) - roundedView.isUserInteractionEnabled = false - backgroundView = roundedView - } - } - - private func configureLocalHandlers() { - addTarget(self, action: #selector(actionTouchUpInside), for: .touchUpInside) - } - - private func configureTextFieldHandlers() { - textField.delegate = self - - textField.addTarget( - self, - action: #selector(actionEditingDidBeginEnd), - for: .editingDidBegin - ) - - textField.addTarget( - self, - action: #selector(actionEditingDidBeginEnd), - for: .editingDidEnd - ) - } - - private func configureContentViewIfNeeded() { - if contentView == nil { - let contentView = UIView() - contentView.backgroundColor = .clear - contentView.isUserInteractionEnabled = false - self.contentView = contentView - } - - contentView?.addSubview(symbolHubMultiValueView) - contentView?.addSubview(priceLabel) - addSubview(textField) - - contentInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 16) - } - - private func lazyIconViewOrCreateIfNeeded() -> AssetIconView { - if let iconView = lazyIconView { - return iconView - } - - let size = 2 * iconRadius - let initFrame = CGRect(origin: .zero, size: .init(width: size, height: size)) - let imageView = AssetIconView(frame: initFrame) - imageView.contentInsets = iconViewContentInsets - imageView.backgroundView.cornerRadius = iconRadius - contentView?.addSubview(imageView) - - lazyIconView = imageView - - if superview != nil { - setNeedsLayout() - } - - return imageView - } - - // MARK: Action - - @objc private func actionEditingDidBeginEnd() { - roundedBackgroundView?.strokeWidth = textField.isFirstResponder ? 0.5 : 0.0 - } - - @objc private func actionTouchUpInside() { - if !textField.isHidden { - textField.becomeFirstResponder() - } - } -} - -extension SwapAmountInputView: UITextFieldDelegate { - func textField( - _: UITextField, - shouldChangeCharactersIn range: NSRange, - replacementString string: String - ) -> Bool { - inputViewModel?.didReceiveReplacement(string, for: range) ?? false - } -} - -extension SwapAmountInputView: AmountInputViewModelObserver { - func amountInputDidChange() { - textField.text = inputViewModel?.displayAmount - - sendActions(for: .editingChanged) - } -} - -extension SwapAmountInputView { - func bind(assetViewModel: SwapsAssetViewModel) { - let width = 2 * iconRadius - iconView.contentInsets.left - iconView.contentInsets.right - let height = 2 * iconRadius - iconView.contentInsets.top - iconView.contentInsets.bottom - iconViewContentInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) - let size = CGSize(width: width, height: height) - iconView.bind(viewModel: assetViewModel.imageViewModel, size: size) - - symbolHubMultiValueView.bind( - symbol: assetViewModel.symbol, - network: assetViewModel.hub.name, - icon: assetViewModel.hub.icon - ) - setNeedsLayout() - } - - func bind(emptyViewModel: EmptySwapsAssetViewModel) { - let size = CGSize(width: 2 * iconRadius, height: 2 * iconRadius) - iconView.bind(viewModel: emptyViewModel.imageViewModel, size: size) - iconViewContentInsets = .zero - symbolHubMultiValueView.bind( - symbol: emptyViewModel.title, - network: emptyViewModel.subtitle, - icon: nil - ) - textField.isHidden = true - setNeedsLayout() - } - - func bind(inputViewModel: AmountInputViewModelProtocol) { - textField.isHidden = false - self.inputViewModel?.observable.remove(observer: self) - inputViewModel.observable.add(observer: self) - - self.inputViewModel = inputViewModel - textField.text = inputViewModel.displayAmount - } - - func bind(priceViewModel: String?) { - priceLabel.text = priceViewModel - - setNeedsLayout() - } -} - -struct SwapsAssetViewModel { - let symbol: String - let imageViewModel: ImageViewModelProtocol? - let hub: NetworkViewModel -} - -struct EmptySwapsAssetViewModel { - let imageViewModel: ImageViewModelProtocol? - let title: String - let subtitle: String -} diff --git a/novawallet/Modules/SwapSetup/SwapSetupViewController.swift b/novawallet/Modules/SwapSetup/SwapSetupViewController.swift index 2ed3760ce9..a5ff3308bc 100644 --- a/novawallet/Modules/SwapSetup/SwapSetupViewController.swift +++ b/novawallet/Modules/SwapSetup/SwapSetupViewController.swift @@ -27,16 +27,16 @@ final class SwapSetupViewController: UIViewController, ViewHolder { } private func setupHandlers() { - rootView.payAmountInputView.symbolHubMultiValueView.addGestureRecognizer( - UITapGestureRecognizer( - target: self, - action: #selector(selectPayTokenAction) - )) - rootView.receiveAmountInputView.symbolHubMultiValueView.addGestureRecognizer( - UITapGestureRecognizer( - target: self, - action: #selector(selectReceiveTokenAction) - )) + rootView.payAmountInputView.swapAssetControl.addTarget( + self, + action: #selector(selectPayTokenAction), + for: .touchUpInside + ) + rootView.receiveAmountInputView.swapAssetControl.addTarget( + self, + action: #selector(selectReceiveTokenAction), + for: .touchUpInside + ) } private func emptyPayViewModel() -> EmptySwapsAssetViewModel { From 553525d3da8645b98f21a5c4dba5fd0bbb937231 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 3 Oct 2023 17:56:53 +0500 Subject: [PATCH 012/204] bump v15 json --- novawallet/Common/Configs/ApplicationConfigs.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novawallet/Common/Configs/ApplicationConfigs.swift b/novawallet/Common/Configs/ApplicationConfigs.swift index f32f97a355..3e3b6bdb71 100644 --- a/novawallet/Common/Configs/ApplicationConfigs.swift +++ b/novawallet/Common/Configs/ApplicationConfigs.swift @@ -129,9 +129,9 @@ extension ApplicationConfig: ApplicationConfigProtocol { var chainListURL: URL { #if F_RELEASE - URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/chains/v14/chains.json")! + URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/chains/v15/chains.json")! #else - URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/chains/v14/chains_dev.json")! + URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/chains/v15/chains_dev.json")! #endif } From dd7fbbbe76638ebf2f2b6a4c556ab02dfab607df Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 4 Oct 2023 08:20:14 +0300 Subject: [PATCH 013/204] localization, cleanup --- novawallet.xcodeproj/project.pbxproj | 32 +++++-- .../Model/MockViewModelFactory.swift | 88 +++++++++++++++++++ .../SwapSetup/Model/SwapsAssetViewModel.swift | 16 ++++ .../SwapSetup/SwapSetupPresenter.swift | 65 ++++++-------- .../SwapSetup/SwapSetupProtocols.swift | 6 +- .../SwapSetup/SwapSetupViewController.swift | 76 ++++++++++------ .../SwapSetup/SwapSetupViewFactory.swift | 5 +- .../SwapSetup/View}/SwapAmountInput.swift | 2 +- .../SwapSetup/View}/SwapAmountInputView.swift | 66 +++++++------- .../SwapSetup/View}/SwapAssetControl.swift | 45 ++++------ .../SwapSetup/View/SwapAssetView.swift} | 32 ++++--- .../{ => View}/SwapSetupViewLayout.swift | 0 novawallet/en.lproj/Localizable.strings | 10 +++ novawallet/ru.lproj/Localizable.strings | 10 +++ 14 files changed, 300 insertions(+), 153 deletions(-) create mode 100644 novawallet/Modules/SwapSetup/Model/MockViewModelFactory.swift create mode 100644 novawallet/Modules/SwapSetup/Model/SwapsAssetViewModel.swift rename novawallet/{Common/View/AmountInputView/Swap => Modules/SwapSetup/View}/SwapAmountInput.swift (98%) rename novawallet/{Common/View/AmountInputView/Swap => Modules/SwapSetup/View}/SwapAmountInputView.swift (67%) rename novawallet/{Common/View/AmountInputView/Swap => Modules/SwapSetup/View}/SwapAssetControl.swift (75%) rename novawallet/{Common/View/AmountInputView/Swap/SwapSymbolView.swift => Modules/SwapSetup/View/SwapAssetView.swift} (60%) rename novawallet/Modules/SwapSetup/{ => View}/SwapSetupViewLayout.swift (100%) diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 738a2d47e7..5387f605e7 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -670,7 +670,7 @@ 7731E9C42A14DA3F0085B5FF /* BorderedActionControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7731E9C32A14DA3F0085B5FF /* BorderedActionControlView.swift */; }; 7738FB6A2A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7738FB692A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift */; }; 774091F92ACB1F4B00172516 /* SwapAmountInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774091F82ACB1F4B00172516 /* SwapAmountInputView.swift */; }; - 774091FC2ACC053000172516 /* SwapSymbolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774091FB2ACC053000172516 /* SwapSymbolView.swift */; }; + 774091FC2ACC053000172516 /* SwapAssetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774091FB2ACC053000172516 /* SwapAssetView.swift */; }; 774091FE2ACC054B00172516 /* SwapAssetControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774091FD2ACC054B00172516 /* SwapAssetControl.swift */; }; 774092002ACC1BE400172516 /* SwapAmountInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774091FF2ACC1BE400172516 /* SwapAmountInput.swift */; }; 774A481129F8BFB70094635B /* OperationAuthPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774A481029F8BFB70094635B /* OperationAuthPresentable.swift */; }; @@ -740,6 +740,8 @@ 77AB55592AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */; }; 77AB555B2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */; }; 77AB555D2AA24BA90058814E /* OperationDetailsPoolRewardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */; }; + 77C9BCBC2ACD1AF500022EA2 /* SwapsAssetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBB2ACD1AF500022EA2 /* SwapsAssetViewModel.swift */; }; + 77C9BCBE2ACD286100022EA2 /* MockViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBD2ACD286100022EA2 /* MockViewModelFactory.swift */; }; 77CB33CE2A38780700B6709A /* structures_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77CB33CD2A38780700B6709A /* structures_output.json */; }; 77CB33D22A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CB33D12A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift */; }; 77CB33D72A3998FD00B6709A /* Array+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CB33D62A3998FC00B6709A /* Array+Sort.swift */; }; @@ -4640,7 +4642,7 @@ 7731E9C32A14DA3F0085B5FF /* BorderedActionControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedActionControlView.swift; sourceTree = ""; }; 7738FB692A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingRelaychainInteractor.swift; sourceTree = ""; }; 774091F82ACB1F4B00172516 /* SwapAmountInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAmountInputView.swift; sourceTree = ""; }; - 774091FB2ACC053000172516 /* SwapSymbolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapSymbolView.swift; sourceTree = ""; }; + 774091FB2ACC053000172516 /* SwapAssetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetView.swift; sourceTree = ""; }; 774091FD2ACC054B00172516 /* SwapAssetControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetControl.swift; sourceTree = ""; }; 774091FF2ACC1BE400172516 /* SwapAmountInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAmountInput.swift; sourceTree = ""; }; 774A481029F8BFB70094635B /* OperationAuthPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationAuthPresentable.swift; sourceTree = ""; }; @@ -4710,6 +4712,8 @@ 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashModel.swift; sourceTree = ""; }; 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashViewModel.swift; sourceTree = ""; }; 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsPoolRewardView.swift; sourceTree = ""; }; + 77C9BCBB2ACD1AF500022EA2 /* SwapsAssetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapsAssetViewModel.swift; sourceTree = ""; }; + 77C9BCBD2ACD286100022EA2 /* MockViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockViewModelFactory.swift; sourceTree = ""; }; 77CB33CD2A38780700B6709A /* structures_output.json */ = {isa = PBXFileReference; explicitFileType = text.json; fileEncoding = 4; path = structures_output.json; sourceTree = ""; usesTabs = 0; wrapsLines = 0; }; 77CB33D12A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Web3NameIntegrityVerifierWithCanonicalizationData.swift; sourceTree = ""; }; 77CB33D62A3998FC00B6709A /* Array+Sort.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Sort.swift"; sourceTree = ""; }; @@ -8813,12 +8817,13 @@ 29BD7DA0076BA8BC3411221A /* SwapSetup */ = { isa = PBXGroup; children = ( + 77C9BCBA2ACD1AE800022EA2 /* Model */, + 774091FA2ACC052400172516 /* View */, 53775773F2060B4B7F6D62DA /* SwapSetupProtocols.swift */, C7F910E56C2CC7AA5224BD21 /* SwapSetupWireframe.swift */, BB494F0B16C9588325CF0D84 /* SwapSetupPresenter.swift */, C585109AC3A2580AB1253C31 /* SwapSetupInteractor.swift */, BBEB2BC52266AF493D310834 /* SwapSetupViewController.swift */, - CDD43373A9159E0A592077BB /* SwapSetupViewLayout.swift */, 6A6BCE8F8DB7576E1E5B5974 /* SwapSetupViewFactory.swift */, ); path = SwapSetup; @@ -9404,15 +9409,16 @@ path = RelaychainStaking; sourceTree = ""; }; - 774091FA2ACC052400172516 /* Swap */ = { + 774091FA2ACC052400172516 /* View */ = { isa = PBXGroup; children = ( 774091F82ACB1F4B00172516 /* SwapAmountInputView.swift */, - 774091FB2ACC053000172516 /* SwapSymbolView.swift */, + 774091FB2ACC053000172516 /* SwapAssetView.swift */, 774091FD2ACC054B00172516 /* SwapAssetControl.swift */, 774091FF2ACC1BE400172516 /* SwapAmountInput.swift */, + CDD43373A9159E0A592077BB /* SwapSetupViewLayout.swift */, ); - path = Swap; + path = View; sourceTree = ""; }; 775692822A24CA5100220756 /* AssetOperation */ = { @@ -9568,6 +9574,15 @@ path = canonicalization; sourceTree = ""; }; + 77C9BCBA2ACD1AE800022EA2 /* Model */ = { + isa = PBXGroup; + children = ( + 77C9BCBB2ACD1AF500022EA2 /* SwapsAssetViewModel.swift */, + 77C9BCBD2ACD286100022EA2 /* MockViewModelFactory.swift */, + ); + path = Model; + sourceTree = ""; + }; 77CB33D32A38894600B6709A /* Integrity */ = { isa = PBXGroup; children = ( @@ -9861,7 +9876,6 @@ 8401620F25E144DA0087A5F3 /* AmountInputView */ = { isa = PBXGroup; children = ( - 774091FA2ACC052400172516 /* Swap */, 840DCBF025DFEE4800D45C6A /* AmountInputView.swift */, 840DCBF525E0059D00D45C6A /* AmountInputView+Inspectable.swift */, 8401620A25E144D50087A5F3 /* AmountInputAccessoryView.swift */, @@ -20284,7 +20298,7 @@ 840D92A1278D8D6F0007B979 /* DAppBrowserStateError.swift in Sources */, 84FC190B29B7DB9F00BCCAA5 /* ExtrinsicServiceTypes.swift in Sources */, 849707A128F3E0AC00DD0A02 /* ReferendumVoterLocal.swift in Sources */, - 774091FC2ACC053000172516 /* SwapSymbolView.swift in Sources */, + 774091FC2ACC053000172516 /* SwapAssetView.swift in Sources */, 84754C882510BAFE00854599 /* ModalAlertFactory.swift in Sources */, 8430AACC2602249B005B1066 /* InitialStakingState.swift in Sources */, 0C56B4FB2A4B0C320030F9C9 /* AssetListBaseBuilder.swift in Sources */, @@ -20981,6 +20995,7 @@ F4D0546B2729949100210294 /* MoonbeamMakeSignatureResponse.swift in Sources */, D9046DBA27451ED700C29F2E /* ParallelContributionSource.swift in Sources */, 0CE629DE2AA9B6BF00E250BD /* RewardDestinationViewModelFactory.swift in Sources */, + 77C9BCBE2ACD286100022EA2 /* MockViewModelFactory.swift in Sources */, 844DB624262D9C710025A8F0 /* ErasRewardDistribution.swift in Sources */, 84FAB0632542C8D600319F74 /* ContactItem.swift in Sources */, 06590486EED4050BADDD32C5 /* AccountManagementPresenter.swift in Sources */, @@ -21423,6 +21438,7 @@ 845B821926EF808D00D25C72 /* MetaAccountMapper.swift in Sources */, 19A29027666EB5388CBFAD61 /* StakingRewardDetailsInteractor.swift in Sources */, 846AC7EF2638D9200075F7DA /* YourValidatorTableCell.swift in Sources */, + 77C9BCBC2ACD1AF500022EA2 /* SwapsAssetViewModel.swift in Sources */, C937154FA9021AECD72A871B /* StakingRewardDetailsViewController.swift in Sources */, 84770F27291F7CD400852A33 /* ReferendumDetailsInitData.swift in Sources */, 84B8AA8529F910AD00347A37 /* WalletConnectStateError.swift in Sources */, diff --git a/novawallet/Modules/SwapSetup/Model/MockViewModelFactory.swift b/novawallet/Modules/SwapSetup/Model/MockViewModelFactory.swift new file mode 100644 index 0000000000..a97fd2f2f2 --- /dev/null +++ b/novawallet/Modules/SwapSetup/Model/MockViewModelFactory.swift @@ -0,0 +1,88 @@ +import SoraFoundation + +final class MockViewModelFactory { + func buttonState() -> ButtonState { + .init( + title: .init { + R.string.localizable.swapsSetupAssetActionSelectReceive(preferredLanguages: $0.rLanguages) + }, + enabled: false + ) + } + + func payTitleModel(locale: Locale) -> TitleHorizontalMultiValueView.Model { + TitleHorizontalMultiValueView.Model( + title: + R.string.localizable.swapsSetupAssetSelectPayTitle(preferredLanguages: locale.rLanguages), + subtitle: + R.string.localizable.swapsSetupAssetMax( + preferredLanguages: locale.rLanguages + ), + value: "100 DOT" + ) + } + + func payModel() -> SwapAssetInputViewModel { + let dotImage = RemoteImageViewModel(url: URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/chains/white/Polkadot.svg")!) + let hubImage = RemoteImageViewModel(url: URL(string: "https://parachains.info/images/parachains/1688559044_assethub.svg")!) + return .asset(SwapsAssetViewModel( + symbol: "DOT", + imageViewModel: dotImage, + hub: .init( + name: "Polkadot Asset Hub", + icon: hubImage + ) + )) + } + + func payPriceModel() -> String? { + "$0" + } + + func receiveTitleModel(locale: Locale) -> TitleHorizontalMultiValueView.Model { + TitleHorizontalMultiValueView.Model( + title: + R.string.localizable.swapsSetupAssetSelectReceiveTitle(preferredLanguages: locale.rLanguages), + subtitle: "", + value: "" + ) + } + + func receiveModel(locale: Locale) -> SwapAssetInputViewModel { + .empty(emptyReceiveViewModel(locale: locale)) + } + + func payAmount( + locale: Locale, + balanceViewModelFactory: BalanceViewModelFactoryFacadeProtocol + ) -> AmountInputViewModelProtocol { + let targetAssetInfo = AssetBalanceDisplayInfo( + displayPrecision: 2, + assetPrecision: 10, + symbol: "DOT", + symbolValueSeparator: "", + symbolPosition: .suffix, + icon: nil + ) + return balanceViewModelFactory.createBalanceInputViewModel( + targetAssetInfo: targetAssetInfo, + amount: nil + ).value(for: locale) + } + + func emptyPayViewModel(locale: Locale) -> EmptySwapsAssetViewModel { + EmptySwapsAssetViewModel( + imageViewModel: StaticImageViewModel(image: R.image.iconAddSwapAmount()!), + title: R.string.localizable.swapsSetupAssetPayTitle(preferredLanguages: locale.rLanguages), + subtitle: R.string.localizable.swapsSetupAssetSelectSubtitle(preferredLanguages: locale.rLanguages) + ) + } + + func emptyReceiveViewModel(locale: Locale) -> EmptySwapsAssetViewModel { + EmptySwapsAssetViewModel( + imageViewModel: StaticImageViewModel(image: R.image.iconAddSwapAmount()!), + title: R.string.localizable.swapsSetupAssetReceiveTitle(preferredLanguages: locale.rLanguages), + subtitle: R.string.localizable.swapsSetupAssetSelectSubtitle(preferredLanguages: locale.rLanguages) + ) + } +} diff --git a/novawallet/Modules/SwapSetup/Model/SwapsAssetViewModel.swift b/novawallet/Modules/SwapSetup/Model/SwapsAssetViewModel.swift new file mode 100644 index 0000000000..5ed2476be8 --- /dev/null +++ b/novawallet/Modules/SwapSetup/Model/SwapsAssetViewModel.swift @@ -0,0 +1,16 @@ +struct SwapsAssetViewModel { + let symbol: String + let imageViewModel: ImageViewModelProtocol? + let hub: NetworkViewModel +} + +struct EmptySwapsAssetViewModel { + let imageViewModel: ImageViewModelProtocol? + let title: String + let subtitle: String +} + +enum SwapAssetInputViewModel { + case asset(SwapsAssetViewModel) + case empty(EmptySwapsAssetViewModel) +} diff --git a/novawallet/Modules/SwapSetup/SwapSetupPresenter.swift b/novawallet/Modules/SwapSetup/SwapSetupPresenter.swift index dd55bd4ae4..887cc55dd5 100644 --- a/novawallet/Modules/SwapSetup/SwapSetupPresenter.swift +++ b/novawallet/Modules/SwapSetup/SwapSetupPresenter.swift @@ -11,60 +11,45 @@ final class SwapSetupPresenter { interactor: SwapSetupInteractorInputProtocol, wireframe: SwapSetupWireframeProtocol, balanceViewModelFactory: BalanceViewModelFactoryFacadeProtocol, - localizationManager _: LocalizationManagerProtocol + localizationManager: LocalizationManagerProtocol ) { self.interactor = interactor self.wireframe = wireframe self.balanceViewModelFactory = balanceViewModelFactory + self.localizationManager = localizationManager } } extension SwapSetupPresenter: SwapSetupPresenterProtocol { func setup() { - view?.didReceiveButtonState(title: "Enter amount", enabled: false) - view?.didReceiveTitle(payViewModel: .init(title: "You pay", subtitle: "Max:", value: "100 USDT")) - view?.didReceiveInputChainAsset(payViewModel: dotModel()) - view?.didReceiveAmount(payInputViewModel: amount()) - view?.didReceiveAmountInputPrice(payViewModel: "$0") - view?.didReceiveTitle(receiveViewModel: .init(title: "You receive", subtitle: "", value: "")) - view?.didReceiveInputChainAsset(receiveViewModel: nil) - } - - func dotModel() -> SwapsAssetViewModel { - let dotImage = RemoteImageViewModel(url: URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/chains/white/Polkadot.svg")!) - let hubImage = RemoteImageViewModel(url: URL(string: "https://parachains.info/images/parachains/1688559044_assethub.svg")!) - return SwapsAssetViewModel( - symbol: "DOT", - imageViewModel: dotImage, - hub: .init( - name: "Polkadot Asset Hub", - icon: hubImage - ) + let mock = MockViewModelFactory() + let buttonState = mock.buttonState() + view?.didReceiveButtonState( + title: buttonState.title.value(for: selectedLocale), + enabled: buttonState.enabled ) + view?.didReceiveTitle(payViewModel: mock.payTitleModel(locale: selectedLocale)) + view?.didReceiveInputChainAsset(payViewModel: mock.payModel()) + view?.didReceiveAmount(payInputViewModel: mock.payAmount( + locale: selectedLocale, + balanceViewModelFactory: balanceViewModelFactory + )) + view?.didReceiveAmountInputPrice(payViewModel: mock.payPriceModel()) + view?.didReceiveTitle(receiveViewModel: mock.receiveTitleModel(locale: selectedLocale)) + view?.didReceiveInputChainAsset(receiveViewModel: mock.receiveModel(locale: selectedLocale)) } - func amount() -> AmountInputViewModelProtocol { - let targetAssetInfo = AssetBalanceDisplayInfo( - displayPrecision: 2, - assetPrecision: 10, - symbol: "DOT", - symbolValueSeparator: "", - symbolPosition: .suffix, - icon: nil - ) - return balanceViewModelFactory.createBalanceInputViewModel( - targetAssetInfo: targetAssetInfo, - amount: 0 - ).value(for: selectedLocale) - } + // TODO: navigate to select token screen + func selectPayToken() {} - func selectPayToken() { - print("SELECT PAY TOKEN") - } + // TODO: navigate to select token screen + func selectReceiveToken() {} - func selectReceiveToken() { - print("SELECT RECEIVE TOKEN") - } + // TODO: implement + func swap() {} + + // TODO: navigate to confirm screen + func proceed() {} } extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol {} diff --git a/novawallet/Modules/SwapSetup/SwapSetupProtocols.swift b/novawallet/Modules/SwapSetup/SwapSetupProtocols.swift index c6da5c9e5a..46c4b6656c 100644 --- a/novawallet/Modules/SwapSetup/SwapSetupProtocols.swift +++ b/novawallet/Modules/SwapSetup/SwapSetupProtocols.swift @@ -1,10 +1,10 @@ protocol SwapSetupViewProtocol: ControllerBackedProtocol { func didReceiveButtonState(title: String, enabled: Bool) - func didReceiveInputChainAsset(payViewModel viewModel: SwapsAssetViewModel?) + func didReceiveInputChainAsset(payViewModel viewModel: SwapAssetInputViewModel) func didReceiveAmount(payInputViewModel inputViewModel: AmountInputViewModelProtocol) func didReceiveAmountInputPrice(payViewModel: String?) func didReceiveTitle(payViewModel viewModel: TitleHorizontalMultiValueView.Model) - func didReceiveInputChainAsset(receiveViewModel viewModel: SwapsAssetViewModel?) + func didReceiveInputChainAsset(receiveViewModel viewModel: SwapAssetInputViewModel) func didReceiveAmount(receiveInputViewModel inputViewModel: AmountInputViewModelProtocol) func didReceiveAmountInputPrice(receiveViewModel: String?) func didReceiveTitle(receiveViewModel viewModel: TitleHorizontalMultiValueView.Model) @@ -14,6 +14,8 @@ protocol SwapSetupPresenterProtocol: AnyObject { func setup() func selectPayToken() func selectReceiveToken() + func proceed() + func swap() } protocol SwapSetupInteractorInputProtocol: AnyObject {} diff --git a/novawallet/Modules/SwapSetup/SwapSetupViewController.swift b/novawallet/Modules/SwapSetup/SwapSetupViewController.swift index a5ff3308bc..b30870e8de 100644 --- a/novawallet/Modules/SwapSetup/SwapSetupViewController.swift +++ b/novawallet/Modules/SwapSetup/SwapSetupViewController.swift @@ -1,13 +1,18 @@ import UIKit +import SoraFoundation final class SwapSetupViewController: UIViewController, ViewHolder { typealias RootViewType = SwapSetupViewLayout let presenter: SwapSetupPresenterProtocol - init(presenter: SwapSetupPresenterProtocol) { + init( + presenter: SwapSetupPresenterProtocol, + localizationManager: LocalizationManager + ) { self.presenter = presenter super.init(nibName: nil, bundle: nil) + self.localizationManager = localizationManager } @available(*, unavailable) @@ -23,45 +28,54 @@ final class SwapSetupViewController: UIViewController, ViewHolder { super.viewDidLoad() setupHandlers() + setupLocalization() presenter.setup() } private func setupHandlers() { - rootView.payAmountInputView.swapAssetControl.addTarget( + rootView.payAmountInputView.assetControl.addTarget( self, action: #selector(selectPayTokenAction), for: .touchUpInside ) - rootView.receiveAmountInputView.swapAssetControl.addTarget( + rootView.receiveAmountInputView.assetControl.addTarget( self, action: #selector(selectReceiveTokenAction), for: .touchUpInside ) - } - - private func emptyPayViewModel() -> EmptySwapsAssetViewModel { - EmptySwapsAssetViewModel( - imageViewModel: StaticImageViewModel(image: R.image.iconAddSwapAmount()!), - title: "Pay", - subtitle: "Select a token" + rootView.actionButton.addTarget( + self, + action: #selector(continueAction), + for: .touchUpInside + ) + rootView.switchButton.addTarget( + self, + action: #selector(swapAction), + for: .touchUpInside ) } - private func emptyReceiveViewModel() -> EmptySwapsAssetViewModel { - EmptySwapsAssetViewModel( - imageViewModel: StaticImageViewModel(image: R.image.iconAddSwapAmount()!), - title: "Receive", - subtitle: "Select a token" - ) + private func setupLocalization() { + title = R.string.localizable.walletAssetsSwap(preferredLanguages: selectedLocale.rLanguages) } @objc private func selectPayTokenAction() { + rootView.receiveAmountInputView.endEditing(true) presenter.selectPayToken() } @objc private func selectReceiveTokenAction() { + rootView.payAmountInputView.endEditing(true) presenter.selectReceiveToken() } + + @objc private func continueAction() { + presenter.proceed() + } + + @objc private func swapAction() { + presenter.swap() + } } extension SwapSetupViewController: SwapSetupViewProtocol { @@ -73,11 +87,12 @@ extension SwapSetupViewController: SwapSetupViewProtocol { rootView.payAmountView.bind(model: viewModel) } - func didReceiveInputChainAsset(payViewModel viewModel: SwapsAssetViewModel?) { - if let viewModel = viewModel { - rootView.payAmountInputView.bind(assetViewModel: viewModel) - } else { - rootView.payAmountInputView.bind(emptyViewModel: emptyPayViewModel()) + func didReceiveInputChainAsset(payViewModel viewModel: SwapAssetInputViewModel) { + switch viewModel { + case let .asset(assetViewModel): + rootView.payAmountInputView.bind(assetViewModel: assetViewModel) + case let .empty(emptySwapsAssetViewModel): + rootView.payAmountInputView.bind(emptyViewModel: emptySwapsAssetViewModel) } } @@ -93,11 +108,12 @@ extension SwapSetupViewController: SwapSetupViewProtocol { rootView.receiveAmountView.bind(model: viewModel) } - func didReceiveInputChainAsset(receiveViewModel viewModel: SwapsAssetViewModel?) { - if let viewModel = viewModel { - rootView.receiveAmountInputView.bind(assetViewModel: viewModel) - } else { - rootView.receiveAmountInputView.bind(emptyViewModel: emptyReceiveViewModel()) + func didReceiveInputChainAsset(receiveViewModel viewModel: SwapAssetInputViewModel) { + switch viewModel { + case let .asset(swapsAssetViewModel): + rootView.receiveAmountInputView.bind(assetViewModel: swapsAssetViewModel) + case let .empty(emptySwapsAssetViewModel): + rootView.receiveAmountInputView.bind(emptyViewModel: emptySwapsAssetViewModel) } } @@ -109,3 +125,11 @@ extension SwapSetupViewController: SwapSetupViewProtocol { rootView.receiveAmountInputView.bind(priceViewModel: viewModel) } } + +extension SwapSetupViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} diff --git a/novawallet/Modules/SwapSetup/SwapSetupViewFactory.swift b/novawallet/Modules/SwapSetup/SwapSetupViewFactory.swift index 6cad9401fe..0639b7782e 100644 --- a/novawallet/Modules/SwapSetup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/SwapSetup/SwapSetupViewFactory.swift @@ -21,7 +21,10 @@ struct SwapSetupViewFactory { localizationManager: LocalizationManager.shared ) - let view = SwapSetupViewController(presenter: presenter) + let view = SwapSetupViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) presenter.view = view interactor.presenter = presenter diff --git a/novawallet/Common/View/AmountInputView/Swap/SwapAmountInput.swift b/novawallet/Modules/SwapSetup/View/SwapAmountInput.swift similarity index 98% rename from novawallet/Common/View/AmountInputView/Swap/SwapAmountInput.swift rename to novawallet/Modules/SwapSetup/View/SwapAmountInput.swift index 6668a2ffeb..ad1d8a99b4 100644 --- a/novawallet/Common/View/AmountInputView/Swap/SwapAmountInput.swift +++ b/novawallet/Modules/SwapSetup/View/SwapAmountInput.swift @@ -108,7 +108,7 @@ final class SwapAmountInput: BackgroundedContentControl { } contentView?.addSubview(priceLabel) - addSubview(textField) + contentView?.addSubview(textField) } @objc private func actionTouchUpInside() { diff --git a/novawallet/Common/View/AmountInputView/Swap/SwapAmountInputView.swift b/novawallet/Modules/SwapSetup/View/SwapAmountInputView.swift similarity index 67% rename from novawallet/Common/View/AmountInputView/Swap/SwapAmountInputView.swift rename to novawallet/Modules/SwapSetup/View/SwapAmountInputView.swift index a9bbce8792..1ea0ebe2f9 100644 --- a/novawallet/Common/View/AmountInputView/Swap/SwapAmountInputView.swift +++ b/novawallet/Modules/SwapSetup/View/SwapAmountInputView.swift @@ -1,7 +1,7 @@ import SoraUI final class SwapAmountInputView: RoundedView { - let swapAssetControl = SwapAssetControl() + let assetControl = SwapAssetControl() let textInputView = SwapAmountInput() var contentInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 16) { @@ -27,7 +27,7 @@ final class SwapAmountInputView: RoundedView { } override var intrinsicContentSize: CGSize { - let leftContentHeight = swapAssetControl.intrinsicContentSize.height + let leftContentHeight = assetControl.intrinsicContentSize.height let rightContentHeight = textInputView.intrinsicContentSize.height let contentHeight = max(leftContentHeight, rightContentHeight) @@ -42,35 +42,37 @@ final class SwapAmountInputView: RoundedView { override func layoutSubviews() { super.layoutSubviews() - layoutContent() + assetControl.frame = swapAssetControlFrame(bounds: bounds) + textInputView.frame = inputViewFrame( + bounds: bounds, + assetControlFrame: assetControl.frame + ) } - private func layoutContent() { + private func swapAssetControlFrame(bounds: CGRect) -> CGRect { let availableWidth = bounds.width - contentInsets.left - contentInsets.right - let swapAssetControlSize = swapAssetControl.intrinsicContentSize - - guard !textInputView.isHidden else { - swapAssetControl.frame = CGRect( - x: contentInsets.left, - y: bounds.midY - swapAssetControlSize.height / 2.0, - width: availableWidth, - height: swapAssetControlSize.height - ) - return - } - swapAssetControl.frame = CGRect( + let swapAssetControlSize = assetControl.intrinsicContentSize + + let width = textInputView.isHidden ? availableWidth : + min(min(availableWidth, swapAssetControlSize.width), 0.7 * availableWidth) + + return CGRect( x: contentInsets.left, y: bounds.midY - swapAssetControlSize.height / 2.0, - width: min(min(availableWidth, swapAssetControlSize.width), 0.7 * availableWidth), + width: width, height: swapAssetControlSize.height ) + } - let estimatedInputViewWidth = bounds.maxX - contentInsets.right - - swapAssetControl.frame.maxX - horizontalSpacing + private func inputViewFrame( + bounds: CGRect, + assetControlFrame: CGRect + ) -> CGRect { + let estimatedInputViewWidth = bounds.maxX - contentInsets.right - assetControlFrame.maxX - horizontalSpacing let inputWidth = max(estimatedInputViewWidth, 0) let inputSize = textInputView.intrinsicContentSize - textInputView.frame = CGRect( + return CGRect( x: bounds.maxX - contentInsets.right - inputWidth, y: bounds.midY - inputSize.height / 2.0, width: inputWidth, @@ -84,21 +86,17 @@ final class SwapAmountInputView: RoundedView { super.configure() backgroundColor = UIColor.clear + apply(style: .strokeOnEditing) - configureBackgroundViewIfNeeded() - configureContentViewIfNeeded() + configureContent() configureInputViewHandlers() } - private func configureBackgroundViewIfNeeded() { - apply(style: .strokeOnEditing) - } - - private func configureContentViewIfNeeded() { - addSubview(swapAssetControl) + private func configureContent() { + addSubview(assetControl) addSubview(textInputView) - swapAssetControl.contentInsets = .zero + assetControl.contentInsets = .zero textInputView.contentInsets = .zero } @@ -116,24 +114,20 @@ final class SwapAmountInputView: RoundedView { ) } - // MARK: Action - @objc private func actionEditingDidBeginEnd() { - strokeWidth = textInputView.isFirstResponder ? 0.5 : 0.0 + strokeWidth = textInputView.textField.isFirstResponder ? 0.5 : 0.0 } } extension SwapAmountInputView { func bind(assetViewModel: SwapsAssetViewModel) { - swapAssetControl.bind(assetViewModel: assetViewModel) - swapAssetControl.invalidateIntrinsicContentSize() + assetControl.bind(assetViewModel: assetViewModel) setNeedsLayout() } func bind(emptyViewModel: EmptySwapsAssetViewModel) { - swapAssetControl.bind(emptyViewModel: emptyViewModel) + assetControl.bind(emptyViewModel: emptyViewModel) textInputView.isHidden = true - swapAssetControl.invalidateIntrinsicContentSize() setNeedsLayout() } diff --git a/novawallet/Common/View/AmountInputView/Swap/SwapAssetControl.swift b/novawallet/Modules/SwapSetup/View/SwapAssetControl.swift similarity index 75% rename from novawallet/Common/View/AmountInputView/Swap/SwapAssetControl.swift rename to novawallet/Modules/SwapSetup/View/SwapAssetControl.swift index 7da05373f5..a19e832559 100644 --- a/novawallet/Common/View/AmountInputView/Swap/SwapAssetControl.swift +++ b/novawallet/Modules/SwapSetup/View/SwapAssetControl.swift @@ -5,9 +5,9 @@ final class SwapAssetControl: BackgroundedContentControl { var iconView: AssetIconView { lazyIconViewOrCreateIfNeeded() } private var lazyIconView: AssetIconView? - let symbolHubMultiValueView = SwapSymbolView() + let assetView = SwapAssetView() - var iconViewContentInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) { + var iconViewContentInsets = UIEdgeInsets.zero { didSet { lazyIconView?.contentInsets = iconViewContentInsets setNeedsLayout() @@ -41,10 +41,11 @@ final class SwapAssetControl: BackgroundedContentControl { override var intrinsicContentSize: CGSize { let contentHeight = max( lazyIconView?.intrinsicContentSize.height ?? 0.0, - symbolHubMultiValueView.intrinsicContentSize.height + assetView.intrinsicContentSize.height ) - let contentWidth = (lazyIconView?.intrinsicContentSize.width ?? 0.0) + horizontalSpacing + symbolHubMultiValueView.intrinsicContentSize.width + let iconWidth = lazyIconView.map { $0.intrinsicContentSize.width + horizontalSpacing } ?? 0 + let contentWidth = iconWidth + assetView.intrinsicContentSize.width let height = contentInsets.top + contentHeight + contentInsets.bottom let width = contentInsets.left + contentWidth + contentInsets.right @@ -62,7 +63,7 @@ final class SwapAssetControl: BackgroundedContentControl { private func layoutContent() { let availableWidth = bounds.width - contentInsets.left - contentInsets.right - let symbolSize = symbolHubMultiValueView.intrinsicContentSize + let assetViewSize = assetView.intrinsicContentSize if let iconView = lazyIconView { iconView.frame = CGRect( @@ -72,18 +73,18 @@ final class SwapAssetControl: BackgroundedContentControl { height: 2.0 * iconRadius ) - symbolHubMultiValueView.frame = CGRect( + assetView.frame = CGRect( x: iconView.frame.maxX + horizontalSpacing, - y: bounds.midY - symbolSize.height / 2.0, - width: min(availableWidth, symbolSize.width), - height: symbolSize.height + y: bounds.midY - assetViewSize.height / 2.0, + width: min(availableWidth, assetViewSize.width), + height: assetViewSize.height ) } else { - symbolHubMultiValueView.frame = CGRect( + assetView.frame = CGRect( x: contentInsets.left, - y: bounds.midY - symbolSize.height / 2.0, - width: min(availableWidth, symbolSize.width), - height: symbolSize.height + y: bounds.midY - assetViewSize.height / 2.0, + width: min(availableWidth, assetViewSize.width), + height: assetViewSize.height ) } } @@ -98,7 +99,7 @@ final class SwapAssetControl: BackgroundedContentControl { self.contentView = contentView } - contentView?.addSubview(symbolHubMultiValueView) + contentView?.addSubview(assetView) } private func lazyIconViewOrCreateIfNeeded() -> AssetIconView { @@ -123,18 +124,6 @@ final class SwapAssetControl: BackgroundedContentControl { } } -struct SwapsAssetViewModel { - let symbol: String - let imageViewModel: ImageViewModelProtocol? - let hub: NetworkViewModel -} - -struct EmptySwapsAssetViewModel { - let imageViewModel: ImageViewModelProtocol? - let title: String - let subtitle: String -} - extension SwapAssetControl { func bind(assetViewModel: SwapsAssetViewModel) { let width = 2 * iconRadius - iconView.contentInsets.left - iconView.contentInsets.right @@ -143,7 +132,7 @@ extension SwapAssetControl { let size = CGSize(width: width, height: height) iconView.bind(viewModel: assetViewModel.imageViewModel, size: size) - symbolHubMultiValueView.bind( + assetView.bind( symbol: assetViewModel.symbol, network: assetViewModel.hub.name, icon: assetViewModel.hub.icon @@ -155,7 +144,7 @@ extension SwapAssetControl { let size = CGSize(width: 2 * iconRadius, height: 2 * iconRadius) iconView.bind(viewModel: emptyViewModel.imageViewModel, size: size) iconViewContentInsets = .zero - symbolHubMultiValueView.bind( + assetView.bind( symbol: emptyViewModel.title, network: emptyViewModel.subtitle, icon: nil diff --git a/novawallet/Common/View/AmountInputView/Swap/SwapSymbolView.swift b/novawallet/Modules/SwapSetup/View/SwapAssetView.swift similarity index 60% rename from novawallet/Common/View/AmountInputView/Swap/SwapSymbolView.swift rename to novawallet/Modules/SwapSetup/View/SwapAssetView.swift index 88966920b4..51a9a7454f 100644 --- a/novawallet/Common/View/AmountInputView/Swap/SwapSymbolView.swift +++ b/novawallet/Modules/SwapSetup/View/SwapAssetView.swift @@ -1,7 +1,9 @@ import UIKit -final class SwapSymbolView: GenericPairValueView, IconDetailsView> { - var symbolLabel: UILabel { fView.fView.detailsLabel } +typealias SwapIconDetailsView = GenericPairValueView + +final class SwapAssetView: GenericPairValueView { + var assetLabel: UILabel { fView.fView.detailsLabel } var disclosureImageView: UIImageView { fView.fView.imageView } var hubNameView: UILabel { sView.detailsLabel } var hubImageView: UIImageView { sView.imageView } @@ -18,7 +20,7 @@ final class SwapSymbolView: GenericPairValueView Date: Wed, 4 Oct 2023 08:24:28 +0300 Subject: [PATCH 014/204] remove tests, add swap folder --- novawallet.xcodeproj/project.pbxproj | 34 ++++++++----------- .../Setup}/Model/MockViewModelFactory.swift | 0 .../Setup/Model/ViewModels.swift} | 0 .../Setup}/SwapSetupInteractor.swift | 0 .../Setup}/SwapSetupPresenter.swift | 0 .../Setup}/SwapSetupProtocols.swift | 0 .../Setup}/SwapSetupViewController.swift | 0 .../Setup}/SwapSetupViewFactory.swift | 0 .../Setup}/SwapSetupWireframe.swift | 0 .../Setup}/View/SwapAmountInput.swift | 0 .../Setup}/View/SwapAmountInputView.swift | 0 .../Setup}/View/SwapAssetControl.swift | 0 .../Setup}/View/SwapAssetView.swift | 0 .../Setup}/View/SwapSetupViewLayout.swift | 0 .../Modules/SwapSetup/SwapSetupTests.swift | 16 --------- 15 files changed, 15 insertions(+), 35 deletions(-) rename novawallet/Modules/{SwapSetup => Swaps/Setup}/Model/MockViewModelFactory.swift (100%) rename novawallet/Modules/{SwapSetup/Model/SwapsAssetViewModel.swift => Swaps/Setup/Model/ViewModels.swift} (100%) rename novawallet/Modules/{SwapSetup => Swaps/Setup}/SwapSetupInteractor.swift (100%) rename novawallet/Modules/{SwapSetup => Swaps/Setup}/SwapSetupPresenter.swift (100%) rename novawallet/Modules/{SwapSetup => Swaps/Setup}/SwapSetupProtocols.swift (100%) rename novawallet/Modules/{SwapSetup => Swaps/Setup}/SwapSetupViewController.swift (100%) rename novawallet/Modules/{SwapSetup => Swaps/Setup}/SwapSetupViewFactory.swift (100%) rename novawallet/Modules/{SwapSetup => Swaps/Setup}/SwapSetupWireframe.swift (100%) rename novawallet/Modules/{SwapSetup => Swaps/Setup}/View/SwapAmountInput.swift (100%) rename novawallet/Modules/{SwapSetup => Swaps/Setup}/View/SwapAmountInputView.swift (100%) rename novawallet/Modules/{SwapSetup => Swaps/Setup}/View/SwapAssetControl.swift (100%) rename novawallet/Modules/{SwapSetup => Swaps/Setup}/View/SwapAssetView.swift (100%) rename novawallet/Modules/{SwapSetup => Swaps/Setup}/View/SwapSetupViewLayout.swift (100%) delete mode 100644 novawalletTests/Modules/SwapSetup/SwapSetupTests.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 5387f605e7..16a07a5c0c 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -740,7 +740,7 @@ 77AB55592AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */; }; 77AB555B2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */; }; 77AB555D2AA24BA90058814E /* OperationDetailsPoolRewardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */; }; - 77C9BCBC2ACD1AF500022EA2 /* SwapsAssetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBB2ACD1AF500022EA2 /* SwapsAssetViewModel.swift */; }; + 77C9BCBC2ACD1AF500022EA2 /* ViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */; }; 77C9BCBE2ACD286100022EA2 /* MockViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBD2ACD286100022EA2 /* MockViewModelFactory.swift */; }; 77CB33CE2A38780700B6709A /* structures_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77CB33CD2A38780700B6709A /* structures_output.json */; }; 77CB33D22A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CB33D12A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift */; }; @@ -3673,7 +3673,6 @@ D264B2A8A516396051016CAB /* AssetReceivePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3934C46625930FA8D171D3E7 /* AssetReceivePresenter.swift */; }; D274117F06B12F955073D35B /* DelegationReferendumVotersViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7059B3F1E8DC94D36733B4C7 /* DelegationReferendumVotersViewLayout.swift */; }; D344C6DAC1F8BB6152BA8DD0 /* RecommendedValidatorListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C6573C52692E4A56E35FF9 /* RecommendedValidatorListProtocols.swift */; }; - D37305C9017F215D98002AC8 /* SwapSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A0F09EB970EC5D7C942BFDB /* SwapSetupTests.swift */; }; D3B48F82A875E301D749AC0B /* StakingUnbondConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674162035C7D9F226FA9964 /* StakingUnbondConfirmViewController.swift */; }; D3B74ED2525DE12423722DE2 /* AssetReceiveInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B243F7A096241F329224A18E /* AssetReceiveInteractor.swift */; }; D3F199376DAEBF380C5FFD9D /* DAppAddFavoriteViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A759A20A4A39B3B0E2A735 /* DAppAddFavoriteViewLayout.swift */; }; @@ -4408,7 +4407,6 @@ 397F057FD5B16A58E5F30F07 /* NPoolsUnstakeConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeConfirmWireframe.swift; sourceTree = ""; }; 39907750D40A8DD7FE1288C8 /* CreateWatchOnlyViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateWatchOnlyViewController.swift; sourceTree = ""; }; 399700B22225DD916DFACAF9 /* DelegateVotedReferendaViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegateVotedReferendaViewFactory.swift; sourceTree = ""; }; - 3A0F09EB970EC5D7C942BFDB /* SwapSetupTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupTests.swift; sourceTree = ""; }; 3A46EE888D60C1538A0A3EFC /* NftDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsProtocols.swift; sourceTree = ""; }; 3A7235097E09C94005B091B4 /* CommonDelegationTracksPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommonDelegationTracksPresenter.swift; sourceTree = ""; }; 3A76BDAB14EEA1C4E23B884E /* ParitySignerTxQrViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxQrViewController.swift; sourceTree = ""; }; @@ -4712,7 +4710,7 @@ 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashModel.swift; sourceTree = ""; }; 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashViewModel.swift; sourceTree = ""; }; 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsPoolRewardView.swift; sourceTree = ""; }; - 77C9BCBB2ACD1AF500022EA2 /* SwapsAssetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapsAssetViewModel.swift; sourceTree = ""; }; + 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModels.swift; sourceTree = ""; }; 77C9BCBD2ACD286100022EA2 /* MockViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockViewModelFactory.swift; sourceTree = ""; }; 77CB33CD2A38780700B6709A /* structures_output.json */ = {isa = PBXFileReference; explicitFileType = text.json; fileEncoding = 4; path = structures_output.json; sourceTree = ""; usesTabs = 0; wrapsLines = 0; }; 77CB33D12A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Web3NameIntegrityVerifierWithCanonicalizationData.swift; sourceTree = ""; }; @@ -8755,14 +8753,6 @@ path = ParaStkCollatorInfo; sourceTree = ""; }; - 1ED929902157CCBAD0BD894E /* SwapSetup */ = { - isa = PBXGroup; - children = ( - 3A0F09EB970EC5D7C942BFDB /* SwapSetupTests.swift */, - ); - path = SwapSetup; - sourceTree = ""; - }; 249BACDEE5CB2B1ECDF470D9 /* AccountExportPassword */ = { isa = PBXGroup; children = ( @@ -8814,7 +8804,7 @@ path = AccountManagement; sourceTree = ""; }; - 29BD7DA0076BA8BC3411221A /* SwapSetup */ = { + 29BD7DA0076BA8BC3411221A /* Setup */ = { isa = PBXGroup; children = ( 77C9BCBA2ACD1AE800022EA2 /* Model */, @@ -8826,7 +8816,7 @@ BBEB2BC52266AF493D310834 /* SwapSetupViewController.swift */, 6A6BCE8F8DB7576E1E5B5974 /* SwapSetupViewFactory.swift */, ); - path = SwapSetup; + path = Setup; sourceTree = ""; }; 2A1FB6759F2E8A05A1894287 /* GovernanceUnavailableTracks */ = { @@ -9577,12 +9567,20 @@ 77C9BCBA2ACD1AE800022EA2 /* Model */ = { isa = PBXGroup; children = ( - 77C9BCBB2ACD1AF500022EA2 /* SwapsAssetViewModel.swift */, + 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */, 77C9BCBD2ACD286100022EA2 /* MockViewModelFactory.swift */, ); path = Model; sourceTree = ""; }; + 77C9BCBF2ACD2E0300022EA2 /* Swaps */ = { + isa = PBXGroup; + children = ( + 29BD7DA0076BA8BC3411221A /* Setup */, + ); + path = Swaps; + sourceTree = ""; + }; 77CB33D32A38894600B6709A /* Integrity */ = { isa = PBXGroup; children = ( @@ -12825,7 +12823,7 @@ 9D97DD4BC9672502D2E2A625 /* TokensManage */, EC1A579A3747EB16688DAEBF /* AssetReceive */, C9850B4B70AEFEABB96269FF /* TransactionHistory */, - 29BD7DA0076BA8BC3411221A /* SwapSetup */, + 77C9BCBF2ACD2E0300022EA2 /* Swaps */, ); path = Modules; sourceTree = ""; @@ -14311,7 +14309,6 @@ 84B7C705289BFA79001A3566 /* AccountManagement */, 84B7C708289BFA79001A3566 /* WalletList */, 84B7C70A289BFA79001A3566 /* ControllerAccount */, - 1ED929902157CCBAD0BD894E /* SwapSetup */, ); path = Modules; sourceTree = ""; @@ -21438,7 +21435,7 @@ 845B821926EF808D00D25C72 /* MetaAccountMapper.swift in Sources */, 19A29027666EB5388CBFAD61 /* StakingRewardDetailsInteractor.swift in Sources */, 846AC7EF2638D9200075F7DA /* YourValidatorTableCell.swift in Sources */, - 77C9BCBC2ACD1AF500022EA2 /* SwapsAssetViewModel.swift in Sources */, + 77C9BCBC2ACD1AF500022EA2 /* ViewModels.swift in Sources */, C937154FA9021AECD72A871B /* StakingRewardDetailsViewController.swift in Sources */, 84770F27291F7CD400852A33 /* ReferendumDetailsInitData.swift in Sources */, 84B8AA8529F910AD00347A37 /* WalletConnectStateError.swift in Sources */, @@ -22972,7 +22969,6 @@ 84B7C748289BFA79001A3566 /* WalletListTests.swift in Sources */, 84B7C720289BFA79001A3566 /* ReferralCrowdloanTests.swift in Sources */, F4897BB126AED13D0075F291 /* EraCountdownOperationFactoryStub.swift in Sources */, - D37305C9017F215D98002AC8 /* SwapSetupTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/novawallet/Modules/SwapSetup/Model/MockViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/MockViewModelFactory.swift similarity index 100% rename from novawallet/Modules/SwapSetup/Model/MockViewModelFactory.swift rename to novawallet/Modules/Swaps/Setup/Model/MockViewModelFactory.swift diff --git a/novawallet/Modules/SwapSetup/Model/SwapsAssetViewModel.swift b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift similarity index 100% rename from novawallet/Modules/SwapSetup/Model/SwapsAssetViewModel.swift rename to novawallet/Modules/Swaps/Setup/Model/ViewModels.swift diff --git a/novawallet/Modules/SwapSetup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift similarity index 100% rename from novawallet/Modules/SwapSetup/SwapSetupInteractor.swift rename to novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift diff --git a/novawallet/Modules/SwapSetup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift similarity index 100% rename from novawallet/Modules/SwapSetup/SwapSetupPresenter.swift rename to novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift diff --git a/novawallet/Modules/SwapSetup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift similarity index 100% rename from novawallet/Modules/SwapSetup/SwapSetupProtocols.swift rename to novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift diff --git a/novawallet/Modules/SwapSetup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift similarity index 100% rename from novawallet/Modules/SwapSetup/SwapSetupViewController.swift rename to novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift diff --git a/novawallet/Modules/SwapSetup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift similarity index 100% rename from novawallet/Modules/SwapSetup/SwapSetupViewFactory.swift rename to novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift diff --git a/novawallet/Modules/SwapSetup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift similarity index 100% rename from novawallet/Modules/SwapSetup/SwapSetupWireframe.swift rename to novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift diff --git a/novawallet/Modules/SwapSetup/View/SwapAmountInput.swift b/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift similarity index 100% rename from novawallet/Modules/SwapSetup/View/SwapAmountInput.swift rename to novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift diff --git a/novawallet/Modules/SwapSetup/View/SwapAmountInputView.swift b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift similarity index 100% rename from novawallet/Modules/SwapSetup/View/SwapAmountInputView.swift rename to novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift diff --git a/novawallet/Modules/SwapSetup/View/SwapAssetControl.swift b/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift similarity index 100% rename from novawallet/Modules/SwapSetup/View/SwapAssetControl.swift rename to novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift diff --git a/novawallet/Modules/SwapSetup/View/SwapAssetView.swift b/novawallet/Modules/Swaps/Setup/View/SwapAssetView.swift similarity index 100% rename from novawallet/Modules/SwapSetup/View/SwapAssetView.swift rename to novawallet/Modules/Swaps/Setup/View/SwapAssetView.swift diff --git a/novawallet/Modules/SwapSetup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift similarity index 100% rename from novawallet/Modules/SwapSetup/View/SwapSetupViewLayout.swift rename to novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift diff --git a/novawalletTests/Modules/SwapSetup/SwapSetupTests.swift b/novawalletTests/Modules/SwapSetup/SwapSetupTests.swift deleted file mode 100644 index 65c8446bda..0000000000 --- a/novawalletTests/Modules/SwapSetup/SwapSetupTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -import XCTest - -class SwapSetupTests: XCTestCase { - - override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() { - XCTFail("Did you forget to add tests?") - } -} From cfa51bd4c3c465deac9417bf050518a01d248236 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 4 Oct 2023 08:27:15 +0300 Subject: [PATCH 015/204] fixes after merge --- novawallet/ru.lproj/Localizable.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index ea3e86a500..14e7d6f1be 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -533,7 +533,7 @@ "common.today" = "Сегодня"; "common.yesterday" = "Вчера"; "common.tx.details" = "Детали транзакции"; -"common.unknown" = "Неизвест��о"; +"common.unknown" = "Неизвестно"; "staking.month.period.title" = "Eжемесячно"; "staking.year.period.title" = "Eжегодно"; "qr.scan.error.no.info" = "Не удается декодировать QR"; @@ -1016,7 +1016,7 @@ "governance.referendums.time.execute" = "Выполнение через %@"; "governance.referendums.time.approve" = "Одобрение через %@"; "governance.referendums.time.reject" = "Отклонение через %@"; -"governance.referendums.time.timeout" = "Тайм-аут ��ерез %@"; +"governance.referendums.time.timeout" = "Тайм-аут через %@"; "governance.referendums.time.waiting.deposit" = "Ожидание депозита"; "governance.referendums.time.deciding" = "Начнёт решаться через %@"; "governance.referendums.status.killed" = "Удалён"; From 48f5207bc0d889b9041a9e2fe35cd178ea19ba02 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 4 Oct 2023 13:15:29 +0500 Subject: [PATCH 016/204] support hex encoded assetId for statemine Assets --- novawallet.xcodeproj/project.pbxproj | 12 +++ .../Common/Model/AssetStorageInfo.swift | 22 +++++- .../Common/Model/StateminAssetExtras.swift | 78 +++++++++++++++++++ .../StorageKeyEncodingOperation.swift | 32 +++++++- .../RemoteSubscriptionRequests.swift | 20 ++++- .../WalletRemoteSubscriptionService.swift | 5 +- .../WalletRemoteSubscriptionWrapper.swift | 8 +- .../ExtrinsicProcessing.swift | 2 +- .../ExtrinsicProcessor+Matching.swift | 18 ++++- .../Calls/Assets/PalletAssets+Call.swift | 52 +++++++++++++ .../Calls/Common/AssetsTransfer.swift | 2 +- .../Calls/Common/SubstrateCallFactory.swift | 8 +- .../AssetConversionPallet.swift | 11 +-- .../Substrate/Types/CallCodingPath.swift | 43 +--------- .../AssetHubSwapOperationFactory.swift | 16 +++- .../AssetHub/AssetHubTokensConverter.swift | 33 +++++--- .../OnChain/OnChainTransferInteractor.swift | 8 +- .../AssetStorageInfoOperationFactory.swift | 4 +- 18 files changed, 278 insertions(+), 96 deletions(-) create mode 100644 novawallet/Common/Substrate/Calls/Assets/PalletAssets+Call.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index add234c2d3..a14c89f769 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -253,6 +253,7 @@ 0CD352952ACAF59900B3E446 /* BigRational.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD352942ACAF59900B3E446 /* BigRational.swift */; }; 0CD352972ACAFADA00B3E446 /* AssetConversionOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD352962ACAFADA00B3E446 /* AssetConversionOperationFactory.swift */; }; 0CD352982ACB01FD00B3E446 /* AccountGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845B822426EFE03E00D25C72 /* AccountGenerator.swift */; }; + 0CD3529B2ACD3E4300B3E446 /* PalletAssets+Call.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD3529A2ACD3E4300B3E446 /* PalletAssets+Call.swift */; }; 0CE150502A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE1504F2A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift */; }; 0CE150542A70EA2200B61CC1 /* NominationPoolsSyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE150532A70EA2200B61CC1 /* NominationPoolsSyncTests.swift */; }; 0CE550B32A49658700F0A7AC /* StakingDuration+Localizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE550B22A49658700F0A7AC /* StakingDuration+Localizable.swift */; }; @@ -4234,6 +4235,7 @@ 0CD352922ACAD7A500B3E446 /* AssetHubExtrinsicService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubExtrinsicService.swift; sourceTree = ""; }; 0CD352942ACAF59900B3E446 /* BigRational.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BigRational.swift; sourceTree = ""; }; 0CD352962ACAFADA00B3E446 /* AssetConversionOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionOperationFactory.swift; sourceTree = ""; }; + 0CD3529A2ACD3E4300B3E446 /* PalletAssets+Call.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PalletAssets+Call.swift"; sourceTree = ""; }; 0CDFFCC54A504417F4ACE7AA /* NftListInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftListInteractor.swift; sourceTree = ""; }; 0CE1504F2A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsPoolSubscriptionService.swift; sourceTree = ""; }; 0CE150532A70EA2200B61CC1 /* NominationPoolsSyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsSyncTests.swift; sourceTree = ""; }; @@ -8616,6 +8618,14 @@ path = V3; sourceTree = ""; }; + 0CD352992ACD3E3500B3E446 /* Assets */ = { + isa = PBXGroup; + children = ( + 0CD3529A2ACD3E4300B3E446 /* PalletAssets+Call.swift */, + ); + path = Assets; + sourceTree = ""; + }; 0CE550B42A4973BA00F0A7AC /* StakingUnbondSetup */ = { isa = PBXGroup; children = ( @@ -11563,6 +11573,7 @@ 845BB8C725E45D0600E5FCDC /* Calls */ = { isa = PBXGroup; children = ( + 0CD352992ACD3E3500B3E446 /* Assets */, 0C22006C2ACAAC0F0067BA61 /* AssetConversionPallet */, 0C7945BC2ABB22AA001C07CA /* XTokens */, 0C13D3052A7FB9170054BB6F /* NominationPools */, @@ -20441,6 +20452,7 @@ 84D8F16324D8194100AF43E9 /* TitleWithSubtitleTableViewCell.swift in Sources */, 0C9C64322A8D67A0004DC078 /* StakingNPoolsInteractor.swift in Sources */, F462B35C260C86880005AB01 /* ViewHolder.swift in Sources */, + 0CD3529B2ACD3E4300B3E446 /* PalletAssets+Call.swift in Sources */, 8846F73E29D7561100B8B776 /* Data+base36.swift in Sources */, 84948C36287DD1C800E6DD3E /* NftListRMRKV2ViewModel.swift in Sources */, 84C515FB26D84F8C000DBA45 /* AccountImportWrapper.swift in Sources */, diff --git a/novawallet/Common/Model/AssetStorageInfo.swift b/novawallet/Common/Model/AssetStorageInfo.swift index 5ef08651bc..f50001a62f 100644 --- a/novawallet/Common/Model/AssetStorageInfo.swift +++ b/novawallet/Common/Model/AssetStorageInfo.swift @@ -19,9 +19,15 @@ struct NativeTokenStorageInfo { let transferCallPath: CallCodingPath } +struct AssetsPalletStorageInfo { + let assetId: JSON + let assetIdString: String + let palletName: String? +} + enum AssetStorageInfo { case native(info: NativeTokenStorageInfo) - case statemine(extras: StatemineAssetExtras) + case statemine(info: AssetsPalletStorageInfo) case orml(info: OrmlTokenStorageInfo) case erc20(contractAccount: AccountId) case evmNative @@ -49,7 +55,19 @@ extension AssetStorageInfo { throw AssetStorageInfoError.unexpectedTypeExtras } - return .statemine(extras: extras) + let assetId = try StatemineAssetSerializer.decode( + assetId: extras.assetId, + palletName: extras.palletName, + codingFactory: codingFactory + ) + + let info = AssetsPalletStorageInfo( + assetId: assetId, + assetIdString: extras.assetId, + palletName: extras.palletName + ) + + return .statemine(info: info) case .evmAsset: guard let contractAddress = asset.typeExtras?.stringValue else { throw AssetStorageInfoError.unexpectedTypeExtras diff --git a/novawallet/Common/Model/StateminAssetExtras.swift b/novawallet/Common/Model/StateminAssetExtras.swift index 3699b68757..cda95d5b7b 100644 --- a/novawallet/Common/Model/StateminAssetExtras.swift +++ b/novawallet/Common/Model/StateminAssetExtras.swift @@ -1,6 +1,84 @@ import Foundation +import SubstrateSdk +import BigInt struct StatemineAssetExtras: Codable { let assetId: String let palletName: String? + + init(info: AssetsPalletStorageInfo) { + assetId = info.assetIdString + palletName = info.palletName + } +} + +enum StatemineAssetSerializerError: Error { + case assetIdTypeNotFound(palletName: String?) +} + +enum StatemineAssetSerializer { + private static func extractAssetIdType( + from codingFactory: RuntimeCoderFactoryProtocol, + palletName: String? + ) -> String? { + let callPath = PalletAssets.assetsTransfer(for: palletName) + + guard let call = codingFactory.getCall(for: callPath), !call.arguments.isEmpty else { + return nil + } + + return call.arguments[0].type + } + + static func decode( + assetId: String, + palletName: String?, + codingFactory: RuntimeCoderFactoryProtocol + ) throws -> JSON { + // assetId is either integer or complicated data structure + guard assetId.isHex() else { + return JSON.stringValue(assetId) + } + + guard let assetIdType = extractAssetIdType(from: codingFactory, palletName: palletName) else { + throw StatemineAssetSerializerError.assetIdTypeNotFound(palletName: palletName) + } + + let data = try Data(hexString: assetId) + + let decoder = try codingFactory.createDecoder(from: data) + + return try decoder.read(type: assetIdType) + } + + static func encode( + assetId: JSON, + palletName: String?, + codingFactory: RuntimeCoderFactoryProtocol + ) throws -> String { + // assetId is either integer or complicated data structure + if case let .stringValue(assetIdString) = assetId, BigUInt(assetIdString) != nil { + return assetIdString + } + + guard let assetIdType = extractAssetIdType(from: codingFactory, palletName: palletName) else { + throw StatemineAssetSerializerError.assetIdTypeNotFound(palletName: palletName) + } + + let encoder = codingFactory.createEncoder() + + try encoder.append(json: assetId, type: assetIdType) + + let data = try encoder.encode() + + return data.toHex(includePrefix: true) + } + + static func subscriptionKeyEncoder(for assetId: String) -> ((String) throws -> Data)? { + if assetId.isHex() { + return { try Data(hexString: $0) } + } else { + return nil + } + } } diff --git a/novawallet/Common/Operation/StorageKeyEncodingOperation.swift b/novawallet/Common/Operation/StorageKeyEncodingOperation.swift index eab497313b..75eab7c33f 100644 --- a/novawallet/Common/Operation/StorageKeyEncodingOperation.swift +++ b/novawallet/Common/Operation/StorageKeyEncodingOperation.swift @@ -60,14 +60,21 @@ class UnkeyedEncodingOperation: BaseOperation { class MapKeyEncodingOperation: BaseOperation<[Data]> { var keyParams: [T]? var codingFactory: RuntimeCoderFactoryProtocol? + var paramEncoder: ((T) throws -> Data)? let path: StorageCodingPath let storageKeyFactory: StorageKeyFactoryProtocol - init(path: StorageCodingPath, storageKeyFactory: StorageKeyFactoryProtocol, keyParams: [T]? = nil) { + init( + path: StorageCodingPath, + storageKeyFactory: StorageKeyFactoryProtocol, + keyParams: [T]? = nil, + paramEncoder: ((T) throws -> Data)? = nil + ) { self.path = path self.keyParams = keyParams self.storageKeyFactory = storageKeyFactory + self.paramEncoder = paramEncoder super.init() } @@ -109,10 +116,17 @@ class MapKeyEncodingOperation: BaseOperation<[Data]> { } let keys: [Data] = try keyParams.map { keyParam in - let encoder = factory.createEncoder() - try encoder.append(keyParam, ofType: keyType) + let encodedParam: Data - let encodedParam = try encoder.encode() + if let paramEncoder = paramEncoder { + encodedParam = try paramEncoder(keyParam) + } else { + encodedParam = try encodeParam( + keyParam, + factory: factory, + type: keyType + ) + } return try storageKeyFactory.createStorageKey( moduleName: path.moduleName, @@ -141,6 +155,16 @@ class MapKeyEncodingOperation: BaseOperation<[Data]> { performEncoding() } + + private func encodeParam( + _ param: T, + factory: RuntimeCoderFactoryProtocol, + type: String + ) throws -> Data { + let encoder = factory.createEncoder() + try encoder.append(param, ofType: type) + return try encoder.encode() + } } class DoubleMapKeyEncodingOperation: BaseOperation<[Data]> { diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/RemoteSubscriptionRequests.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/RemoteSubscriptionRequests.swift index 521bbb2d32..e6f15bc020 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/RemoteSubscriptionRequests.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/RemoteSubscriptionRequests.swift @@ -38,12 +38,30 @@ struct MapSubscriptionRequest: SubscriptionRequestProtocol { let storagePath: StorageCodingPath let localKey: String let keyParamClosure: () throws -> T + let paramEncoder: ((T) throws -> Data)? + + init( + storagePath: StorageCodingPath, + localKey: String, + keyParamClosure: @escaping () throws -> T, + paramEncoder: ((T) throws -> Data)? = nil + ) { + self.storagePath = storagePath + self.localKey = localKey + self.keyParamClosure = keyParamClosure + self.paramEncoder = paramEncoder + } func createKeyEncodingWrapper( using storageKeyFactory: StorageKeyFactoryProtocol, codingFactoryClosure: @escaping () throws -> RuntimeCoderFactoryProtocol ) -> CompoundOperationWrapper { - let encodingOperation = MapKeyEncodingOperation(path: storagePath, storageKeyFactory: storageKeyFactory) + let encodingOperation = MapKeyEncodingOperation( + path: storagePath, + storageKeyFactory: storageKeyFactory, + paramEncoder: paramEncoder + ) + encodingOperation.configurationBlock = { do { let keyParam = try keyParamClosure() diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscriptionService.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscriptionService.swift index 937fa4426a..ad4c0da8a8 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscriptionService.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscriptionService.swift @@ -211,7 +211,7 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu storagePath: accountStoragePath, localKey: accountLocalKey, keyParamClosure: { (assetId, accountId) }, - param1Encoder: nil, + param1Encoder: StatemineAssetSerializer.subscriptionKeyEncoder(for: assetId), param2Encoder: nil ) @@ -225,7 +225,8 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu let detailsRequest = MapSubscriptionRequest( storagePath: detailsStoragePath, localKey: detailsLocalKey, - keyParamClosure: { StringScaleMapper(value: assetId) } + keyParamClosure: { assetId }, + paramEncoder: StatemineAssetSerializer.subscriptionKeyEncoder(for: assetId) ) let handlingFactory = AssetsSubscriptionHandlingFactory( diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscriptionWrapper.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscriptionWrapper.swift index 512e8dccd2..8eb7e6a6e9 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscriptionWrapper.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscriptionWrapper.swift @@ -146,9 +146,9 @@ extension WalletRemoteSubscriptionWrapper: WalletRemoteSubscriptionWrapperProtoc chainFormat: chainAsset.chain.chainFormat, completion: completion ) - case let .statemine(extras): + case let .statemine(info): return subscribeAssets( - using: extras, + using: .init(info: info), accountId: accountId, chainAssetId: chainAsset.chainAssetId, completion: completion @@ -182,11 +182,11 @@ extension WalletRemoteSubscriptionWrapper: WalletRemoteSubscriptionWrapperProtoc queue: .main, closure: completion ) - case let .statemine(extras): + case let .statemine(info): remoteSubscriptionService.detachFromAsset( for: subscriptionId, accountId: accountId, - extras: extras, + extras: .init(info: info), chainId: chainAssetId.chainId, queue: .main, closure: completion diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessing.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessing.swift index 6262a58aec..7fa2865359 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessing.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessing.swift @@ -60,7 +60,7 @@ extension ExtrinsicProcessor: ExtrinsicProcessing { extrinsicIndex: extrinsicIndex, extrinsic: extrinsic, eventRecords: eventRecords, - metadata: coderFactory.metadata, + codingFactory: coderFactory, context: runtimeJsonContext ) { return processingResult diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Matching.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Matching.swift index 70b9dc2a1d..d3ff1b3613 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Matching.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Matching.swift @@ -14,7 +14,7 @@ private typealias AssetsParsingResult = ( callPath: CallCodingPath, isAccountMatched: Bool, callAccountId: AccountId?, - callAssetId: String, + callAssetId: JSON, callAmount: BigUInt ) @@ -296,10 +296,12 @@ extension ExtrinsicProcessor { extrinsicIndex: UInt32, extrinsic: Extrinsic, eventRecords: [EventRecord], - metadata: RuntimeMetadataProtocol, + codingFactory: RuntimeCoderFactoryProtocol, context: RuntimeJsonContext ) -> ExtrinsicProcessingResult? { do { + let metadata = codingFactory.metadata + let rawContext = context.toRawContext() let maybeAddress = extrinsic.signature?.address let maybeSender = try maybeAddress?.map(to: MultiAddress.self, with: rawContext).accountId @@ -307,7 +309,6 @@ extension ExtrinsicProcessor { let result = try parseAssetsExtrinsic(extrinsic, sender: maybeSender, context: context) guard - result.callPath.isAssetsTransfer, result.isAccountMatched, let sender = maybeSender else { return nil @@ -329,6 +330,13 @@ extension ExtrinsicProcessor { let peerId = accountId == sender ? result.callAccountId : sender + let remotePalletName = result.callPath.moduleName + let remoteAssetId = try StatemineAssetSerializer.encode( + assetId: result.callAssetId, + palletName: remotePalletName, + codingFactory: codingFactory + ) + let maybeAsset = chain.assets.first { asset in guard asset.type == AssetType.statemine.rawValue, @@ -336,7 +344,9 @@ extension ExtrinsicProcessor { return false } - return typeExtra.assetId == result.callAssetId + let localPalletName = typeExtra.palletName ?? PalletAssets.name + + return remotePalletName == localPalletName && typeExtra.assetId == remoteAssetId } guard let asset = maybeAsset else { diff --git a/novawallet/Common/Substrate/Calls/Assets/PalletAssets+Call.swift b/novawallet/Common/Substrate/Calls/Assets/PalletAssets+Call.swift new file mode 100644 index 0000000000..d7014c0c58 --- /dev/null +++ b/novawallet/Common/Substrate/Calls/Assets/PalletAssets+Call.swift @@ -0,0 +1,52 @@ +import Foundation + +extension PalletAssets { + static let knownPalletNames: [String] = [ + PalletAssets.name, + "LocalAssets", + "ForeignAssets" + ] + + static func assetsTransfer(for palletName: String?) -> CallCodingPath { + CallCodingPath(moduleName: palletName ?? PalletAssets.name, callName: "transfer") + } + + static func assetsTransferKeepAlive(for palletName: String?) -> CallCodingPath { + CallCodingPath(moduleName: palletName ?? PalletAssets.name, callName: "transfer_keep_alive") + } + + static func assetsForceTransfer(for palletName: String?) -> CallCodingPath { + CallCodingPath(moduleName: palletName ?? PalletAssets.name, callName: "force_transfer") + } + + static func assetsTransferAll(for palletName: String?) -> CallCodingPath { + CallCodingPath(moduleName: palletName ?? PalletAssets.name, callName: "transfer_all") + } + + static func possibleTransferCallPaths() -> [CallCodingPath] { + knownPalletNames.map { palletName in + [ + assetsTransfer(for: palletName), + assetsTransferKeepAlive(for: palletName), + assetsForceTransfer(for: palletName), + assetsTransferAll(for: palletName) + ] + }.flatMap { $0 } + } + + static var localAssetsTransfer: CallCodingPath { + CallCodingPath(moduleName: "LocalAssets", callName: "transfer") + } + + static var localAssetsTransferKeepAlive: CallCodingPath { + CallCodingPath(moduleName: "LocalAssets", callName: "transfer_keep_alive") + } + + static var localAssetsForceTransfer: CallCodingPath { + CallCodingPath(moduleName: "LocalAssets", callName: "force_transfer") + } + + static var localAssetsTransferAll: CallCodingPath { + CallCodingPath(moduleName: "LocalAssets", callName: "transfer_all") + } +} diff --git a/novawallet/Common/Substrate/Calls/Common/AssetsTransfer.swift b/novawallet/Common/Substrate/Calls/Common/AssetsTransfer.swift index 40412c070d..7594ac6fbf 100644 --- a/novawallet/Common/Substrate/Calls/Common/AssetsTransfer.swift +++ b/novawallet/Common/Substrate/Calls/Common/AssetsTransfer.swift @@ -9,7 +9,7 @@ struct AssetsTransfer: Codable { case amount } - let assetId: String + let assetId: JSON let target: MultiAddress @StringCodable var amount: BigUInt } diff --git a/novawallet/Common/Substrate/Calls/Common/SubstrateCallFactory.swift b/novawallet/Common/Substrate/Calls/Common/SubstrateCallFactory.swift index eb6890783c..fcf60da1b9 100644 --- a/novawallet/Common/Substrate/Calls/Common/SubstrateCallFactory.swift +++ b/novawallet/Common/Substrate/Calls/Common/SubstrateCallFactory.swift @@ -14,7 +14,7 @@ protocol SubstrateCallFactoryProtocol { func assetsTransfer( to receiver: AccountId, - extras: StatemineAssetExtras, + info: AssetsPalletStorageInfo, amount: BigUInt ) -> RuntimeCall @@ -101,11 +101,11 @@ final class SubstrateCallFactory: SubstrateCallFactoryProtocol { func assetsTransfer( to receiver: AccountId, - extras: StatemineAssetExtras, + info: AssetsPalletStorageInfo, amount: BigUInt ) -> RuntimeCall { - let args = AssetsTransfer(assetId: extras.assetId, target: .accoundId(receiver), amount: amount) - let callCodingPath = CallCodingPath.assetsTransfer(for: extras.palletName) + let args = AssetsTransfer(assetId: info.assetId, target: .accoundId(receiver), amount: amount) + let callCodingPath = PalletAssets.assetsTransfer(for: info.palletName) return RuntimeCall( moduleName: callCodingPath.moduleName, diff --git a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift index a1bb537e65..c8c487754b 100644 --- a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift +++ b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift @@ -10,7 +10,7 @@ enum AssetConversionPallet { enum PoolAsset { case native case assets(pallet: UInt8, index: BigUInt) - case foreignNetwork(XcmV3.NetworkId) + case foreign(AssetId) case undefined(AssetId) init(multilocation: XcmV3.Multilocation) { @@ -34,15 +34,8 @@ enum AssetConversionPallet { default: self = .undefined(multilocation) } - } else if multilocation.parents == 2, junctions.count == 1 { - switch junctions[0] { - case let .globalConsensus(network): - self = .foreignNetwork(network) - default: - self = .undefined(multilocation) - } } else { - self = .undefined(multilocation) + self = .foreign(multilocation) } } } diff --git a/novawallet/Common/Substrate/Types/CallCodingPath.swift b/novawallet/Common/Substrate/Types/CallCodingPath.swift index 406b0499ea..b6a928e61b 100644 --- a/novawallet/Common/Substrate/Types/CallCodingPath.swift +++ b/novawallet/Common/Substrate/Types/CallCodingPath.swift @@ -23,16 +23,7 @@ extension CallCodingPath { } var isAssetsTransfer: Bool { - [ - .assetsTransfer(for: nil), - .assetsTransferKeepAlive(for: nil), - .assetsForceTransfer(for: nil), - .assetsTransferAll(for: nil), - .localAssetsTransfer, - .localAssetsTransferKeepAlive, - .localAssetsForceTransfer, - .localAssetsTransferAll - ].contains(self) + PalletAssets.possibleTransferCallPaths().contains(self) } var isTokensTransfer: Bool { @@ -104,38 +95,6 @@ extension CallCodingPath { CallCodingPath(moduleName: "Currencies", callName: "transfer_all") } - static func assetsTransfer(for palletName: String?) -> CallCodingPath { - CallCodingPath(moduleName: palletName ?? "Assets", callName: "transfer") - } - - static func assetsTransferKeepAlive(for palletName: String?) -> CallCodingPath { - CallCodingPath(moduleName: palletName ?? "Assets", callName: "transfer_keep_alive") - } - - static func assetsForceTransfer(for palletName: String?) -> CallCodingPath { - CallCodingPath(moduleName: palletName ?? "Assets", callName: "force_transfer") - } - - static func assetsTransferAll(for palletName: String?) -> CallCodingPath { - CallCodingPath(moduleName: palletName ?? "Assets", callName: "transfer_all") - } - - static var localAssetsTransfer: CallCodingPath { - CallCodingPath(moduleName: "LocalAssets", callName: "transfer") - } - - static var localAssetsTransferKeepAlive: CallCodingPath { - CallCodingPath(moduleName: "LocalAssets", callName: "transfer_keep_alive") - } - - static var localAssetsForceTransfer: CallCodingPath { - CallCodingPath(moduleName: "LocalAssets", callName: "force_transfer") - } - - static var localAssetsTransferAll: CallCodingPath { - CallCodingPath(moduleName: "LocalAssets", callName: "transfer_all") - } - static var ethereumTransact: CallCodingPath { CallCodingPath(moduleName: "Ethereum", callName: "transact") } diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift index 712f04b1a4..55f8b9d1b1 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift @@ -81,14 +81,14 @@ final class AssetHubSwapOperationFactory { let optNativeAsset = chain.utilityAsset() - let initAssetsStore = [BigUInt: (AssetModel, StatemineAssetExtras)]() + let initAssetsStore = [JSON: (AssetModel, AssetsPalletStorageInfo)]() let assetsPalletTokens = chain.assets.reduce(into: initAssetsStore) { store, asset in let optStorageInfo = try? AssetStorageInfo.extract(from: asset, codingFactory: codingFactory) - guard case let .statemine(extras) = optStorageInfo, let assetId = BigUInt(extras.assetId) else { + guard case let .statemine(info) = optStorageInfo else { return } - store[assetId] = (asset, extras) + store[info.assetId] = (asset, info) } let mappingClosure: (AssetConversionPallet.PoolAsset) -> ChainAssetId? = { remoteAsset in @@ -100,7 +100,7 @@ final class AssetHubSwapOperationFactory { return nil } case let .assets(pallet, index): - guard let localToken = assetsPalletTokens[index] else { + guard let localToken = assetsPalletTokens[.stringValue(String(index))] else { return nil } @@ -113,6 +113,14 @@ final class AssetHubSwapOperationFactory { return nil } + return ChainAssetId(chainId: chain.chainId, assetId: localToken.0.assetId) + case let .foreign(remoteId): + guard + let json = try? remoteId.toScaleCompatibleJSON(), + let localToken = assetsPalletTokens[json] else { + return nil + } + return ChainAssetId(chainId: chain.chainId, assetId: localToken.0.assetId) default: return nil diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift index d386f85770..3a53564e37 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift @@ -33,19 +33,28 @@ enum AssetHubTokensConverter { switch storageInfo { case .native: return .init(parents: 0, interior: .init(items: [])) - case let .statemine(extras): - let palletName = extras.palletName ?? PalletAssets.name - - guard - let palletIndex = codingFactory.metadata.getModuleIndex(palletName), - let generalIndex = BigUInt(extras.assetId) else { - return nil + case let .statemine(info): + if info.assetIdString.isHex() { + let remoteAssetId = try? info.assetId.map( + to: AssetConversionPallet.AssetId.self, + with: codingFactory.createRuntimeJsonContext().toRawContext() + ) + + return remoteAssetId + } else { + let palletName = info.palletName ?? PalletAssets.name + + guard + let palletIndex = codingFactory.metadata.getModuleIndex(palletName), + let generalIndex = BigUInt(info.assetIdString) else { + return nil + } + + let palletJunction = XcmV3.Junction.palletInstance(palletIndex) + let generalIndexJunction = XcmV3.Junction.generalIndex(generalIndex) + + return .init(parents: 0, interior: .init(items: [palletJunction, generalIndexJunction])) } - - let palletJunction = XcmV3.Junction.palletInstance(palletIndex) - let generalIndexJunction = XcmV3.Junction.generalIndex(generalIndex) - - return .init(parents: 0, interior: .init(items: [palletJunction, generalIndexJunction])) default: return nil } diff --git a/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift b/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift index 545fd8baec..4bb0cf4733 100644 --- a/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift +++ b/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift @@ -282,11 +282,11 @@ class OnChainTransferInteractor: OnChainTransferBaseInteractor, RuntimeConstantF to builder: ExtrinsicBuilderProtocol, amount: OnChainTransferAmount, recepient: AccountId, - extras: StatemineAssetExtras + info: AssetsPalletStorageInfo ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { let call = callFactory.assetsTransfer( to: recepient, - extras: extras, + info: info, amount: amount.value ) @@ -327,12 +327,12 @@ class OnChainTransferInteractor: OnChainTransferBaseInteractor, RuntimeConstantF recepient: recepient, tokenStorageInfo: info ) - case let .statemine(extras): + case let .statemine(info): return try addingAssetsTransferCommand( to: builder, amount: amount, recepient: recepient, - extras: extras + info: info ) case let .native(info): return try addingNativeTransferCommand( diff --git a/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift b/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift index c2c3021250..79224ab149 100644 --- a/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift +++ b/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift @@ -137,7 +137,7 @@ extension AssetStorageInfoOperationFactory: AssetStorageInfoOperationFactoryProt } return createNativeAssetExistenceOperation(for: runtimeService) - case let .statemine(extras): + case let .statemine(info): guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { return .createWithError(ChainRegistryError.runtimeMetadaUnavailable) } @@ -147,7 +147,7 @@ extension AssetStorageInfoOperationFactory: AssetStorageInfoOperationFactoryProt } return createAssetsExistenceOperation( - for: extras, + for: .init(info: info), connection: connection, runtimeService: runtimeService ) From f54f93965c836047435db8f2e1c278cb34894491 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 5 Oct 2023 23:46:57 +0300 Subject: [PATCH 017/204] init --- novawallet.xcodeproj/project.pbxproj | 8 ++ .../Contents.json | 12 +++ .../new-info-icon.pdf | Bin 0 -> 2253 bytes .../StackTableCollapsableHeaderCell.swift | 45 +++++++++ .../StackTable/StackTitleMultiValueCell.swift | 5 + .../StackTitleMultiValueEditCell.swift | 61 ++++++++++++ .../Swaps/Setup/SwapSetupInteractor.swift | 89 +++++++++++++++++- .../Swaps/Setup/SwapSetupPresenter.swift | 9 +- .../Swaps/Setup/SwapSetupProtocols.swift | 21 ++++- .../Swaps/Setup/SwapSetupViewController.swift | 9 ++ .../Swaps/Setup/SwapSetupViewFactory.swift | 42 ++++++++- .../Setup/View/SwapSetupViewLayout.swift | 43 ++++++++- novawallet/en.lproj/Localizable.strings | 2 + novawallet/ru.lproj/Localizable.strings | 2 + 14 files changed, 340 insertions(+), 8 deletions(-) create mode 100644 novawallet/Assets.xcassets/iconInfoFilledAccent.imageset/Contents.json create mode 100644 novawallet/Assets.xcassets/iconInfoFilledAccent.imageset/new-info-icon.pdf create mode 100644 novawallet/Common/View/StackTable/StackTableCollapsableHeaderCell.swift create mode 100644 novawallet/Common/View/StackTable/StackTitleMultiValueEditCell.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index b18347b0fd..f8806ae9f7 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -783,6 +783,8 @@ 77EA2A292A333C1500B0670B /* arrays_input.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A1F2A333C1500B0670B /* arrays_input.json */; }; 77EA2A2A2A333C1500B0670B /* weird_input.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A202A333C1500B0670B /* weird_input.json */; }; 77EA2A2B2A333C1500B0670B /* structures_input.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A212A333C1500B0670B /* structures_input.json */; }; + 77ECB4702ACEEE2E0015CE9F /* StackTitleMultiValueEditCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77ECB46F2ACEEE2D0015CE9F /* StackTitleMultiValueEditCell.swift */; }; + 77ECB4722ACEF12E0015CE9F /* StackTableCollapsableHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77ECB4712ACEF12E0015CE9F /* StackTableCollapsableHeaderCell.swift */; }; 77ED167A2A0CF41700E1FC8C /* StakingRewardFiltersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77ED16792A0CF41600E1FC8C /* StakingRewardFiltersViewModel.swift */; }; 77ED167C2A0CF42E00E1FC8C /* StakingRewardFiltersPeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77ED167B2A0CF42E00E1FC8C /* StakingRewardFiltersPeriod.swift */; }; 77ED167E2A0D0AE900E1FC8C /* Lenses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77ED167D2A0D0AE900E1FC8C /* Lenses.swift */; }; @@ -4774,6 +4776,8 @@ 77EA2A1F2A333C1500B0670B /* arrays_input.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = arrays_input.json; sourceTree = ""; }; 77EA2A202A333C1500B0670B /* weird_input.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = weird_input.json; sourceTree = ""; }; 77EA2A212A333C1500B0670B /* structures_input.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = structures_input.json; sourceTree = ""; }; + 77ECB46F2ACEEE2D0015CE9F /* StackTitleMultiValueEditCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackTitleMultiValueEditCell.swift; sourceTree = ""; }; + 77ECB4712ACEF12E0015CE9F /* StackTableCollapsableHeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackTableCollapsableHeaderCell.swift; sourceTree = ""; }; 77ED16792A0CF41600E1FC8C /* StakingRewardFiltersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardFiltersViewModel.swift; sourceTree = ""; }; 77ED167B2A0CF42E00E1FC8C /* StakingRewardFiltersPeriod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardFiltersPeriod.swift; sourceTree = ""; }; 77ED167D2A0D0AE900E1FC8C /* Lenses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lenses.swift; sourceTree = ""; }; @@ -12157,8 +12161,10 @@ 8489A6CF27FD5B9E0040C066 /* StackActionView.swift */, 8489A6D127FD5FB80040C066 /* StackActionCell.swift */, 844D2A3F281B0ED70049CF5E /* StackTableHeaderCell.swift */, + 77ECB4712ACEF12E0015CE9F /* StackTableCollapsableHeaderCell.swift */, 844D2A41281B24510049CF5E /* StackUrlCell.swift */, 844D2A43281B28FB0049CF5E /* StackTitleMultiValueCell.swift */, + 77ECB46F2ACEEE2D0015CE9F /* StackTitleMultiValueEditCell.swift */, 845AADA22902D1EA00B5AE96 /* StackTitleValueDiffCell.swift */, 847012652982AE5700F29C87 /* StackTableView+Cell.swift */, 849D14C92994D9BC0048E947 /* StackIconTitleValueCell.swift */, @@ -19538,6 +19544,7 @@ 84FBECF92927403100FBEB83 /* EvmAssetContractId.swift in Sources */, 842EBB2F28909A7900B952D8 /* RoundedIconTitleHeaderView.swift in Sources */, 8472979A260B3095009B86D0 /* InitBondSelectValidatorsStartWireframe.swift in Sources */, + 77ECB4722ACEF12E0015CE9F /* StackTableCollapsableHeaderCell.swift in Sources */, 77A6F5CD2A31C4AA004AFD1A /* Web3TransferRecipientIntegrityVerifierFactory.swift in Sources */, 77A6F5CF2A31C4D4004AFD1A /* Web3TransferRecipientRepositoryFactory.swift in Sources */, 84BAD215293B0E8A00C55C49 /* UITableView+Section.swift in Sources */, @@ -20417,6 +20424,7 @@ 88C5F082297F0706001CCADE /* ReleaseVersion.swift in Sources */, 849013E224A9288B008F705E /* Language.swift in Sources */, 840D92A1278D8D6F0007B979 /* DAppBrowserStateError.swift in Sources */, + 77ECB4702ACEEE2E0015CE9F /* StackTitleMultiValueEditCell.swift in Sources */, 84FC190B29B7DB9F00BCCAA5 /* ExtrinsicServiceTypes.swift in Sources */, 849707A128F3E0AC00DD0A02 /* ReferendumVoterLocal.swift in Sources */, 774091FC2ACC053000172516 /* SwapAssetView.swift in Sources */, diff --git a/novawallet/Assets.xcassets/iconInfoFilledAccent.imageset/Contents.json b/novawallet/Assets.xcassets/iconInfoFilledAccent.imageset/Contents.json new file mode 100644 index 0000000000..837aee4580 --- /dev/null +++ b/novawallet/Assets.xcassets/iconInfoFilledAccent.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "new-info-icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconInfoFilledAccent.imageset/new-info-icon.pdf b/novawallet/Assets.xcassets/iconInfoFilledAccent.imageset/new-info-icon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..aba8e7fbd02c606fc3980095421056625c58ef94 GIT binary patch literal 2253 zcma)8TW{1j6n^)w@Jl5?!sFxPtAr{oZ9yw`wPng%#Y0Hbc2@0#odne7*KCY2 zA6n&YQ;qMCBp`V%S}Yz5TI_P&3s&U47q?WrdPBu2CdDg z*Tfbf0W9g289mmy7Wd>isZX1L;BBcs1@RUVGcl<@Bm$(HYi5EG4Rm@7^$V*>{TwN) zpYs*!Yh~#Bq3BtCN^BBRRat3G2Oq6nsc)$~gXCAJZ(w`>;4})m|f`4%Qc}{dZ>)OZWxqP;}c|MY|*|u#jx#5A}&DHK(L)8yI3#vmuv|}`aI61la G=goKdw9b|Q literal 0 HcmV?d00001 diff --git a/novawallet/Common/View/StackTable/StackTableCollapsableHeaderCell.swift b/novawallet/Common/View/StackTable/StackTableCollapsableHeaderCell.swift new file mode 100644 index 0000000000..e2b5b3daa3 --- /dev/null +++ b/novawallet/Common/View/StackTable/StackTableCollapsableHeaderCell.swift @@ -0,0 +1,45 @@ +import UIKit +import SnapKit +import SoraUI + +final class CollapsableView: UIView { + var titleLabel = UILabel(style: .footnoteSecondary, textAlignment: .left, numberOfLines: 1) + var actionControl: ActionTitleControl = .create { + $0.indicator = ResizableImageActionIndicator(size: .init(width: 24, height: 24)) + $0.imageView.image = R.image.iconLinkChevron()?.tinted(with: R.color.colorTextSecondary()!) + $0.identityIconAngle = CGFloat.pi / 2.0 + $0.activationIconAngle = -CGFloat.pi / 2.0 + $0.titleLabel.text = nil + $0.horizontalSpacing = 0 + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .clear + + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var intrinsicContentSize: CGSize { + CGSize(width: UIView.noIntrinsicMetric, height: 24) + } + + private func configure() { + let contentView = UIView.hStack([ + titleLabel, + FlexibleSpaceView(), + actionControl + ]) + + addSubview(contentView) + contentView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } +} diff --git a/novawallet/Common/View/StackTable/StackTitleMultiValueCell.swift b/novawallet/Common/View/StackTable/StackTitleMultiValueCell.swift index 3fe2844052..a14caf8ef6 100644 --- a/novawallet/Common/View/StackTable/StackTitleMultiValueCell.swift +++ b/novawallet/Common/View/StackTable/StackTitleMultiValueCell.swift @@ -70,4 +70,9 @@ extension StackTitleMultiValueCell { bottomValue: viewModel.price ) } + + // TODO: Skeleton + func bind(loadableViewModel: LoadableViewModelState) { + loadableViewModel.value.map(bind) + } } diff --git a/novawallet/Common/View/StackTable/StackTitleMultiValueEditCell.swift b/novawallet/Common/View/StackTable/StackTitleMultiValueEditCell.swift new file mode 100644 index 0000000000..ee85983c6c --- /dev/null +++ b/novawallet/Common/View/StackTable/StackTitleMultiValueEditCell.swift @@ -0,0 +1,61 @@ +import Foundation +import UIKit + +final class StackTitleMultiValueEditCell: RowView>> { + var titleLabel: UILabel { rowContentView.titleView.detailsLabel } + var titleImageView: UIImageView { rowContentView.titleView.imageView } + var topValueImageView: UIImageView { rowContentView.valueView.fView.imageView } + var topValueLabel: UILabel { rowContentView.valueView.fView.detailsLabel } + var bottomValueLabel: UILabel { rowContentView.valueView.sView } + + convenience init() { + self.init(frame: CGRect(origin: .zero, size: CGSize(width: 340, height: 44.0))) + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .clear + + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + titleLabel.textColor = R.color.colorTextSecondary() + titleLabel.font = .regularFootnote + titleImageView.image = R.image.iconInfoFilledAccent() + + rowContentView.titleView.mode = .detailsIcon + rowContentView.titleView.spacing = 4 + + topValueImageView.image = R.image.iconPencil()?.tinted(with: R.color.colorIconSecondary()!) + topValueLabel.textColor = R.color.colorTextPrimary() + topValueLabel.font = .regularFootnote + + bottomValueLabel.textColor = R.color.colorTextSecondary() + bottomValueLabel.font = .caption1 + bottomValueLabel.textAlignment = .right + rowContentView.valueView.fView.iconWidth = 12 + rowContentView.valueView.fView.spacing = 6 + borderView.strokeColor = R.color.colorDivider()! + } +} + +extension StackTitleMultiValueEditCell: StackTableViewCellProtocol {} + +extension StackTitleMultiValueEditCell { + func bind(viewModel: BalanceViewModelProtocol) { + topValueLabel.text = viewModel.amount + bottomValueLabel.text = viewModel.price + } + + // TODO: Skeleton + func bind(loadableViewModel: LoadableViewModelState) { + loadableViewModel.value.map(bind) + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 0b4035f0f2..61e52e5a7e 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -1,7 +1,92 @@ import UIKit +import RobinHood +import BigInt -final class SwapSetupInteractor { +final class SwapSetupInteractor: AnyCancellableCleaning { weak var presenter: SwapSetupInteractorOutputProtocol? + let assetConversionOperationFactory: AssetConversionOperationFactoryProtocol + let assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol + let runtimeService: RuntimeProviderProtocol + let feeProxy: ExtrinsicFeeProxyProtocol + let extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol + + private let operationQueue: OperationQueue + private var quoteCall: CancellableCall? + + init( + assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, + assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol, + runtimeService: RuntimeProviderProtocol, + feeProxy: ExtrinsicFeeProxyProtocol, + extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, + operationQueue: OperationQueue + ) { + self.assetConversionOperationFactory = assetConversionOperationFactory + self.assetConversionExtrinsicService = assetConversionExtrinsicService + self.runtimeService = runtimeService + self.feeProxy = feeProxy + self.extrinsicServiceFactory = extrinsicServiceFactory + self.operationQueue = operationQueue + } + + private func quote(args: AssetConversion.QuoteArgs) { + clear(cancellable: "eCall) + + let wrapper = assetConversionOperationFactory.quote(for: args) + wrapper.targetOperation.completionBlock = { [weak self] in + guard self?.quoteCall === wrapper else { + return + } + do { + let result = try wrapper.targetOperation.extractNoCancellableResultData() + DispatchQueue.main.async { + self?.presenter?.didReceive(quote: result) + } + } catch { + self?.presenter?.didReceive(error: .quote(error)) + } + } + + quoteCall = wrapper + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } + + private func extrinsicService() -> ExtrinsicServiceProtocol? { + nil + } + + private func fee(args _: AssetConversion.CallArgs) { + guard let extrinsicService = extrinsicService() else { + presenter?.didReceive(error: .fetchFeeFailed(CommonError.undefined)) + return + } + +// let builder = assetConversionExtrinsicService.fetchExtrinsicBuilderClosure( +// for: args, +// codingFactory: runtimeCoderFactory +// ) +// feeProxy.estimateFee(using: extrinsicService, reuseIdentifier: "", setupBy: builder) + } +} + +extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { + func setup() { + feeProxy.delegate = self + } + + func calculateQuote(for args: AssetConversion.QuoteArgs) { + quote(args: args) + } } -extension SwapSetupInteractor: SwapSetupInteractorInputProtocol {} +extension SwapSetupInteractor: ExtrinsicFeeProxyDelegate { + func didReceiveFee(result: Result, for _: TransactionFeeId) { + switch result { + case let .success(dispatchInfo): + let fee = BigUInt(dispatchInfo.fee) + presenter?.didReceive(fee: fee) + case let .failure(error): + presenter?.didReceive(error: .fetchFeeFailed(error)) + } + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 887cc55dd5..186c4e0421 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -1,5 +1,6 @@ import Foundation import SoraFoundation +import BigInt final class SwapSetupPresenter { weak var view: SwapSetupViewProtocol? @@ -52,7 +53,13 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { func proceed() {} } -extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol {} +extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { + func didReceive(error _: SwapSetupError) {} + + func didReceive(quote _: AssetConversion.Quote) {} + + func didReceive(fee _: BigUInt?) {} +} extension SwapSetupPresenter: Localizable { func applyLocalization() { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 46c4b6656c..7fae05b665 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -1,3 +1,5 @@ +import BigInt + protocol SwapSetupViewProtocol: ControllerBackedProtocol { func didReceiveButtonState(title: String, enabled: Bool) func didReceiveInputChainAsset(payViewModel viewModel: SwapAssetInputViewModel) @@ -8,6 +10,8 @@ protocol SwapSetupViewProtocol: ControllerBackedProtocol { func didReceiveAmount(receiveInputViewModel inputViewModel: AmountInputViewModelProtocol) func didReceiveAmountInputPrice(receiveViewModel: String?) func didReceiveTitle(receiveViewModel viewModel: TitleHorizontalMultiValueView.Model) + func didReceiveRate(viewModel: LoadableViewModelState) + func didReceiveNetworkFee(viewModel: LoadableViewModelState) } protocol SwapSetupPresenterProtocol: AnyObject { @@ -18,8 +22,19 @@ protocol SwapSetupPresenterProtocol: AnyObject { func swap() } -protocol SwapSetupInteractorInputProtocol: AnyObject {} +protocol SwapSetupInteractorInputProtocol: AnyObject { + func calculateQuote(for args: AssetConversion.QuoteArgs) +} -protocol SwapSetupInteractorOutputProtocol: AnyObject {} +protocol SwapSetupInteractorOutputProtocol: AnyObject { + func didReceive(quote: AssetConversion.Quote) + func didReceive(fee: BigUInt?) + func didReceive(error: SwapSetupError) +} -protocol SwapSetupWireframeProtocol: AnyObject {} +protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, ErrorPresentable {} + +enum SwapSetupError: Error { + case quote(Error) + case fetchFeeFailed(Error) +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index b30870e8de..1300ea57f5 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -57,6 +57,7 @@ final class SwapSetupViewController: UIViewController, ViewHolder { private func setupLocalization() { title = R.string.localizable.walletAssetsSwap(preferredLanguages: selectedLocale.rLanguages) + rootView.setup(locale: selectedLocale) } @objc private func selectPayTokenAction() { @@ -124,6 +125,14 @@ extension SwapSetupViewController: SwapSetupViewProtocol { func didReceiveAmountInputPrice(receiveViewModel viewModel: String?) { rootView.receiveAmountInputView.bind(priceViewModel: viewModel) } + + func didReceiveRate(viewModel: LoadableViewModelState) { + rootView.rateCell.bind(loadableViewModel: viewModel) + } + + func didReceiveNetworkFee(viewModel: LoadableViewModelState) { + rootView.networkFeeCell.bind(loadableViewModel: viewModel) + } } extension SwapSetupViewController: Localizable { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index 0639b7782e..c563dfbd8d 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -1,5 +1,6 @@ import Foundation import SoraFoundation +import RobinHood struct SwapSetupViewFactory { static func createView() -> SwapSetupViewProtocol? { @@ -11,7 +12,10 @@ struct SwapSetupViewFactory { let balanceViewModelFactory = BalanceViewModelFactoryFacade( priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager)) - let interactor = SwapSetupInteractor() + guard let interactor = createInteractor() else { + return nil + } + let wireframe = SwapSetupWireframe() let presenter = SwapSetupPresenter( @@ -31,4 +35,40 @@ struct SwapSetupViewFactory { return view } + + private static func createInteractor() -> SwapSetupInteractor? { + let westmintChainId = KnowChainId.westmint + let chainRegistry = ChainRegistryFacade.sharedRegistry + + guard let connection = chainRegistry.getConnection(for: westmintChainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: westmintChainId), + let chainModel = chainRegistry.getChain(for: westmintChainId) else { + return nil + } + + let operationQueue = OperationManagerFacade.sharedDefaultQueue + + let assetConversionOperationFactory = AssetHubSwapOperationFactory( + chain: chainModel, + runtimeService: runtimeService, + connection: connection, + operationQueue: operationQueue + ) + let extrinsicServiceFactory = ExtrinsicServiceFactory( + runtimeRegistry: runtimeService, + engine: connection, + operationManager: OperationManager(operationQueue: operationQueue) + ) + + let interactor = SwapSetupInteractor( + assetConversionOperationFactory: assetConversionOperationFactory, + assetConversionExtrinsicService: AssetHubExtrinsicService(chain: chainModel), + runtimeService: runtimeService, + feeProxy: ExtrinsicFeeProxy(), + extrinsicServiceFactory: extrinsicServiceFactory, + operationQueue: operationQueue + ) + + return interactor + } } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index 860dd3c0b0..69835d0a10 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -27,6 +27,26 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { $0.imageWithTitleView?.iconImage = R.image.iconActionSwap() } + let detailsHeaderCell: CollapsableView = .create { + $0.actionControl.addTarget(self, action: #selector(detailsControlAction), for: .valueChanged) + $0.actionControl.imageView.isUserInteractionEnabled = false + } + + let detailsTableView: StackTableView = .create { + $0.cellHeight = 44 + $0.hasSeparators = true + $0.contentInsets = UIEdgeInsets(top: 4, left: 16, bottom: 4, right: 16) + $0.isHidden = true + } + + let rateCell: StackTitleMultiValueCell = .create { + $0.titleLabel.apply(style: .footnoteSecondary) + $0.rowContentView.titleView.iconWidth = 16 + $0.rowContentView.titleView.imageView.image = R.image.iconInfoFilledAccent() + } + + let networkFeeCell = StackTitleMultiValueEditCell() + override func setupStyle() { backgroundColor = R.color.colorSecondaryScreenBackground() } @@ -55,11 +75,16 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { receiveAmountView.snp.makeConstraints { $0.height.equalTo(18) } - addArrangedSubview(receiveAmountInputView) + addArrangedSubview(receiveAmountInputView, spacingAfter: 16) receiveAmountInputView.snp.makeConstraints { $0.height.equalTo(64) } + addArrangedSubview(detailsHeaderCell, spacingAfter: 8) + addArrangedSubview(detailsTableView) + detailsTableView.addArrangedSubview(rateCell) + detailsTableView.addArrangedSubview(networkFeeCell) + addSubview(switchButton) switchButton.snp.makeConstraints { $0.height.equalTo(switchButton.snp.width) @@ -68,4 +93,20 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { $0.centerX.equalTo(payAmountInputView.snp.centerX) } } + + func setup(locale: Locale) { + detailsHeaderCell.titleLabel.text = R.string.localizable.swapsSetupDetailsTitle( + preferredLanguages: locale.rLanguages + ) + rateCell.titleLabel.text = R.string.localizable.swapsSetupDetailsRate(preferredLanguages: locale.rLanguages) + networkFeeCell.titleLabel.text = R.string.localizable.commonNetwork(preferredLanguages: locale.rLanguages) + } + + @objc + private func detailsControlAction() { + detailsTableView.isHidden = !detailsHeaderCell.actionControl.isActivated + + detailsHeaderCell.actionControl.invalidateLayout() + detailsHeaderCell.setNeedsLayout() + } } diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index a0bf97d4fe..7ebcbc542a 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1387,3 +1387,5 @@ "swaps.setup.asset.max" = "Max:"; "common.alert.external.link.disclaimer.title" = "You are leaving Nova Wallet"; "common.alert.external.link.disclaimer.message" = "You will be redirected to %@"; +"swaps.setup.details.rate" = "Rate"; +"swaps.setup.details.title" = "Swap details"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 14e7d6f1be..905e0cd3ae 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1388,3 +1388,5 @@ "swaps.setup.asset.max" = "Максимум:"; "common.alert.external.link.disclaimer.title" = "Вы покидаете Nova Wallet"; "common.alert.external.link.disclaimer.message" = "Вы будете перенаправлены на сайт %@"; +"swaps.setup.details.rate" = "Курс"; +"swaps.setup.details.title" = "Детали обмена"; From ddbe6a55fccfba3bd87758b92c46082e5fe7aef3 Mon Sep 17 00:00:00 2001 From: Gulnaz <666lynx666@mail.ru> Date: Fri, 6 Oct 2023 12:32:57 +0300 Subject: [PATCH 018/204] Swaps: Setup (#848) * add control * add swap button, add titles * refactor control * localization, cleanup * remove tests, add swap folder * fixes after merge --- Podfile.lock | 2 +- novawallet.xcodeproj/project.pbxproj | 84 ++++++++++ .../iconActionSwap.imageset/Contents.json | 12 ++ .../container-transaction-type.pdf | Bin 0 -> 3770 bytes .../iconAddSwapAmount.imageset/Contents.json | 12 ++ .../container-token.pdf | Bin 0 -> 8656 bytes .../AssetList/AssetListPresenter.swift | 4 + .../AssetList/AssetListProtocols.swift | 3 + .../AssetList/AssetListViewController.swift | 9 + .../AssetList/AssetListWireframe.swift | 12 ++ .../View/AssetListTotalBalanceCell.swift | 10 ++ .../Setup/Model/MockViewModelFactory.swift | 88 ++++++++++ .../Swaps/Setup/Model/ViewModels.swift | 16 ++ .../Swaps/Setup/SwapSetupInteractor.swift | 7 + .../Swaps/Setup/SwapSetupPresenter.swift | 63 +++++++ .../Swaps/Setup/SwapSetupProtocols.swift | 25 +++ .../Swaps/Setup/SwapSetupViewController.swift | 135 +++++++++++++++ .../Swaps/Setup/SwapSetupViewFactory.swift | 34 ++++ .../Swaps/Setup/SwapSetupWireframe.swift | 3 + .../Swaps/Setup/View/SwapAmountInput.swift | 152 +++++++++++++++++ .../Setup/View/SwapAmountInputView.swift | 143 ++++++++++++++++ .../Swaps/Setup/View/SwapAssetControl.swift | 154 ++++++++++++++++++ .../Swaps/Setup/View/SwapAssetView.swift | 78 +++++++++ .../Setup/View/SwapSetupViewLayout.swift | 71 ++++++++ novawallet/en.lproj/Localizable.strings | 11 ++ novawallet/ru.lproj/Localizable.strings | 11 ++ 26 files changed, 1138 insertions(+), 1 deletion(-) create mode 100644 novawallet/Assets.xcassets/iconActionSwap.imageset/Contents.json create mode 100644 novawallet/Assets.xcassets/iconActionSwap.imageset/container-transaction-type.pdf create mode 100644 novawallet/Assets.xcassets/iconAddSwapAmount.imageset/Contents.json create mode 100644 novawallet/Assets.xcassets/iconAddSwapAmount.imageset/container-token.pdf create mode 100644 novawallet/Modules/Swaps/Setup/Model/MockViewModelFactory.swift create mode 100644 novawallet/Modules/Swaps/Setup/Model/ViewModels.swift create mode 100644 novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift create mode 100644 novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift create mode 100644 novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift create mode 100644 novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift create mode 100644 novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift create mode 100644 novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift create mode 100644 novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift create mode 100644 novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift create mode 100644 novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift create mode 100644 novawallet/Modules/Swaps/Setup/View/SwapAssetView.swift create mode 100644 novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift diff --git a/Podfile.lock b/Podfile.lock index 9500b91c99..7581bee044 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -305,4 +305,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f37e3724d47617fb7ce7ed5e0a583491617b5899 -COCOAPODS: 1.12.1 +COCOAPODS: 1.13.0 diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index a14c89f769..909ad14ae6 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -432,6 +432,7 @@ 3441DDC002503A0DC9A8A925 /* ReferendumSearchViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B179EA3EF793684717BA9D68 /* ReferendumSearchViewFactory.swift */; }; 347BBBBCC84CA155006FDCDB /* GovernanceSelectTracksViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3593D7650F5126266ED9FE84 /* GovernanceSelectTracksViewLayout.swift */; }; 34D6FF85BEA25EFD1D15D460 /* InAppUpdatesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15E09DD01C1CC61EA5CDED9C /* InAppUpdatesInteractor.swift */; }; + 350B8A18C9C91DF07D2E53C5 /* SwapSetupViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD43373A9159E0A592077BB /* SwapSetupViewLayout.swift */; }; 355476A5AECD2FFE4ED3DE39 /* MessageSheetViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A719A9FC28373296AB195CB /* MessageSheetViewLayout.swift */; }; 3592E885646B3ED9F2717412 /* GovernanceRevokeDelegationTracksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CAF58F7D0659E89B66B75E4 /* GovernanceRevokeDelegationTracksViewController.swift */; }; 35F9157CAA182493B2F0E1D3 /* ParaStkRedeemInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53B5C1C920BFDA8F5C9C89D9 /* ParaStkRedeemInteractor.swift */; }; @@ -469,6 +470,7 @@ 3E480EEAF501AEB5D543506D /* UsernameSetupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172B3E9BE51A339D7A09BDA3 /* UsernameSetupPresenter.swift */; }; 3E6215E91AE1C1F78246A43C /* ParaStkUnstakeViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611AD2A7BEEEBA634F56163D /* ParaStkUnstakeViewLayout.swift */; }; 3EAB85420EDDDE7D5B03A1CF /* GovernanceUnavailableTracksWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374AF5133A7FD39B961B9C84 /* GovernanceUnavailableTracksWireframe.swift */; }; + 3EE29545824B68594751769C /* SwapSetupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB494F0B16C9588325CF0D84 /* SwapSetupPresenter.swift */; }; 3F3AE7490C59A0CE0BF2D7A7 /* DAppWalletAuthViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A5B4E6779AA27F10713C6 /* DAppWalletAuthViewFactory.swift */; }; 3F7F10D0E1BDE09CBE64BD2D /* CrowdloanYourContributionsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC155A29FA8777D90A46913D /* CrowdloanYourContributionsViewFactory.swift */; }; 3FC436AED4098456EDEAF484 /* MessageSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A2F34329579E12A2836E77 /* MessageSheetViewController.swift */; }; @@ -544,6 +546,7 @@ 542588DA751A44C993BC1F27 /* ParaStkYourCollatorsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8C13B77688FFF0FFBBB6612 /* ParaStkYourCollatorsWireframe.swift */; }; 5443122935BBFDD55AE9E6FD /* ParitySignerAddressesProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4F3C080F3D5C1E64475903 /* ParitySignerAddressesProtocols.swift */; }; 544C8EB3D71227FAF2FD4658 /* GovernanceRemoveVotesConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29FE1BF422468BECDCDEE63 /* GovernanceRemoveVotesConfirmPresenter.swift */; }; + 54813408A51B4AEBA3EED0A5 /* SwapSetupInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C585109AC3A2580AB1253C31 /* SwapSetupInteractor.swift */; }; 54983C354F7EDCD8014C8371 /* WalletConnectSessionDetailsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE103341935B2A4B8C32B966 /* WalletConnectSessionDetailsViewFactory.swift */; }; 54D334605E9A7C71A4873CFC /* ParaStkRedeemWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 578743E9101B334BFBE44CB6 /* ParaStkRedeemWireframe.swift */; }; 5510625BDA756B939ED7C586 /* AddDelegationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68D44AF5C59681B54ECD7658 /* AddDelegationPresenter.swift */; }; @@ -567,6 +570,7 @@ 59A0AF440ABAAA459EF7D993 /* GovernanceYourDelegationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4462DCD832DB73AA78D44C /* GovernanceYourDelegationsViewController.swift */; }; 59D03D8CB4AE60DE53130729 /* NPoolsRedeemViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EF6FA61F2B7E3B2ADD3200 /* NPoolsRedeemViewLayout.swift */; }; 5AC2A8AD94278DFA4B68A718 /* NominationPoolSearchInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44AA632DE49B746BC38B959F /* NominationPoolSearchInteractor.swift */; }; + 5B02D050E6E067906A05B46B /* SwapSetupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7F910E56C2CC7AA5224BD21 /* SwapSetupWireframe.swift */; }; 5B54978244C37502DD592486 /* NftListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A092ADC09DA0429548EBC08 /* NftListPresenter.swift */; }; 5B652F1E0040F68F835A2F1D /* AssetDetailsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEE05ECCFE3DD11A2EAAF495 /* AssetDetailsViewLayout.swift */; }; 5C796EF8ED29F564B5D1126B /* CrowdloanContributionConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F75722D2F921FD1C2D4105D /* CrowdloanContributionConfirmViewController.swift */; }; @@ -607,6 +611,7 @@ 65C06FCE82EEC0B476DB1CEF /* DAppBrowserProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E38ABE379CA48E63328C4 /* DAppBrowserProtocols.swift */; }; 65CD159259A06EC3E92FD4B0 /* AssetDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 998CDEAB9F149770B27F5317 /* AssetDetailsProtocols.swift */; }; 663DB041307C59E939BF0BE2 /* ParitySignerAddConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44809BCF44D7329266A60A9D /* ParitySignerAddConfirmInteractor.swift */; }; + 66531C7E2E0E99C89A89A35A /* SwapSetupViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A6BCE8F8DB7576E1E5B5974 /* SwapSetupViewFactory.swift */; }; 671C5788468FE8445A46C09F /* AdvancedWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 597A3C3F2937333D0EC7ABD5 /* AdvancedWalletViewController.swift */; }; 67684F7576ED0252C1050CA5 /* OperationDetailsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D569738955713647612599 /* OperationDetailsViewLayout.swift */; }; 676B1511C4A34528C668751D /* GovernanceRevokeDelegationConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B263D5668F1C91E2CF61D9 /* GovernanceRevokeDelegationConfirmWireframe.swift */; }; @@ -687,6 +692,10 @@ 772B1C7D2A93837800A19061 /* StartStakingCustomValidatorListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 772B1C7C2A93837800A19061 /* StartStakingCustomValidatorListWireframe.swift */; }; 7731E9C42A14DA3F0085B5FF /* BorderedActionControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7731E9C32A14DA3F0085B5FF /* BorderedActionControlView.swift */; }; 7738FB6A2A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7738FB692A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift */; }; + 774091F92ACB1F4B00172516 /* SwapAmountInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774091F82ACB1F4B00172516 /* SwapAmountInputView.swift */; }; + 774091FC2ACC053000172516 /* SwapAssetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774091FB2ACC053000172516 /* SwapAssetView.swift */; }; + 774091FE2ACC054B00172516 /* SwapAssetControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774091FD2ACC054B00172516 /* SwapAssetControl.swift */; }; + 774092002ACC1BE400172516 /* SwapAmountInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774091FF2ACC1BE400172516 /* SwapAmountInput.swift */; }; 774A481129F8BFB70094635B /* OperationAuthPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774A481029F8BFB70094635B /* OperationAuthPresentable.swift */; }; 7756927D2A20B88200220756 /* TokenOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7756927C2A20B88200220756 /* TokenOperation.swift */; }; 775F194D2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775F194C2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift */; }; @@ -754,6 +763,8 @@ 77AB55592AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */; }; 77AB555B2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */; }; 77AB555D2AA24BA90058814E /* OperationDetailsPoolRewardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */; }; + 77C9BCBC2ACD1AF500022EA2 /* ViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */; }; + 77C9BCBE2ACD286100022EA2 /* MockViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBD2ACD286100022EA2 /* MockViewModelFactory.swift */; }; 77CB33CE2A38780700B6709A /* structures_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77CB33CD2A38780700B6709A /* structures_output.json */; }; 77CB33D22A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CB33D12A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift */; }; 77CB33D72A3998FD00B6709A /* Array+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CB33D62A3998FC00B6709A /* Array+Sort.swift */; }; @@ -3044,6 +3055,7 @@ 85A093F6055DDD2E2E9253F2 /* ControllerAccountProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F829E7F8B39EE7D977001510 /* ControllerAccountProtocols.swift */; }; 86EB789787B731691B36C827 /* OnChainTransferSetupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1A2F7E5E278FDCC89FE097 /* OnChainTransferSetupPresenter.swift */; }; 873FAB6E5CAD1FD4D02737D0 /* NominationPoolBondMoreSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C9473AB7D1FE4A27403078 /* NominationPoolBondMoreSetupViewController.swift */; }; + 8786222ADF4643BE7A6FBBEB /* SwapSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBEB2BC52266AF493D310834 /* SwapSetupViewController.swift */; }; 879D493C025963619CFADF4F /* GovernanceUnlockSetupProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4642DD5186EFA940518CCB4 /* GovernanceUnlockSetupProtocols.swift */; }; 87F7556E02F6F5BB6F1B1AEA /* ParitySignerTxQrViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A5DCA28ABF42D342BBDF9A /* ParitySignerTxQrViewLayout.swift */; }; 880059D828EEBC0200E87B9B /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880059D728EEBC0200E87B9B /* SliderView.swift */; }; @@ -3355,6 +3367,7 @@ 91A1286763617DE022BD495F /* LedgerInstructionsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B08ACC71BE679A48A7B66E /* LedgerInstructionsPresenter.swift */; }; 921E4891E85C0DC6FDD8A0D0 /* CrowdloanContributionConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1366336078BCA34EFB4C6FF9 /* CrowdloanContributionConfirmInteractor.swift */; }; 924BADB89E7FA2DC54BF1A02 /* NPoolsClaimRewardsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5F1F933F624B01855AA3BA5 /* NPoolsClaimRewardsInteractor.swift */; }; + 92984DFE797C52644C084377 /* SwapSetupProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53775773F2060B4B7F6D62DA /* SwapSetupProtocols.swift */; }; 93434E8E407A6C63D8862A21 /* AssetSelectionProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DECC58C93DB18E79A03B5A0 /* AssetSelectionProtocols.swift */; }; 934F229F4E5A588D5AF2A093 /* TokensAddSelectNetworkViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E800814C025B38C87CC282D /* TokensAddSelectNetworkViewFactory.swift */; }; 9358E048B1AA0F71F519101E /* GovernanceDelegateConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27FB20961F2F221A96624A6 /* GovernanceDelegateConfirmInteractor.swift */; }; @@ -4522,6 +4535,7 @@ 53235E51143C6E93303E30FE /* DAppSearchViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppSearchViewController.swift; sourceTree = ""; }; 534173384708B3CF8F47E4DA /* GovernanceDelegateConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateConfirmPresenter.swift; sourceTree = ""; }; 536D1CA47A5753B9E0389BEA /* ReferendumSearchProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumSearchProtocols.swift; sourceTree = ""; }; + 53775773F2060B4B7F6D62DA /* SwapSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupProtocols.swift; sourceTree = ""; }; 537CCA1F2667A51731C56C88 /* CreateWatchOnlyProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateWatchOnlyProtocols.swift; sourceTree = ""; }; 53A058D4A585F253CBF2968D /* ReferendumVoteSetupViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVoteSetupViewController.swift; sourceTree = ""; }; 53B5C1C920BFDA8F5C9C89D9 /* ParaStkRedeemInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkRedeemInteractor.swift; sourceTree = ""; }; @@ -4605,6 +4619,7 @@ 6A3105383F2825940D0105D5 /* ReferendumVoteSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVoteSetupViewLayout.swift; sourceTree = ""; }; 6A4009AE7EF95E9CE1EB88B8 /* NominationPoolBondMoreBaseWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreBaseWireframe.swift; sourceTree = ""; }; 6A695CA303926DFB5D54E309 /* LedgerAccountConfirmationViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerAccountConfirmationViewLayout.swift; sourceTree = ""; }; + 6A6BCE8F8DB7576E1E5B5974 /* SwapSetupViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupViewFactory.swift; sourceTree = ""; }; 6A7302440137F083F7AEC64E /* NPoolsClaimRewardsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsClaimRewardsViewLayout.swift; sourceTree = ""; }; 6A825B6368073B06F32D7C8F /* StakingMainViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingMainViewFactory.swift; sourceTree = ""; }; 6AD8B98AB03AAF06AA891695 /* TransferConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferConfirmViewLayout.swift; sourceTree = ""; }; @@ -4668,6 +4683,10 @@ 772B1C7C2A93837800A19061 /* StartStakingCustomValidatorListWireframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartStakingCustomValidatorListWireframe.swift; sourceTree = ""; }; 7731E9C32A14DA3F0085B5FF /* BorderedActionControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedActionControlView.swift; sourceTree = ""; }; 7738FB692A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingRelaychainInteractor.swift; sourceTree = ""; }; + 774091F82ACB1F4B00172516 /* SwapAmountInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAmountInputView.swift; sourceTree = ""; }; + 774091FB2ACC053000172516 /* SwapAssetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetView.swift; sourceTree = ""; }; + 774091FD2ACC054B00172516 /* SwapAssetControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetControl.swift; sourceTree = ""; }; + 774091FF2ACC1BE400172516 /* SwapAmountInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAmountInput.swift; sourceTree = ""; }; 774A481029F8BFB70094635B /* OperationAuthPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationAuthPresentable.swift; sourceTree = ""; }; 7756927C2A20B88200220756 /* TokenOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenOperation.swift; sourceTree = ""; }; 775F194C2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoRelaychainPresenter.swift; sourceTree = ""; }; @@ -4735,6 +4754,8 @@ 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashModel.swift; sourceTree = ""; }; 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashViewModel.swift; sourceTree = ""; }; 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsPoolRewardView.swift; sourceTree = ""; }; + 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModels.swift; sourceTree = ""; }; + 77C9BCBD2ACD286100022EA2 /* MockViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockViewModelFactory.swift; sourceTree = ""; }; 77CB33CD2A38780700B6709A /* structures_output.json */ = {isa = PBXFileReference; explicitFileType = text.json; fileEncoding = 4; path = structures_output.json; sourceTree = ""; usesTabs = 0; wrapsLines = 0; }; 77CB33D12A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Web3NameIntegrityVerifierWithCanonicalizationData.swift; sourceTree = ""; }; 77CB33D62A3998FC00B6709A /* Array+Sort.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Sort.swift"; sourceTree = ""; }; @@ -7637,6 +7658,8 @@ BAB2478DE3AF0885A3ED7ED8 /* StakingRedeemPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRedeemPresenter.swift; sourceTree = ""; }; BAF9ED27CF12B7DA8B1378CF /* MarkdownDescriptionViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MarkdownDescriptionViewFactory.swift; sourceTree = ""; }; BB1A1934B76A5DCC65855EE1 /* TransactionHistoryWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransactionHistoryWireframe.swift; sourceTree = ""; }; + BB494F0B16C9588325CF0D84 /* SwapSetupPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupPresenter.swift; sourceTree = ""; }; + BBEB2BC52266AF493D310834 /* SwapSetupViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupViewController.swift; sourceTree = ""; }; BC15D0B7B9F29E97FCECC1D2 /* AssetsSettingsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetsSettingsViewLayout.swift; sourceTree = ""; }; BC216C4DBF86A9F3ADB3AECF /* ParitySignerAddConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerAddConfirmWireframe.swift; sourceTree = ""; }; BC767DE76479F70A8FA3292A /* StakingMoreOptionsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionsViewLayout.swift; sourceTree = ""; }; @@ -7666,8 +7689,10 @@ C4E807E9E12A130C50E8FFDF /* StakingDashboardViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingDashboardViewFactory.swift; sourceTree = ""; }; C503100478AB56E903598A78 /* ReferralCrowdloanPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferralCrowdloanPresenter.swift; sourceTree = ""; }; C52D6675524DB913210F0459 /* DAppSettingsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppSettingsPresenter.swift; sourceTree = ""; }; + C585109AC3A2580AB1253C31 /* SwapSetupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupInteractor.swift; sourceTree = ""; }; C5E9D289393AA2CC1E34C2F4 /* AssetDetailsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetDetailsWireframe.swift; sourceTree = ""; }; C74A2166B054240BD5D925B6 /* UsernameSetupViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UsernameSetupViewFactory.swift; sourceTree = ""; }; + C7F910E56C2CC7AA5224BD21 /* SwapSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupWireframe.swift; sourceTree = ""; }; C80D934D47929D2331111AD7 /* ReferendumFullDetailsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumFullDetailsWireframe.swift; sourceTree = ""; }; C92B3D5B314FB3EAE65FA471 /* StartStakingInfoProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingInfoProtocols.swift; sourceTree = ""; }; C96C3B5ABF4A8124848EFD17 /* ControllerAccountConfirmationWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountConfirmationWireframe.swift; sourceTree = ""; }; @@ -7688,6 +7713,7 @@ CD6B5B187E83839481846C7E /* NftDetailsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsInteractor.swift; sourceTree = ""; }; CD7A6C62EC06FC3B2693FB43 /* GovernanceDelegateSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateSetupViewLayout.swift; sourceTree = ""; }; CDB47990BC7A594E663DAC00 /* ReferendumVoteConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVoteConfirmPresenter.swift; sourceTree = ""; }; + CDD43373A9159E0A592077BB /* SwapSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupViewLayout.swift; sourceTree = ""; }; CE98454DC77EAA01301B9BBF /* ParaStkCollatorFiltersProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkCollatorFiltersProtocols.swift; sourceTree = ""; }; CF389223A781CA2088C7A4DD /* NPoolsClaimRewardsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsClaimRewardsWireframe.swift; sourceTree = ""; }; CF7A019F89C6CD418AEEE79C /* YourWalletsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourWalletsPresenter.swift; sourceTree = ""; }; @@ -8896,6 +8922,21 @@ path = AccountManagement; sourceTree = ""; }; + 29BD7DA0076BA8BC3411221A /* Setup */ = { + isa = PBXGroup; + children = ( + 77C9BCBA2ACD1AE800022EA2 /* Model */, + 774091FA2ACC052400172516 /* View */, + 53775773F2060B4B7F6D62DA /* SwapSetupProtocols.swift */, + C7F910E56C2CC7AA5224BD21 /* SwapSetupWireframe.swift */, + BB494F0B16C9588325CF0D84 /* SwapSetupPresenter.swift */, + C585109AC3A2580AB1253C31 /* SwapSetupInteractor.swift */, + BBEB2BC52266AF493D310834 /* SwapSetupViewController.swift */, + 6A6BCE8F8DB7576E1E5B5974 /* SwapSetupViewFactory.swift */, + ); + path = Setup; + sourceTree = ""; + }; 2A1FB6759F2E8A05A1894287 /* GovernanceUnavailableTracks */ = { isa = PBXGroup; children = ( @@ -9476,6 +9517,18 @@ path = RelaychainStaking; sourceTree = ""; }; + 774091FA2ACC052400172516 /* View */ = { + isa = PBXGroup; + children = ( + 774091F82ACB1F4B00172516 /* SwapAmountInputView.swift */, + 774091FB2ACC053000172516 /* SwapAssetView.swift */, + 774091FD2ACC054B00172516 /* SwapAssetControl.swift */, + 774091FF2ACC1BE400172516 /* SwapAmountInput.swift */, + CDD43373A9159E0A592077BB /* SwapSetupViewLayout.swift */, + ); + path = View; + sourceTree = ""; + }; 775692822A24CA5100220756 /* AssetOperation */ = { isa = PBXGroup; children = ( @@ -9629,6 +9682,23 @@ path = canonicalization; sourceTree = ""; }; + 77C9BCBA2ACD1AE800022EA2 /* Model */ = { + isa = PBXGroup; + children = ( + 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */, + 77C9BCBD2ACD286100022EA2 /* MockViewModelFactory.swift */, + ); + path = Model; + sourceTree = ""; + }; + 77C9BCBF2ACD2E0300022EA2 /* Swaps */ = { + isa = PBXGroup; + children = ( + 29BD7DA0076BA8BC3411221A /* Setup */, + ); + path = Swaps; + sourceTree = ""; + }; 77CB33D32A38894600B6709A /* Integrity */ = { isa = PBXGroup; children = ( @@ -12877,6 +12947,7 @@ 9D97DD4BC9672502D2E2A625 /* TokensManage */, EC1A579A3747EB16688DAEBF /* AssetReceive */, C9850B4B70AEFEABB96269FF /* TransactionHistory */, + 77C9BCBF2ACD2E0300022EA2 /* Swaps */, ); path = Modules; sourceTree = ""; @@ -19308,6 +19379,7 @@ 84FBED052927B1CA00FBEB83 /* EvmEventParser.swift in Sources */, 84A1742428ED3CF70096F943 /* ReferendumLocal.swift in Sources */, 841AB7922993D01A00A362E8 /* GovernanceDelegateConfirmInteractorError.swift in Sources */, + 774091F92ACB1F4B00172516 /* SwapAmountInputView.swift in Sources */, 7728E5912A1324A2007901E0 /* ReferendumsSearchManager.swift in Sources */, 843C49DB24DF373000B71DDA /* AccountImportRequest.swift in Sources */, 843910C1253F36F300E3C217 /* BaseStorageChildSubscription.swift in Sources */, @@ -19321,6 +19393,7 @@ 848CCB4C2833979700A1FD00 /* ParaStkStateViewModelFactory+Alert.swift in Sources */, 8444407528AA628300446D22 /* LedgerAccount.swift in Sources */, 84C74361251E4B5E009576C6 /* FeeType.swift in Sources */, + 774091FE2ACC054B00172516 /* SwapAssetControl.swift in Sources */, 844DAAE128AD106B008E11DA /* UInt+Serialization.swift in Sources */, 84981EE429D3352600948306 /* TransactionHistoryFetching.swift in Sources */, 8428765224ADDE0200D91AD8 /* SettingsPresenter.swift in Sources */, @@ -20357,6 +20430,7 @@ 840D92A1278D8D6F0007B979 /* DAppBrowserStateError.swift in Sources */, 84FC190B29B7DB9F00BCCAA5 /* ExtrinsicServiceTypes.swift in Sources */, 849707A128F3E0AC00DD0A02 /* ReferendumVoterLocal.swift in Sources */, + 774091FC2ACC053000172516 /* SwapAssetView.swift in Sources */, 84754C882510BAFE00854599 /* ModalAlertFactory.swift in Sources */, 8430AACC2602249B005B1066 /* InitialStakingState.swift in Sources */, 0C56B4FB2A4B0C320030F9C9 /* AssetListBaseBuilder.swift in Sources */, @@ -21056,6 +21130,7 @@ F4D0546B2729949100210294 /* MoonbeamMakeSignatureResponse.swift in Sources */, D9046DBA27451ED700C29F2E /* ParallelContributionSource.swift in Sources */, 0CE629DE2AA9B6BF00E250BD /* RewardDestinationViewModelFactory.swift in Sources */, + 77C9BCBE2ACD286100022EA2 /* MockViewModelFactory.swift in Sources */, 844DB624262D9C710025A8F0 /* ErasRewardDistribution.swift in Sources */, 84FAB0632542C8D600319F74 /* ContactItem.swift in Sources */, 06590486EED4050BADDD32C5 /* AccountManagementPresenter.swift in Sources */, @@ -21500,6 +21575,7 @@ 845B821926EF808D00D25C72 /* MetaAccountMapper.swift in Sources */, 19A29027666EB5388CBFAD61 /* StakingRewardDetailsInteractor.swift in Sources */, 846AC7EF2638D9200075F7DA /* YourValidatorTableCell.swift in Sources */, + 77C9BCBC2ACD1AF500022EA2 /* ViewModels.swift in Sources */, C937154FA9021AECD72A871B /* StakingRewardDetailsViewController.swift in Sources */, 84770F27291F7CD400852A33 /* ReferendumDetailsInitData.swift in Sources */, 84B8AA8529F910AD00347A37 /* WalletConnectStateError.swift in Sources */, @@ -22240,6 +22316,7 @@ 84770F25291F72D700852A33 /* ReferendumVotingInitData.swift in Sources */, 84E0C51E29CA40DA000B65C8 /* OperationContractCallModel.swift in Sources */, 846A835D28B8D09600D92892 /* LedgerMessageSheetViewFactory.swift in Sources */, + 774092002ACC1BE400172516 /* SwapAmountInput.swift in Sources */, 60FFEE5B386E82D70333BE80 /* CreateWatchOnlyPresenter.swift in Sources */, 842E9EA02A2DC23900759972 /* StakingDashboardBuilderProtocol.swift in Sources */, 454D41CC5C7CC2FDAB778026 /* CreateWatchOnlyInteractor.swift in Sources */, @@ -22875,6 +22952,13 @@ EB877554208E91A80985F1E5 /* NPoolsRedeemViewController.swift in Sources */, 59D03D8CB4AE60DE53130729 /* NPoolsRedeemViewLayout.swift in Sources */, F3719F7C2AD0B75FC271DCE9 /* NPoolsRedeemViewFactory.swift in Sources */, + 92984DFE797C52644C084377 /* SwapSetupProtocols.swift in Sources */, + 5B02D050E6E067906A05B46B /* SwapSetupWireframe.swift in Sources */, + 3EE29545824B68594751769C /* SwapSetupPresenter.swift in Sources */, + 54813408A51B4AEBA3EED0A5 /* SwapSetupInteractor.swift in Sources */, + 8786222ADF4643BE7A6FBBEB /* SwapSetupViewController.swift in Sources */, + 350B8A18C9C91DF07D2E53C5 /* SwapSetupViewLayout.swift in Sources */, + 66531C7E2E0E99C89A89A35A /* SwapSetupViewFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/novawallet/Assets.xcassets/iconActionSwap.imageset/Contents.json b/novawallet/Assets.xcassets/iconActionSwap.imageset/Contents.json new file mode 100644 index 0000000000..96847b1a57 --- /dev/null +++ b/novawallet/Assets.xcassets/iconActionSwap.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "container-transaction-type.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconActionSwap.imageset/container-transaction-type.pdf b/novawallet/Assets.xcassets/iconActionSwap.imageset/container-transaction-type.pdf new file mode 100644 index 0000000000000000000000000000000000000000..53e799c5e24cec49b702cc85ecb4e0fed4bbcd74 GIT binary patch literal 3770 zcmc&%O>f*r487}D%q2i_h;}$%oB;v@v7H!28^pDH2oUtJSu2jy^{%^;i=@B4k0WU$ zd!42(P*ewd^fV-gkK~a1(jNNKhG@VQdz#S69gAiwwDvP>^{4t=}jroCD2n$7C#o$m(+gR1k}4_!9;db_(G zu(PJ!-tJaSn@zm>(w&@l%dY9)O#Zi8DqY8@@u*SPWO{$KctwwwJ2)u17l()2FX}yv zib)tP3u{JbTKx(~aH_W3HOuR2aoqfIwQ9~z4%N~5{B6}%zgKdy%eJCXOHus1tjf1o zQ%cE`B3+X+$vL&UotNIxm`jv;Nss>`X`CUSu*--l5=49JM9xQYma;CRHBJO9 z1j9jRU4l7QsSaeE75#9=IA@~b+F47%14Ha}VrF25qKTr!v9WLth}IxSj9ZpZTw6|* zZGtwYm;`e+L=yeNvrRY@z~Cu3tpxHuL4@lRgR!EY3?*Y#gvAs=!3w7!PNEH1^d3|R z8uf+WH&ECL7U$Z{zNZJ-j&!GJKRZT233C947gZ~9$A zrC}h!aOe<>*`bWvD#EW=j=ECGW%hG z|3UVX!#RDR0D294&<5!b5N;Tjw08(|h>AP{%mG;l0g#rAKvoiy0D&zN6Wj)fTqM+> z=yL)3kfI(0C1M8>VNXjy@*^3oI;2dfx0_X&!+E$WlUhfv^`|5agJo0`M-uEsU<9%O)L=u>+w}6AXgc0~z>?bqZFa zXnQ&nF0U$~xMPF^nN6#+_ar5Z1Xrd8F&W5o59_eQ8t2#7@qbFNKiG)t8NOWvXC zu@AB_A|EAmxNdRF2&$bI>shCuNCI(*ShIlpsX3KK)odS*SBpWhp z3;dl9?*69^KKAGTu7^&o5<|_ND(mo4HVHBUq7*|J&LieZ> zgx4WyC*_mGgdszUo(Wx7lcEWoRu6rh^q?=7A!I_)iwZg%&Z|k$1AY}~e#ipKF8u7(Q$drM%7_R%~RbwRGTFCG_Ktn8vEhz{LQlc zEerH>uDsrvO84i+9$B0ysj^}G{noAlUtsQixxV?d{J!e3G9J`84CBRbxnR45y- z1dJ;)riKBYM}{6VmM%c;@fNN6f%`}l4p9olG)BCc>6e?$wrkaU8Hn)=E}bqfn*n&! zTwE<5J6emT={JI;#rVVIXuIi}4J--+BX}ymKRShRG($6<1nNDItMkXJ3mjF62WM(L z@zM4cvb1;p7RXGKwA+~~@vtoxC(n9P%FI&@V@*+byrw&8`x9 ze>euYPuuNv^GrRxx_H)8yg1u#!LQJlS;7fqCKt`gj6i@_c?8I1XgSz$t8=&ZeA}jWEXGZRfrbxEvf8Xzr z)HxiFoKY@sn+Xt8pU89P;o;Gf%kx*y^rDI)trFG0{XJ6ZhaXh@Q@6Pt?ot?x`Br{HVwcoVEvfupc!+oc6tb6`E+6~*TSx0{< zu-B(IL?q+{av-7ObwYnYDUvAQUe?Kuen7rG+6*brUl`w5? zZrtKi^SxLz=d<8E|8KQ?ryP^KDvM&S27dBv4IZCAt-)jSKdOPL$|}uH`e43mt#!ze ze@ETs#s>#R6u=P$t&8L$OX$5tGe(2>&S%B<7?@unb`wK;XCj0S<8vaq>M}8Tsa!OA zGP9%QAdo0Fo+XQ_uHh9=Fn3#eir}R4u;l|yKI#s#v2#b7*6fagQF6zN7ybJ%CvZl| zCq|uD1KI{AK&2Ca9-YKlwTn-`k_^e;`+w1$L?Nof;{kd$ri*574W!Z4z$#Fd9 zTSZ&MXZw?@!U^%|jS)^iIUX%(4}kP@RC@V-z`lq1hwZ*||9!HNaq66u)J}+f=zWs7 zv7J3Gox|z*a~yDfA87>9OTpvma4PsD12;p(eZvjaD z1eMYFwhZ2eobRGBh7@cWUuv~$H_^Lj6HT0q6c{cQXI0LrxMEboMBIpI8>>O{RpW{S zXDX7v<@Uqees%HU;ijFDoFuhVb^k53k0u*KZ+w3A@3^;YZku}&eP(K(@^6fT?BaT8 zhR&w&m#O{JHgq8=Z6!HJmH4Hrhpt~YyZ;1d0XOcMO&*+&^n{fD2mX-*Z@=WE z9OF1VBjk*c^W^{U^rVyWmv5DXYNNa8-`~R+6m993MO(74xhX-GDmC3(F{n(E)u}B` zaPcbKbu$b+LF+}DC3$XChGk`46u8hIuVAQ+gbXT2PkKdlRhDX#;nK$_V&1St@zjA~VHR1t3UhnZA@YUm?B99mrXuOksz&|t)YBbaF3(srNV zC8&rxj%@jTv_@8h77?B#ze_N`U|-F;=xDx5$Ou;*ZjEunlc`a?uMz9ID3T1yT;t4rSg^`tx8gjrOS)wcAWc5Hp+Uf=iI^Cdek<>~e(6^&Ps6`fm1#|c#oCmL_ zxVDn1969ob(S>)B=SzOmiMA&70(zaGGwUG`D@<43Ba6Wb)?g`|$wpcs1nbgdXr&7X zb+b&Tkk7iMMjsyDg;NdW^A;kzAbK+c2YW|XxRQrqPWI8bp^TSqEVbtY1rCGGJT*SvVmyAtghhkPOnM^bJ48cyRnG6pyC~_|Z7Gaq0p+uO!YX zi3Ne~n`rjN{YES5vt98e_Tkn33$N(7l+!MocPXcS?~kkdyXN$@c-$D1cka2j?6GeR zV&msH99E&JeCL|7r?b(lo3Xza$1v`Bk89jSMzR#Wr%K^2AEhWG{kYlm1HRUbUzS`e z`?9(1oPpxk-OaN3vTsy2HoSZA@n(Y$6kuu1Frt^#eIJw_yDOo&2y<$X!2ev@k4xHq zzZ}0%f$E-?dznRv9gvWgw1J5hsR=d$XHe6_A~vj@O?Tf z6mb8e?Lj|B)| u?mP8#dGnp~YJAoAa4C7rMoE0V{BK9rhd*~ivmLBIOq%QH$&;5qzxp3l@&K&> literal 0 HcmV?d00001 diff --git a/novawallet/Modules/AssetList/AssetListPresenter.swift b/novawallet/Modules/AssetList/AssetListPresenter.swift index d4260a4dd5..9cb7466717 100644 --- a/novawallet/Modules/AssetList/AssetListPresenter.swift +++ b/novawallet/Modules/AssetList/AssetListPresenter.swift @@ -429,6 +429,10 @@ extension AssetListPresenter: AssetListPresenterProtocol { wireframe.showBuyTokens(from: view) } + func swap() { + wireframe.showSwapTokens(from: view) + } + func presentWalletConnect() { if walletConnectSessionsCount > 0 { wireframe.showWalletConnect(from: view) diff --git a/novawallet/Modules/AssetList/AssetListProtocols.swift b/novawallet/Modules/AssetList/AssetListProtocols.swift index 3794b0e1b0..02aff21019 100644 --- a/novawallet/Modules/AssetList/AssetListProtocols.swift +++ b/novawallet/Modules/AssetList/AssetListProtocols.swift @@ -23,6 +23,7 @@ protocol AssetListPresenterProtocol: AnyObject { func send() func receive() func buy() + func swap() func presentWalletConnect() } @@ -73,6 +74,8 @@ protocol AssetListWireframeProtocol: AnyObject, WalletSwitchPresentable, AlertPr ) func showBuyTokens(from view: AssetListViewProtocol?) + + func showSwapTokens(from view: AssetListViewProtocol?) } typealias WalletConnectSessionsError = WalletConnectSessionsInteractorError diff --git a/novawallet/Modules/AssetList/AssetListViewController.swift b/novawallet/Modules/AssetList/AssetListViewController.swift index d166d3367a..2a3a996dfa 100644 --- a/novawallet/Modules/AssetList/AssetListViewController.swift +++ b/novawallet/Modules/AssetList/AssetListViewController.swift @@ -122,6 +122,10 @@ final class AssetListViewController: UIViewController, ViewHolder { @objc private func actionBuy() { presenter.buy() } + + @objc private func actionSwap() { + presenter.swap() + } } extension AssetListViewController: UICollectionViewDelegateFlowLayout { @@ -267,6 +271,11 @@ extension AssetListViewController: UICollectionViewDataSource { action: #selector(actionBuy), for: .touchUpInside ) + totalBalanceCell.swapButton.addTarget( + self, + action: #selector(actionSwap), + for: .touchUpInside + ) if let viewModel = headerViewModel { totalBalanceCell.bind(viewModel: viewModel) } diff --git a/novawallet/Modules/AssetList/AssetListWireframe.swift b/novawallet/Modules/AssetList/AssetListWireframe.swift index 4839d46da8..3d1ebef65f 100644 --- a/novawallet/Modules/AssetList/AssetListWireframe.swift +++ b/novawallet/Modules/AssetList/AssetListWireframe.swift @@ -127,6 +127,18 @@ final class AssetListWireframe: AssetListWireframeProtocol { view?.controller.present(navigationController, animated: true, completion: nil) } + func showSwapTokens(from view: AssetListViewProtocol?) { + guard let swapTokensView = SwapSetupViewFactory.createView() else { + return + } + + let navigationController = NovaNavigationController( + rootViewController: swapTokensView.controller + ) + + view?.controller.present(navigationController, animated: true, completion: nil) + } + func showNfts(from view: AssetListViewProtocol?) { guard let nftListView = NftListViewFactory.createView() else { return diff --git a/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift b/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift index 8a8300567f..93546d03ab 100644 --- a/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift +++ b/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift @@ -51,6 +51,12 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { R.string.localizable.walletAssetReceive(preferredLanguages: locale.rLanguages), icon: R.image.iconReceive() ) + lazy var swapButton = createActionButton( + title: R.string.localizable.walletAssetsSwap( + preferredLanguages: locale.rLanguages + ), + icon: R.image.iconActionChange() + ) lazy var buyButton = createActionButton( title: R.string.localizable.walletAssetBuy( preferredLanguages: locale.rLanguages @@ -63,6 +69,7 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { [ sendButton, receiveButton, + swapButton, buyButton ] ) @@ -206,6 +213,9 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { buyButton.imageWithTitleView?.title = R.string.localizable.walletAssetBuy( preferredLanguages: locale.rLanguages ) + swapButton.imageWithTitleView?.title = R.string.localizable.walletAssetsSwap( + preferredLanguages: locale.rLanguages + ) } private func setupLayout() { diff --git a/novawallet/Modules/Swaps/Setup/Model/MockViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/MockViewModelFactory.swift new file mode 100644 index 0000000000..a97fd2f2f2 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/Model/MockViewModelFactory.swift @@ -0,0 +1,88 @@ +import SoraFoundation + +final class MockViewModelFactory { + func buttonState() -> ButtonState { + .init( + title: .init { + R.string.localizable.swapsSetupAssetActionSelectReceive(preferredLanguages: $0.rLanguages) + }, + enabled: false + ) + } + + func payTitleModel(locale: Locale) -> TitleHorizontalMultiValueView.Model { + TitleHorizontalMultiValueView.Model( + title: + R.string.localizable.swapsSetupAssetSelectPayTitle(preferredLanguages: locale.rLanguages), + subtitle: + R.string.localizable.swapsSetupAssetMax( + preferredLanguages: locale.rLanguages + ), + value: "100 DOT" + ) + } + + func payModel() -> SwapAssetInputViewModel { + let dotImage = RemoteImageViewModel(url: URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/chains/white/Polkadot.svg")!) + let hubImage = RemoteImageViewModel(url: URL(string: "https://parachains.info/images/parachains/1688559044_assethub.svg")!) + return .asset(SwapsAssetViewModel( + symbol: "DOT", + imageViewModel: dotImage, + hub: .init( + name: "Polkadot Asset Hub", + icon: hubImage + ) + )) + } + + func payPriceModel() -> String? { + "$0" + } + + func receiveTitleModel(locale: Locale) -> TitleHorizontalMultiValueView.Model { + TitleHorizontalMultiValueView.Model( + title: + R.string.localizable.swapsSetupAssetSelectReceiveTitle(preferredLanguages: locale.rLanguages), + subtitle: "", + value: "" + ) + } + + func receiveModel(locale: Locale) -> SwapAssetInputViewModel { + .empty(emptyReceiveViewModel(locale: locale)) + } + + func payAmount( + locale: Locale, + balanceViewModelFactory: BalanceViewModelFactoryFacadeProtocol + ) -> AmountInputViewModelProtocol { + let targetAssetInfo = AssetBalanceDisplayInfo( + displayPrecision: 2, + assetPrecision: 10, + symbol: "DOT", + symbolValueSeparator: "", + symbolPosition: .suffix, + icon: nil + ) + return balanceViewModelFactory.createBalanceInputViewModel( + targetAssetInfo: targetAssetInfo, + amount: nil + ).value(for: locale) + } + + func emptyPayViewModel(locale: Locale) -> EmptySwapsAssetViewModel { + EmptySwapsAssetViewModel( + imageViewModel: StaticImageViewModel(image: R.image.iconAddSwapAmount()!), + title: R.string.localizable.swapsSetupAssetPayTitle(preferredLanguages: locale.rLanguages), + subtitle: R.string.localizable.swapsSetupAssetSelectSubtitle(preferredLanguages: locale.rLanguages) + ) + } + + func emptyReceiveViewModel(locale: Locale) -> EmptySwapsAssetViewModel { + EmptySwapsAssetViewModel( + imageViewModel: StaticImageViewModel(image: R.image.iconAddSwapAmount()!), + title: R.string.localizable.swapsSetupAssetReceiveTitle(preferredLanguages: locale.rLanguages), + subtitle: R.string.localizable.swapsSetupAssetSelectSubtitle(preferredLanguages: locale.rLanguages) + ) + } +} diff --git a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift new file mode 100644 index 0000000000..5ed2476be8 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift @@ -0,0 +1,16 @@ +struct SwapsAssetViewModel { + let symbol: String + let imageViewModel: ImageViewModelProtocol? + let hub: NetworkViewModel +} + +struct EmptySwapsAssetViewModel { + let imageViewModel: ImageViewModelProtocol? + let title: String + let subtitle: String +} + +enum SwapAssetInputViewModel { + case asset(SwapsAssetViewModel) + case empty(EmptySwapsAssetViewModel) +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift new file mode 100644 index 0000000000..0b4035f0f2 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -0,0 +1,7 @@ +import UIKit + +final class SwapSetupInteractor { + weak var presenter: SwapSetupInteractorOutputProtocol? +} + +extension SwapSetupInteractor: SwapSetupInteractorInputProtocol {} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift new file mode 100644 index 0000000000..887cc55dd5 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -0,0 +1,63 @@ +import Foundation +import SoraFoundation + +final class SwapSetupPresenter { + weak var view: SwapSetupViewProtocol? + let wireframe: SwapSetupWireframeProtocol + let interactor: SwapSetupInteractorInputProtocol + let balanceViewModelFactory: BalanceViewModelFactoryFacadeProtocol + + init( + interactor: SwapSetupInteractorInputProtocol, + wireframe: SwapSetupWireframeProtocol, + balanceViewModelFactory: BalanceViewModelFactoryFacadeProtocol, + localizationManager: LocalizationManagerProtocol + ) { + self.interactor = interactor + self.wireframe = wireframe + self.balanceViewModelFactory = balanceViewModelFactory + self.localizationManager = localizationManager + } +} + +extension SwapSetupPresenter: SwapSetupPresenterProtocol { + func setup() { + let mock = MockViewModelFactory() + let buttonState = mock.buttonState() + view?.didReceiveButtonState( + title: buttonState.title.value(for: selectedLocale), + enabled: buttonState.enabled + ) + view?.didReceiveTitle(payViewModel: mock.payTitleModel(locale: selectedLocale)) + view?.didReceiveInputChainAsset(payViewModel: mock.payModel()) + view?.didReceiveAmount(payInputViewModel: mock.payAmount( + locale: selectedLocale, + balanceViewModelFactory: balanceViewModelFactory + )) + view?.didReceiveAmountInputPrice(payViewModel: mock.payPriceModel()) + view?.didReceiveTitle(receiveViewModel: mock.receiveTitleModel(locale: selectedLocale)) + view?.didReceiveInputChainAsset(receiveViewModel: mock.receiveModel(locale: selectedLocale)) + } + + // TODO: navigate to select token screen + func selectPayToken() {} + + // TODO: navigate to select token screen + func selectReceiveToken() {} + + // TODO: implement + func swap() {} + + // TODO: navigate to confirm screen + func proceed() {} +} + +extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol {} + +extension SwapSetupPresenter: Localizable { + func applyLocalization() { + if view?.isSetup == true { + setup() + } + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift new file mode 100644 index 0000000000..46c4b6656c --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -0,0 +1,25 @@ +protocol SwapSetupViewProtocol: ControllerBackedProtocol { + func didReceiveButtonState(title: String, enabled: Bool) + func didReceiveInputChainAsset(payViewModel viewModel: SwapAssetInputViewModel) + func didReceiveAmount(payInputViewModel inputViewModel: AmountInputViewModelProtocol) + func didReceiveAmountInputPrice(payViewModel: String?) + func didReceiveTitle(payViewModel viewModel: TitleHorizontalMultiValueView.Model) + func didReceiveInputChainAsset(receiveViewModel viewModel: SwapAssetInputViewModel) + func didReceiveAmount(receiveInputViewModel inputViewModel: AmountInputViewModelProtocol) + func didReceiveAmountInputPrice(receiveViewModel: String?) + func didReceiveTitle(receiveViewModel viewModel: TitleHorizontalMultiValueView.Model) +} + +protocol SwapSetupPresenterProtocol: AnyObject { + func setup() + func selectPayToken() + func selectReceiveToken() + func proceed() + func swap() +} + +protocol SwapSetupInteractorInputProtocol: AnyObject {} + +protocol SwapSetupInteractorOutputProtocol: AnyObject {} + +protocol SwapSetupWireframeProtocol: AnyObject {} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift new file mode 100644 index 0000000000..b30870e8de --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -0,0 +1,135 @@ +import UIKit +import SoraFoundation + +final class SwapSetupViewController: UIViewController, ViewHolder { + typealias RootViewType = SwapSetupViewLayout + + let presenter: SwapSetupPresenterProtocol + + init( + presenter: SwapSetupPresenterProtocol, + localizationManager: LocalizationManager + ) { + self.presenter = presenter + super.init(nibName: nil, bundle: nil) + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = SwapSetupViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupHandlers() + setupLocalization() + presenter.setup() + } + + private func setupHandlers() { + rootView.payAmountInputView.assetControl.addTarget( + self, + action: #selector(selectPayTokenAction), + for: .touchUpInside + ) + rootView.receiveAmountInputView.assetControl.addTarget( + self, + action: #selector(selectReceiveTokenAction), + for: .touchUpInside + ) + rootView.actionButton.addTarget( + self, + action: #selector(continueAction), + for: .touchUpInside + ) + rootView.switchButton.addTarget( + self, + action: #selector(swapAction), + for: .touchUpInside + ) + } + + private func setupLocalization() { + title = R.string.localizable.walletAssetsSwap(preferredLanguages: selectedLocale.rLanguages) + } + + @objc private func selectPayTokenAction() { + rootView.receiveAmountInputView.endEditing(true) + presenter.selectPayToken() + } + + @objc private func selectReceiveTokenAction() { + rootView.payAmountInputView.endEditing(true) + presenter.selectReceiveToken() + } + + @objc private func continueAction() { + presenter.proceed() + } + + @objc private func swapAction() { + presenter.swap() + } +} + +extension SwapSetupViewController: SwapSetupViewProtocol { + func didReceiveButtonState(title: String, enabled: Bool) { + rootView.actionButton.applyState(title: title, enabled: enabled) + } + + func didReceiveTitle(payViewModel viewModel: TitleHorizontalMultiValueView.Model) { + rootView.payAmountView.bind(model: viewModel) + } + + func didReceiveInputChainAsset(payViewModel viewModel: SwapAssetInputViewModel) { + switch viewModel { + case let .asset(assetViewModel): + rootView.payAmountInputView.bind(assetViewModel: assetViewModel) + case let .empty(emptySwapsAssetViewModel): + rootView.payAmountInputView.bind(emptyViewModel: emptySwapsAssetViewModel) + } + } + + func didReceiveAmount(payInputViewModel inputViewModel: AmountInputViewModelProtocol) { + rootView.payAmountInputView.bind(inputViewModel: inputViewModel) + } + + func didReceiveAmountInputPrice(payViewModel viewModel: String?) { + rootView.payAmountInputView.bind(priceViewModel: viewModel) + } + + func didReceiveTitle(receiveViewModel viewModel: TitleHorizontalMultiValueView.Model) { + rootView.receiveAmountView.bind(model: viewModel) + } + + func didReceiveInputChainAsset(receiveViewModel viewModel: SwapAssetInputViewModel) { + switch viewModel { + case let .asset(swapsAssetViewModel): + rootView.receiveAmountInputView.bind(assetViewModel: swapsAssetViewModel) + case let .empty(emptySwapsAssetViewModel): + rootView.receiveAmountInputView.bind(emptyViewModel: emptySwapsAssetViewModel) + } + } + + func didReceiveAmount(receiveInputViewModel inputViewModel: AmountInputViewModelProtocol) { + rootView.receiveAmountInputView.bind(inputViewModel: inputViewModel) + } + + func didReceiveAmountInputPrice(receiveViewModel viewModel: String?) { + rootView.receiveAmountInputView.bind(priceViewModel: viewModel) + } +} + +extension SwapSetupViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift new file mode 100644 index 0000000000..0639b7782e --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -0,0 +1,34 @@ +import Foundation +import SoraFoundation + +struct SwapSetupViewFactory { + static func createView() -> SwapSetupViewProtocol? { + guard + let currencyManager = CurrencyManager.shared else { + return nil + } + + let balanceViewModelFactory = BalanceViewModelFactoryFacade( + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager)) + + let interactor = SwapSetupInteractor() + let wireframe = SwapSetupWireframe() + + let presenter = SwapSetupPresenter( + interactor: interactor, + wireframe: wireframe, + balanceViewModelFactory: balanceViewModelFactory, + localizationManager: LocalizationManager.shared + ) + + let view = SwapSetupViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + interactor.presenter = presenter + + return view + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift new file mode 100644 index 0000000000..d397c0756f --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -0,0 +1,3 @@ +import Foundation + +final class SwapSetupWireframe: SwapSetupWireframeProtocol {} diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift b/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift new file mode 100644 index 0000000000..ad1d8a99b4 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift @@ -0,0 +1,152 @@ +import SoraUI + +final class SwapAmountInput: BackgroundedContentControl { + let textField: UITextField = .create { + $0.font = .title2 + $0.textColor = R.color.colorTextPrimary() + $0.tintColor = R.color.colorTextPrimary() + $0.textAlignment = .right + $0.attributedPlaceholder = NSAttributedString( + string: "0", + attributes: [ + .foregroundColor: R.color.colorHintText()!, + .font: UIFont.title2 + ] + ) + $0.keyboardType = .decimalPad + } + + let priceLabel = UILabel( + style: .footnoteSecondary, + textAlignment: .right, + numberOfLines: 1 + ) + + let spacing: CGFloat = 4 + + override var intrinsicContentSize: CGSize { + let contentHeight = textField.intrinsicContentSize.height + spacing + priceLabel.intrinsicContentSize.height + + let height = contentInsets.top + contentHeight + contentInsets.bottom + + return CGSize(width: UIView.noIntrinsicMetric, height: height) + } + + private(set) var inputViewModel: AmountInputViewModelProtocol? + + var completed: Bool { + inputViewModel?.isValid == true + } + + var hasValidNumber: Bool { + inputViewModel?.decimalAmount != nil + } + + override public init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + contentView?.frame = bounds + + layoutContent() + } + + private func layoutContent() { + let availableWidth = bounds.width - contentInsets.left - contentInsets.right + let textFieldWidth = max(availableWidth, 0) + + let textFieldHeight: CGFloat = textField.intrinsicContentSize.height + let textFieldY: CGFloat + + if !priceLabel.text.isNilOrEmpty { + textFieldY = bounds.midY - textFieldHeight + spacing + } else { + textFieldY = bounds.midY - textFieldHeight / 2 + } + + textField.frame = CGRect( + x: bounds.maxX - contentInsets.right - textFieldWidth, + y: textFieldY, + width: textFieldWidth, + height: textFieldHeight + ) + + priceLabel.frame = CGRect( + x: bounds.maxX - contentInsets.right - textFieldWidth, + y: textField.frame.maxY, + width: textFieldWidth, + height: priceLabel.intrinsicContentSize.height + ) + } + + private func configure() { + backgroundColor = UIColor.clear + + configureContentViewIfNeeded() + configureLocalHandlers() + } + + private func configureLocalHandlers() { + addTarget(self, action: #selector(actionTouchUpInside), for: .touchUpInside) + } + + private func configureContentViewIfNeeded() { + if contentView == nil { + let contentView = UIView() + contentView.backgroundColor = .clear + contentView.isUserInteractionEnabled = false + self.contentView = contentView + } + + contentView?.addSubview(priceLabel) + contentView?.addSubview(textField) + } + + @objc private func actionTouchUpInside() { + textField.becomeFirstResponder() + } +} + +extension SwapAmountInput: UITextFieldDelegate { + func textField( + _: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + inputViewModel?.didReceiveReplacement(string, for: range) ?? false + } +} + +extension SwapAmountInput: AmountInputViewModelObserver { + func amountInputDidChange() { + textField.text = inputViewModel?.displayAmount + + sendActions(for: .editingChanged) + } +} + +extension SwapAmountInput { + func bind(inputViewModel: AmountInputViewModelProtocol) { + textField.isHidden = false + self.inputViewModel?.observable.remove(observer: self) + inputViewModel.observable.add(observer: self) + + self.inputViewModel = inputViewModel + textField.text = inputViewModel.displayAmount + } + + func bind(priceViewModel: String?) { + priceLabel.text = priceViewModel + + setNeedsLayout() + } +} diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift new file mode 100644 index 0000000000..1ea0ebe2f9 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift @@ -0,0 +1,143 @@ +import SoraUI + +final class SwapAmountInputView: RoundedView { + let assetControl = SwapAssetControl() + let textInputView = SwapAmountInput() + + var contentInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 16) { + didSet { + setNeedsLayout() + } + } + + var horizontalSpacing: CGFloat = 8 { + didSet { + setNeedsLayout() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var intrinsicContentSize: CGSize { + let leftContentHeight = assetControl.intrinsicContentSize.height + let rightContentHeight = textInputView.intrinsicContentSize.height + + let contentHeight = max(leftContentHeight, rightContentHeight) + + let height = contentInsets.top + contentHeight + contentInsets.bottom + + return CGSize(width: UIView.noIntrinsicMetric, height: height) + } + + // MARK: Layout + + override func layoutSubviews() { + super.layoutSubviews() + + assetControl.frame = swapAssetControlFrame(bounds: bounds) + textInputView.frame = inputViewFrame( + bounds: bounds, + assetControlFrame: assetControl.frame + ) + } + + private func swapAssetControlFrame(bounds: CGRect) -> CGRect { + let availableWidth = bounds.width - contentInsets.left - contentInsets.right + let swapAssetControlSize = assetControl.intrinsicContentSize + + let width = textInputView.isHidden ? availableWidth : + min(min(availableWidth, swapAssetControlSize.width), 0.7 * availableWidth) + + return CGRect( + x: contentInsets.left, + y: bounds.midY - swapAssetControlSize.height / 2.0, + width: width, + height: swapAssetControlSize.height + ) + } + + private func inputViewFrame( + bounds: CGRect, + assetControlFrame: CGRect + ) -> CGRect { + let estimatedInputViewWidth = bounds.maxX - contentInsets.right - assetControlFrame.maxX - horizontalSpacing + let inputWidth = max(estimatedInputViewWidth, 0) + let inputSize = textInputView.intrinsicContentSize + + return CGRect( + x: bounds.maxX - contentInsets.right - inputWidth, + y: bounds.midY - inputSize.height / 2.0, + width: inputWidth, + height: inputSize.height + ) + } + + // MARK: Configure + + override func configure() { + super.configure() + + backgroundColor = UIColor.clear + apply(style: .strokeOnEditing) + + configureContent() + configureInputViewHandlers() + } + + private func configureContent() { + addSubview(assetControl) + addSubview(textInputView) + + assetControl.contentInsets = .zero + textInputView.contentInsets = .zero + } + + private func configureInputViewHandlers() { + textInputView.textField.addTarget( + self, + action: #selector(actionEditingDidBeginEnd), + for: .editingDidBegin + ) + + textInputView.textField.addTarget( + self, + action: #selector(actionEditingDidBeginEnd), + for: .editingDidEnd + ) + } + + @objc private func actionEditingDidBeginEnd() { + strokeWidth = textInputView.textField.isFirstResponder ? 0.5 : 0.0 + } +} + +extension SwapAmountInputView { + func bind(assetViewModel: SwapsAssetViewModel) { + assetControl.bind(assetViewModel: assetViewModel) + setNeedsLayout() + } + + func bind(emptyViewModel: EmptySwapsAssetViewModel) { + assetControl.bind(emptyViewModel: emptyViewModel) + textInputView.isHidden = true + setNeedsLayout() + } + + func bind(inputViewModel: AmountInputViewModelProtocol) { + textInputView.isHidden = false + textInputView.bind(inputViewModel: inputViewModel) + } + + func bind(priceViewModel: String?) { + textInputView.bind(priceViewModel: priceViewModel) + setNeedsLayout() + } +} diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift b/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift new file mode 100644 index 0000000000..a19e832559 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift @@ -0,0 +1,154 @@ +import UIKit +import SoraUI + +final class SwapAssetControl: BackgroundedContentControl { + var iconView: AssetIconView { lazyIconViewOrCreateIfNeeded() } + private var lazyIconView: AssetIconView? + + let assetView = SwapAssetView() + + var iconViewContentInsets = UIEdgeInsets.zero { + didSet { + lazyIconView?.contentInsets = iconViewContentInsets + setNeedsLayout() + } + } + + var horizontalSpacing: CGFloat = 8 { + didSet { + setNeedsLayout() + } + } + + var iconRadius: CGFloat = 16 { + didSet { + lazyIconView?.backgroundView.cornerRadius = iconRadius + + setNeedsLayout() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var intrinsicContentSize: CGSize { + let contentHeight = max( + lazyIconView?.intrinsicContentSize.height ?? 0.0, + assetView.intrinsicContentSize.height + ) + + let iconWidth = lazyIconView.map { $0.intrinsicContentSize.width + horizontalSpacing } ?? 0 + let contentWidth = iconWidth + assetView.intrinsicContentSize.width + + let height = contentInsets.top + contentHeight + contentInsets.bottom + let width = contentInsets.left + contentWidth + contentInsets.right + + return CGSize(width: width, height: height) + } + + override func layoutSubviews() { + super.layoutSubviews() + + contentView?.frame = bounds + + layoutContent() + } + + private func layoutContent() { + let availableWidth = bounds.width - contentInsets.left - contentInsets.right + let assetViewSize = assetView.intrinsicContentSize + + if let iconView = lazyIconView { + iconView.frame = CGRect( + x: bounds.minX + contentInsets.left, + y: bounds.midY - iconRadius, + width: 2.0 * iconRadius, + height: 2.0 * iconRadius + ) + + assetView.frame = CGRect( + x: iconView.frame.maxX + horizontalSpacing, + y: bounds.midY - assetViewSize.height / 2.0, + width: min(availableWidth, assetViewSize.width), + height: assetViewSize.height + ) + } else { + assetView.frame = CGRect( + x: contentInsets.left, + y: bounds.midY - assetViewSize.height / 2.0, + width: min(availableWidth, assetViewSize.width), + height: assetViewSize.height + ) + } + } + + private func configure() { + backgroundColor = UIColor.clear + + if contentView == nil { + let contentView = UIView() + contentView.backgroundColor = .clear + contentView.isUserInteractionEnabled = false + self.contentView = contentView + } + + contentView?.addSubview(assetView) + } + + private func lazyIconViewOrCreateIfNeeded() -> AssetIconView { + if let iconView = lazyIconView { + return iconView + } + + let size = 2 * iconRadius + let initFrame = CGRect(origin: .zero, size: .init(width: size, height: size)) + let imageView = AssetIconView(frame: initFrame) + imageView.contentInsets = iconViewContentInsets + imageView.backgroundView.cornerRadius = iconRadius + contentView?.addSubview(imageView) + + lazyIconView = imageView + + if superview != nil { + setNeedsLayout() + } + + return imageView + } +} + +extension SwapAssetControl { + func bind(assetViewModel: SwapsAssetViewModel) { + let width = 2 * iconRadius - iconView.contentInsets.left - iconView.contentInsets.right + let height = 2 * iconRadius - iconView.contentInsets.top - iconView.contentInsets.bottom + iconViewContentInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) + let size = CGSize(width: width, height: height) + iconView.bind(viewModel: assetViewModel.imageViewModel, size: size) + + assetView.bind( + symbol: assetViewModel.symbol, + network: assetViewModel.hub.name, + icon: assetViewModel.hub.icon + ) + invalidateIntrinsicContentSize() + } + + func bind(emptyViewModel: EmptySwapsAssetViewModel) { + let size = CGSize(width: 2 * iconRadius, height: 2 * iconRadius) + iconView.bind(viewModel: emptyViewModel.imageViewModel, size: size) + iconViewContentInsets = .zero + assetView.bind( + symbol: emptyViewModel.title, + network: emptyViewModel.subtitle, + icon: nil + ) + invalidateIntrinsicContentSize() + } +} diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAssetView.swift b/novawallet/Modules/Swaps/Setup/View/SwapAssetView.swift new file mode 100644 index 0000000000..51a9a7454f --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/View/SwapAssetView.swift @@ -0,0 +1,78 @@ +import UIKit + +typealias SwapIconDetailsView = GenericPairValueView + +final class SwapAssetView: GenericPairValueView { + var assetLabel: UILabel { fView.fView.detailsLabel } + var disclosureImageView: UIImageView { fView.fView.imageView } + var hubNameView: UILabel { sView.detailsLabel } + var hubImageView: UIImageView { sView.imageView } + + private var imageViewModel: ImageViewModelProtocol? + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + fView.makeHorizontal() + fView.fView.spacing = 0 + fView.fView.iconWidth = 20 + fView.fView.mode = .detailsIcon + + sView.spacing = 8 + sView.iconWidth = 16 + sView.mode = .iconDetails + + spacing = 4 + makeVertical() + + assetLabel.apply(style: .semiboldBodyPrimary) + hubNameView.apply(style: .footnoteSecondary) + } + + override var intrinsicContentSize: CGSize { + let assetViewWidth = assetLabel.intrinsicContentSize.width + iconWidth + let hubViewWidth = iconWidth + iconSpacing + hubNameView.intrinsicContentSize.width + let width: CGFloat = max(assetViewWidth, hubViewWidth) + let assetHeight = max(assetLabel.intrinsicContentSize.height, iconWidth) + let hubViewHeight = max(hubNameView.intrinsicContentSize.height, iconWidth) + let height = assetHeight + spacing + hubViewHeight + return .init( + width: width, + height: height + ) + } + + private var iconWidth: CGFloat { + imageViewModel == nil ? 0 : fView.fView.iconWidth + } + + private var iconSpacing: CGFloat { + imageViewModel == nil ? 0 : sView.spacing + } + + func bind(symbol: String, network: String, icon: ImageViewModelProtocol?) { + assetLabel.text = symbol + imageViewModel?.cancel(on: hubImageView) + imageViewModel = icon + icon?.loadImage( + on: hubImageView, + targetSize: .init( + width: sView.iconWidth, + height: sView.iconWidth + ), + animated: true + ) + sView.hidesIcon = icon == nil + hubNameView.text = network + disclosureImageView.image = R.image.iconSmallArrow()?.tinted(with: R.color.colorIconSecondary()!) + invalidateIntrinsicContentSize() + } +} diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift new file mode 100644 index 0000000000..860dd3c0b0 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -0,0 +1,71 @@ +import UIKit +import SoraUI + +final class SwapSetupViewLayout: ScrollableContainerLayoutView { + let payAmountView: TitleHorizontalMultiValueView = .create { + $0.titleView.apply(style: .footnoteSecondary) + $0.detailsTitleLabel.apply(style: .footnoteAccentText) + $0.detailsValueLabel.apply(style: .footnotePrimary) + } + + let payAmountInputView = SwapAmountInputView() + + let receiveAmountView: TitleHorizontalMultiValueView = .create { + $0.titleView.apply(style: .footnoteSecondary) + $0.detailsTitleLabel.apply(style: .footnoteSecondary) + $0.detailsValueLabel.apply(style: .footnotePrimary) + } + + let receiveAmountInputView = SwapAmountInputView() + + let actionButton: TriangularedButton = .create { + $0.applyDefaultStyle() + } + + let switchButton: RoundedButton = .create { + $0.applyIconStyle() + $0.imageWithTitleView?.iconImage = R.image.iconActionSwap() + } + + override func setupStyle() { + backgroundColor = R.color.colorSecondaryScreenBackground() + } + + override func setupLayout() { + super.setupLayout() + + stackView.layoutMargins = UIEdgeInsets(top: 12, left: 16, bottom: 0, right: 16) + + addSubview(actionButton) + actionButton.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) + make.bottom.equalTo(safeAreaLayoutGuide).inset(UIConstants.actionBottomInset) + make.height.equalTo(UIConstants.actionHeight) + } + + addArrangedSubview(payAmountView, spacingAfter: 8) + payAmountView.snp.makeConstraints { + $0.height.equalTo(18) + } + addArrangedSubview(payAmountInputView, spacingAfter: 24) + payAmountInputView.snp.makeConstraints { + $0.height.equalTo(64) + } + addArrangedSubview(receiveAmountView, spacingAfter: 8) + receiveAmountView.snp.makeConstraints { + $0.height.equalTo(18) + } + addArrangedSubview(receiveAmountInputView) + receiveAmountInputView.snp.makeConstraints { + $0.height.equalTo(64) + } + + addSubview(switchButton) + switchButton.snp.makeConstraints { + $0.height.equalTo(switchButton.snp.width) + $0.top.equalTo(payAmountInputView.snp.bottom).offset(9) + $0.bottom.equalTo(receiveAmountInputView.snp.top).offset(-9) + $0.centerX.equalTo(payAmountInputView.snp.centerX) + } + } +} diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index e0899b3b2b..a0bf97d4fe 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1374,5 +1374,16 @@ "common.back" = "Back"; "asset.operation.send.empty.state.message" = "You don’t have tokens to send.\nBuy or Receive tokens to your\naccount."; "governance.referendums.status.deciding" = "Deciding"; +"wallet.assets.swap" = "Swap"; +"swaps.setup.asset.select.subtitle" = "Select a token"; +"swaps.setup.asset.pay.title" = "Pay"; +"swaps.setup.asset.receive.title" = "Receive"; +"swaps.setup.asset.action.select.tokens" = "Select tokens to swap"; +"swaps.setup.asset.action.select.receive" = "Select a token to receive"; +"swaps.setup.asset.action.select.pay" = "Select a token to pay"; +"swaps.setup.asset.action.enter.amount" = "Enter amount"; +"swaps.setup.asset.select.pay.title" = "You pay"; +"swaps.setup.asset.select.receive.title" = "You receive"; +"swaps.setup.asset.max" = "Max:"; "common.alert.external.link.disclaimer.title" = "You are leaving Nova Wallet"; "common.alert.external.link.disclaimer.message" = "You will be redirected to %@"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index a7b0c2fee5..14e7d6f1be 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1375,5 +1375,16 @@ "common.back" = "Назад"; "asset.operation.send.empty.state.message" = "У вас нет токенов для отправки.\nКупите или получите токены\nна свой аккаунт."; "governance.referendums.status.deciding" = "На рассмотрении"; +"wallet.assets.swap" = "Обмен"; +"swaps.setup.asset.select.subtitle" = "Выберите токeн"; +"swaps.setup.asset.pay.title" = "Оплата в"; +"swaps.setup.asset.receive.title" = "Получение"; +"swaps.setup.asset.action.select.tokens" = "Выберите токены для обмена"; +"swaps.setup.asset.action.select.receive" = "Выберите получаемый токен"; +"swaps.setup.asset.action.select.pay" = "Выберите отдаваемый токен"; +"swaps.setup.asset.action.enter.amount" = "Введите сумму"; +"swaps.setup.asset.select.pay.title" = "Вы платите"; +"swaps.setup.asset.select.receive.title" = "Вы получаете"; +"swaps.setup.asset.max" = "Максимум:"; "common.alert.external.link.disclaimer.title" = "Вы покидаете Nova Wallet"; "common.alert.external.link.disclaimer.message" = "Вы будете перенаправлены на сайт %@"; From b3b57fe47c402bc83d3314a8b9c95e2517568025 Mon Sep 17 00:00:00 2001 From: Gulnaz <666lynx666@mail.ru> Date: Fri, 6 Oct 2023 12:58:20 +0300 Subject: [PATCH 019/204] Swaps: Select token to pay (#854) * init module * renaming * fix * remove delegate * receive chainAsset instead chainAssetId * bugfix * init (#856) --- novawallet.xcodeproj/project.pbxproj | 40 ++++++ .../AssetListAssetsViewModelFactory.swift | 14 +- .../Swaps/SwapAssetListViewModelFactory.swift | 47 +++++++ .../Swaps/SwapAssetOperationPresenter.swift | 63 +++++++++ .../Swaps/SwapAssetOperationWireframe.swift | 10 ++ .../Swaps/SwapAssetsOperationInteractor.swift | 123 +++++++++++++++++ .../Swaps/SwapAssetsOperationProtocols.swift | 18 +++ .../SwapAssetsOperationViewController.swift | 39 ++++++ .../SwapAssetsOperationViewFactory.swift | 127 ++++++++++++++++++ .../Swaps/SwapAssetsOperationViewLayout.swift | 26 ++++ novawallet/en.lproj/Localizable.strings | 2 + novawallet/ru.lproj/Localizable.strings | 2 + 12 files changed, 504 insertions(+), 7 deletions(-) create mode 100644 novawallet/Modules/AssetsSearch/Swaps/SwapAssetListViewModelFactory.swift create mode 100644 novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationPresenter.swift create mode 100644 novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationWireframe.swift create mode 100644 novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift create mode 100644 novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationProtocols.swift create mode 100644 novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewController.swift create mode 100644 novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift create mode 100644 novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewLayout.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 909ad14ae6..39c51dfc14 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -763,6 +763,14 @@ 77AB55592AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */; }; 77AB555B2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */; }; 77AB555D2AA24BA90058814E /* OperationDetailsPoolRewardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */; }; + 77C9BCC42ACD570100022EA2 /* SwapAssetsOperationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */; }; + 77C9BCC62ACD571400022EA2 /* SwapAssetOperationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCC52ACD571400022EA2 /* SwapAssetOperationPresenter.swift */; }; + 77C9BCCA2ACD574800022EA2 /* SwapAssetOperationWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCC92ACD574800022EA2 /* SwapAssetOperationWireframe.swift */; }; + 77C9BCCC2ACD576B00022EA2 /* SwapAssetsOperationProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCCB2ACD576B00022EA2 /* SwapAssetsOperationProtocols.swift */; }; + 77C9BCCE2ACD691300022EA2 /* SwapAssetsOperationViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCCD2ACD691300022EA2 /* SwapAssetsOperationViewFactory.swift */; }; + 77C9BCD02ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCCF2ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift */; }; + 77C9BCD22ACD8AF400022EA2 /* SwapAssetsOperationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCD12ACD8AF400022EA2 /* SwapAssetsOperationViewController.swift */; }; + 77C9BCD42ACDF1AD00022EA2 /* SwapAssetListViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCD32ACDF1AD00022EA2 /* SwapAssetListViewModelFactory.swift */; }; 77C9BCBC2ACD1AF500022EA2 /* ViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */; }; 77C9BCBE2ACD286100022EA2 /* MockViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBD2ACD286100022EA2 /* MockViewModelFactory.swift */; }; 77CB33CE2A38780700B6709A /* structures_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77CB33CD2A38780700B6709A /* structures_output.json */; }; @@ -4754,6 +4762,14 @@ 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashModel.swift; sourceTree = ""; }; 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashViewModel.swift; sourceTree = ""; }; 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsPoolRewardView.swift; sourceTree = ""; }; + 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationInteractor.swift; sourceTree = ""; }; + 77C9BCC52ACD571400022EA2 /* SwapAssetOperationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetOperationPresenter.swift; sourceTree = ""; }; + 77C9BCC92ACD574800022EA2 /* SwapAssetOperationWireframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetOperationWireframe.swift; sourceTree = ""; }; + 77C9BCCB2ACD576B00022EA2 /* SwapAssetsOperationProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationProtocols.swift; sourceTree = ""; }; + 77C9BCCD2ACD691300022EA2 /* SwapAssetsOperationViewFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationViewFactory.swift; sourceTree = ""; }; + 77C9BCCF2ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationViewLayout.swift; sourceTree = ""; }; + 77C9BCD12ACD8AF400022EA2 /* SwapAssetsOperationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationViewController.swift; sourceTree = ""; }; + 77C9BCD32ACDF1AD00022EA2 /* SwapAssetListViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetListViewModelFactory.swift; sourceTree = ""; }; 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModels.swift; sourceTree = ""; }; 77C9BCBD2ACD286100022EA2 /* MockViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockViewModelFactory.swift; sourceTree = ""; }; 77CB33CD2A38780700B6709A /* structures_output.json */ = {isa = PBXFileReference; explicitFileType = text.json; fileEncoding = 4; path = structures_output.json; sourceTree = ""; usesTabs = 0; wrapsLines = 0; }; @@ -9682,6 +9698,21 @@ path = canonicalization; sourceTree = ""; }; + 77C9BCC22ACD563E00022EA2 /* Swaps */ = { + isa = PBXGroup; + children = ( + 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */, + 77C9BCC52ACD571400022EA2 /* SwapAssetOperationPresenter.swift */, + 77C9BCC92ACD574800022EA2 /* SwapAssetOperationWireframe.swift */, + 77C9BCCB2ACD576B00022EA2 /* SwapAssetsOperationProtocols.swift */, + 77C9BCCD2ACD691300022EA2 /* SwapAssetsOperationViewFactory.swift */, + 77C9BCCF2ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift */, + 77C9BCD12ACD8AF400022EA2 /* SwapAssetsOperationViewController.swift */, + 77C9BCD32ACDF1AD00022EA2 /* SwapAssetListViewModelFactory.swift */, + ); + path = Swaps; + sourceTree = ""; + }; 77C9BCBA2ACD1AE800022EA2 /* Model */ = { isa = PBXGroup; children = ( @@ -17193,6 +17224,7 @@ A29C55960FE9EADBDEAC6F03 /* AssetsSearch */ = { isa = PBXGroup; children = ( + 77C9BCC22ACD563E00022EA2 /* Swaps */, 0C1FE4F22A52EDD5003769E7 /* Model */, 775692822A24CA5100220756 /* AssetOperation */, 778D97A02A24D459002BA681 /* AssetSearch */, @@ -19816,6 +19848,7 @@ 77F189402A49972300E8B933 /* UILabel+bind.swift in Sources */, 8467FCFE24E5C50B005D486C /* KeystoreImportService.swift in Sources */, 847A259029B5D2710054F90C /* GovJsonLocalStorageHandler.swift in Sources */, + 77C9BCCE2ACD691300022EA2 /* SwapAssetsOperationViewFactory.swift in Sources */, 84C6801024D6EE4500006BF5 /* PlusIndicatorView.swift in Sources */, 8428765A24ADDE0200D91AD8 /* SettingsViewFactory.swift in Sources */, 849E17E8279143B2002D1744 /* NavigationBarStyle.swift in Sources */, @@ -20469,6 +20502,7 @@ 84744953289268770042FD80 /* WalletSwitchViewModel.swift in Sources */, 84FACCD925F8C22A00F32ED4 /* BigInt+Hex.swift in Sources */, 77799AF02A7CFB7C00B7E564 /* ValidatorViewModel.swift in Sources */, + 77C9BCCA2ACD574800022EA2 /* SwapAssetOperationWireframe.swift in Sources */, 849F33BC29F7C659001AEFA4 /* DAppInteractionProtocols.swift in Sources */, F409672626B29B04008CD244 /* UIScrollView+ScrollToPage.swift in Sources */, 84E0EE0E292D69A9008B2953 /* CallMetadata+TypeCheck.swift in Sources */, @@ -21304,6 +21338,7 @@ 842A736D27DB7B5E006EE1EA /* OperationTransferViewModel.swift in Sources */, 842B18022864F9950014CC57 /* CrossChainTransferPresenter.swift in Sources */, 84C1DBBA29C0A11200F295A5 /* XcmTransferService+Fee.swift in Sources */, + 77C9BCD02ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift in Sources */, 845B081529190056005785D3 /* Gov1UnlockReferendum.swift in Sources */, 84B24FB02A2F7B6F00F9BF59 /* StakingDashboardMoreOptionsCell.swift in Sources */, 77799ADD2A74219A00B7E564 /* ButtonState.swift in Sources */, @@ -21878,6 +21913,7 @@ 880855F028D099F2004255E7 /* CrowdloanOnChainSyncService.swift in Sources */, 843CE3A627D2098100436F4E /* NftDetailsLabel.swift in Sources */, 84216FD42827982800479375 /* SelectedRoundCollators.swift in Sources */, + 77C9BCC62ACD571400022EA2 /* SwapAssetOperationPresenter.swift in Sources */, 0C2F868B2A725C3C00593C01 /* EraNominationPoolsChanged.swift in Sources */, 849E17DC27909179002D1744 /* DAppSearchQueryTableViewCell.swift in Sources */, 2AC7BC842731A214001D99B0 /* AssetLocks+Sort.swift in Sources */, @@ -22154,6 +22190,7 @@ 6ECB27B386124F87382073FD /* DAppAddFavoriteProtocols.swift in Sources */, 84033057290FD8AB009C18E6 /* ReferendumsPersonalActivityView.swift in Sources */, 433A3C2B0D1E4BA5974D681B /* DAppAddFavoriteWireframe.swift in Sources */, + 77C9BCCC2ACD576B00022EA2 /* SwapAssetsOperationProtocols.swift in Sources */, F5CA222FA684AAC8B556E667 /* DAppAddFavoritePresenter.swift in Sources */, AC904E313DC15AE40C927946 /* DAppAddFavoriteInteractor.swift in Sources */, 0CD352932ACAD7A500B3E446 /* AssetHubExtrinsicService.swift in Sources */, @@ -22307,6 +22344,7 @@ 2F21134DE157A4B98ED309E2 /* AssetsSearchViewController.swift in Sources */, 8407716828CE8A1B007DBD24 /* ParaStkYieldBoostSetupInteractor+Children.swift in Sources */, 73B9C322A5033A4534238B25 /* AssetsSearchViewLayout.swift in Sources */, + 77C9BCD42ACDF1AD00022EA2 /* SwapAssetListViewModelFactory.swift in Sources */, 77ED167E2A0D0AE900E1FC8C /* Lenses.swift in Sources */, 6BBD025775841F8B055CA367 /* AssetsSearchViewFactory.swift in Sources */, 88C5F07E297EE7BC001CCADE /* InAppUpdatesRepository.swift in Sources */, @@ -22342,6 +22380,7 @@ 1812D5012A1765CB38D32A4A /* WalletsListPresenter.swift in Sources */, A265CC9857E951EB71E5E831 /* WalletsListInteractor.swift in Sources */, 28B4C94DBAF461CBF18B1B63 /* WalletsListViewController.swift in Sources */, + 77C9BCC42ACD570100022EA2 /* SwapAssetsOperationInteractor.swift in Sources */, 233CB11F486DE1953D977295 /* WalletsListViewLayout.swift in Sources */, 0C9525E52A7AF82B00BD724D /* DirectStakingMinStakeBuilder.swift in Sources */, FD43B68CFBD5C3497B446F53 /* ChangeWatchOnlyProtocols.swift in Sources */, @@ -22680,6 +22719,7 @@ 34D6FF85BEA25EFD1D15D460 /* InAppUpdatesInteractor.swift in Sources */, 0C1FE4F62A52F137003769E7 /* AssetSearchBuilderResult.swift in Sources */, 841E54A07ACD3AD160A1250A /* InAppUpdatesViewController.swift in Sources */, + 77C9BCD22ACD8AF400022EA2 /* SwapAssetsOperationViewController.swift in Sources */, DDA07514BEF3E2FD6EE1BB4E /* InAppUpdatesViewLayout.swift in Sources */, 32009DBB90D19ACD6D7B7A5C /* InAppUpdatesViewFactory.swift in Sources */, 77AB555D2AA24BA90058814E /* OperationDetailsPoolRewardView.swift in Sources */, diff --git a/novawallet/Modules/AssetList/Base/AssetListAssetsViewModelFactory.swift b/novawallet/Modules/AssetList/Base/AssetListAssetsViewModelFactory.swift index da8d38da6a..b5a69842b9 100644 --- a/novawallet/Modules/AssetList/Base/AssetListAssetsViewModelFactory.swift +++ b/novawallet/Modules/AssetList/Base/AssetListAssetsViewModelFactory.swift @@ -112,6 +112,13 @@ class AssetListAssetViewModelFactory { return .loading } } + + func formatPrice(amount: Decimal, priceData: PriceData?, locale: Locale) -> String { + let currencyId = priceData?.currencyId ?? currencyManager.selectedCurrency.id + let assetDisplayInfo = priceAssetInfoFactory.createAssetBalanceDisplayInfo(from: currencyId) + let priceFormatter = assetFormatterFactory.createAssetPriceFormatter(for: assetDisplayInfo) + return priceFormatter.value(for: locale).stringFromDecimal(amount) ?? "" + } } extension AssetListAssetViewModelFactory: AssetListAssetViewModelFactoryProtocol { @@ -149,13 +156,6 @@ extension AssetListAssetViewModelFactory: AssetListAssetViewModelFactoryProtocol ) } - func formatPrice(amount: Decimal, priceData: PriceData?, locale: Locale) -> String { - let currencyId = priceData?.currencyId ?? currencyManager.selectedCurrency.id - let assetDisplayInfo = priceAssetInfoFactory.createAssetBalanceDisplayInfo(from: currencyId) - let priceFormatter = assetFormatterFactory.createAssetPriceFormatter(for: assetDisplayInfo) - return priceFormatter.value(for: locale).stringFromDecimal(amount) ?? "" - } - func createAssetViewModel( chainId: ChainModel.Id, assetAccountInfo: AssetListAssetAccountInfo, diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetListViewModelFactory.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetListViewModelFactory.swift new file mode 100644 index 0000000000..d770718dd9 --- /dev/null +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetListViewModelFactory.swift @@ -0,0 +1,47 @@ +import Foundation + +final class SwapAssetListViewModelFactory: AssetListAssetViewModelFactory { + override func formatPrice(amount: Decimal, priceData: PriceData?, locale: Locale) -> String { + guard amount > 0 else { + return "" + } + + let formattedPrice = super.formatPrice( + amount: amount, + priceData: priceData, + locale: locale + ) + return wrap(price: formattedPrice) + } + + override func createBalanceState( + assetAccountInfo: AssetListAssetAccountInfo, + connected: Bool, + locale: Locale + ) -> (LoadableViewModelState, LoadableViewModelState) { + let (balanceState, priceState) = super.createBalanceState( + assetAccountInfo: assetAccountInfo, + connected: connected, + locale: locale + ) + guard let balance = assetAccountInfo.balance, balance > 0 else { + return (balanceState, priceState) + } + + switch priceState { + case .loading: + return (balanceState, priceState) + case let .cached(value): + return (balanceState, .cached(value: wrap(price: value))) + case let .loaded(value): + return (balanceState, .loaded(value: wrap(price: value))) + } + } + + private func wrap(price: String) -> String { + guard !price.isEmpty else { + return price + } + return "~\(price)" + } +} diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationPresenter.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationPresenter.swift new file mode 100644 index 0000000000..cee53517d6 --- /dev/null +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationPresenter.swift @@ -0,0 +1,63 @@ +import Foundation +import BigInt +import RobinHood +import SoraFoundation + +final class SwapAssetsOperationPresenter: AssetsSearchPresenter { + var swapAssetsWireframe: SwapAssetsOperationWireframeProtocol? { + wireframe as? SwapAssetsOperationWireframeProtocol + } + + var swapAssetsView: SwapAssetsViewProtocol? { + view as? SwapAssetsViewProtocol + } + + let selectClosure: (ChainAsset) -> Void + + init( + selectClosure: @escaping (ChainAsset) -> Void, + interactor: SwapAssetsOperationInteractorInputProtocol, + viewModelFactory: AssetListAssetViewModelFactoryProtocol, + localizationManager: LocalizationManagerProtocol, + wireframe: SwapAssetsOperationWireframeProtocol + ) { + self.selectClosure = selectClosure + + super.init( + delegate: nil, + interactor: interactor, + wireframe: wireframe, + viewModelFactory: viewModelFactory, + localizationManager: localizationManager + ) + } + + override func setup() { + interactor.setup() + swapAssetsView?.didStartLoading() + } + + override func selectAsset(for chainAssetId: ChainAssetId) { + guard let chainAsset = result?.state.chainAsset(for: chainAssetId) else { + return + } + selectClosure(chainAsset) + wireframe.close(view: view) + } +} + +extension SwapAssetsOperationPresenter: SwapAssetsOperationPresenterProtocol { + func directionsLoaded() { + swapAssetsView?.didStopLoading() + } + + func didReceive(error _: SwapAssetsOperationError) { + swapAssetsWireframe?.presentRequestStatus( + on: swapAssetsView, + locale: selectedLocale, + retryAction: { [weak self] in + self?.interactor.setup() + } + ) + } +} diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationWireframe.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationWireframe.swift new file mode 100644 index 0000000000..43b09ff06d --- /dev/null +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationWireframe.swift @@ -0,0 +1,10 @@ +import UIKit +import SoraUI + +final class SwapAssetsOperationWireframe: SwapAssetsOperationWireframeProtocol {} + +extension SwapAssetsOperationWireframe: AssetsSearchWireframeProtocol { + func close(view: AssetsSearchViewProtocol?) { + view?.controller.navigationController?.popViewController(animated: true) + } +} diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift new file mode 100644 index 0000000000..24ce76df6a --- /dev/null +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift @@ -0,0 +1,123 @@ +import BigInt +import RobinHood + +final class SwapAssetsOperationInteractor: AnyCancellableCleaning { + weak var presenter: SwapAssetsOperationPresenterProtocol? + + let stateObservable: AssetListModelObservable + let logger: LoggerProtocol + let chainAsset: ChainAsset? + let assetConversionOperationFactory: AssetConversionOperationFactoryProtocol + + private let operationQueue: OperationQueue + private var builder: AssetSearchBuilder? + private var directionsCall: CancellableCall? + private var availableDirections: [ChainAssetId: Set]? + + init( + stateObservable: AssetListModelObservable, + chainAsset: ChainAsset?, + assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, + logger: LoggerProtocol, + operationQueue: OperationQueue + ) { + self.stateObservable = stateObservable + self.logger = logger + self.chainAsset = chainAsset + self.assetConversionOperationFactory = assetConversionOperationFactory + self.operationQueue = operationQueue + } + + private func loadDirections() { + if let chainAsset = chainAsset { + loadDirections(for: chainAsset) + } else { + loadAllDirections() + } + } + + private func loadAllDirections() { + let wrapper = assetConversionOperationFactory.availableDirections() + loadDirections(wrapper: wrapper, mapper: { $0 }) + } + + private func loadDirections(for chainAsset: ChainAsset) { + let wrapper = assetConversionOperationFactory.availableDirectionsForAsset(chainAsset.chainAssetId) + loadDirections(wrapper: wrapper) { + var result = [ChainAssetId: Set]() + result[chainAsset.chainAssetId] = $0 + return result + } + } + + private func loadDirections( + wrapper: CompoundOperationWrapper, + mapper: @escaping (Result) -> [ChainAssetId: Set] + ) { + clear(cancellable: &directionsCall) + + wrapper.targetOperation.completionBlock = { [weak self] in + guard self?.directionsCall === wrapper else { + return + } + + self?.directionsCall = nil + + DispatchQueue.main.async { + do { + let result = try wrapper.targetOperation.extractNoCancellableResultData() + self?.availableDirections = mapper(result) + self?.createBuilder() + self?.presenter?.directionsLoaded() + } catch { + self?.presenter?.didReceive(error: .directions(error)) + } + } + } + + directionsCall = wrapper + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } + + private func createBuilder() { + let searchQueue = OperationQueue() + searchQueue.maxConcurrentOperationCount = 1 + + let filter: ChainAssetsFilter = { [weak self] chainAsset in + guard let availableDirections = self?.availableDirections else { + return false + } + return availableDirections[chainAsset.chainAssetId]?.isEmpty == false + } + + builder = .init( + filter: filter, + workingQueue: .main, + callbackQueue: .main, + callbackClosure: { [weak self] result in + self?.presenter?.didReceive(result: result) + }, + operationQueue: searchQueue, + logger: logger + ) + + builder?.apply(model: stateObservable.state.value) + + stateObservable.addObserver(with: self) { [weak self] _, newState in + guard let self = self else { + return + } + self.builder?.apply(model: newState.value) + } + } +} + +extension SwapAssetsOperationInteractor: SwapAssetsOperationInteractorInputProtocol { + func setup() { + loadDirections() + } + + func search(query: String) { + builder?.apply(query: query) + } +} diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationProtocols.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationProtocols.swift new file mode 100644 index 0000000000..2f139cff57 --- /dev/null +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationProtocols.swift @@ -0,0 +1,18 @@ +protocol SwapAssetsOperationWireframeProtocol: AssetsSearchWireframeProtocol, ErrorPresentable, + AlertPresentable, CommonRetryable {} + +protocol SwapAssetsOperationPresenterProtocol: AssetsSearchInteractorOutputProtocol { + func directionsLoaded() + func didReceive(error: SwapAssetsOperationError) +} + +protocol SwapAssetsOperationInteractorInputProtocol: AssetsSearchInteractorInputProtocol {} + +enum SwapAssetsOperationError: Error { + case directions(Error) +} + +protocol SwapAssetsViewProtocol: AssetsSearchViewProtocol { + func didStartLoading() + func didStopLoading() +} diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewController.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewController.swift new file mode 100644 index 0000000000..2aba29fbf2 --- /dev/null +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewController.swift @@ -0,0 +1,39 @@ +import UIKit + +final class SwapAssetsOperationViewController: AssetsSearchViewController { + private var isLoading: Bool = false + + var swapPresenter: SwapAssetsOperationPresenterProtocol? { + presenter as? SwapAssetsOperationPresenterProtocol + } + + var swapView: SwapAssetsOperationViewLayout? { + rootView as? SwapAssetsOperationViewLayout + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if isLoading { + swapView?.searchView.isUserInteractionEnabled = false + swapView?.activityIndicator.startAnimating() + } else { + swapView?.searchView.isUserInteractionEnabled = true + swapView?.activityIndicator.stopAnimating() + } + } +} + +extension SwapAssetsOperationViewController: SwapAssetsViewProtocol { + func didStartLoading() { + isLoading = true + swapView?.searchView.isUserInteractionEnabled = false + swapView?.activityIndicator.startAnimating() + } + + func didStopLoading() { + isLoading = false + swapView?.searchView.isUserInteractionEnabled = true + swapView?.activityIndicator.stopAnimating() + } +} diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift new file mode 100644 index 0000000000..28a1ccd005 --- /dev/null +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift @@ -0,0 +1,127 @@ +import Foundation +import SoraFoundation + +enum SwapAssetsOperationViewFactory { + static func createSelectPayTokenView( + for stateObservable: AssetListModelObservable, + chainAsset: ChainAsset? = nil, + selectClosure: @escaping (ChainAsset) -> Void + ) -> AssetsSearchViewProtocol? { + let title: LocalizableResource = .init { + R.string.localizable.swapsPayTokenSelectionTitle( + preferredLanguages: $0.rLanguages + ) + } + + return createView( + for: stateObservable, + chainAsset: chainAsset, + title: title, + selectClosure: selectClosure + ) + } + + static func createSelectReceiveTokenView( + for stateObservable: AssetListModelObservable, + chainAsset: ChainAsset? = nil, + selectClosure: @escaping (ChainAsset) -> Void + ) -> AssetsSearchViewProtocol? { + let title: LocalizableResource = .init { + R.string.localizable.swapsReceiveTokenSelectionTitle( + preferredLanguages: $0.rLanguages + ) + } + + return createView( + for: stateObservable, + chainAsset: chainAsset, + title: title, + selectClosure: selectClosure + ) + } + + static func createView( + for stateObservable: AssetListModelObservable, + chainAsset: ChainAsset? = nil, + title: LocalizableResource, + selectClosure: @escaping (ChainAsset) -> Void + ) -> AssetsSearchViewProtocol? { + guard let currencyManager = CurrencyManager.shared else { + return nil + } + + let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) + let viewModelFactory = SwapAssetListViewModelFactory( + priceAssetInfoFactory: priceAssetInfoFactory, + assetFormatterFactory: AssetBalanceFormatterFactory(), + percentFormatter: NumberFormatter.signedPercent.localizableResource(), + currencyManager: currencyManager + ) + + guard let presenter = createPresenter( + stateObservable: stateObservable, + viewModelFactory: viewModelFactory, + chainAsset: chainAsset, + selectClosure: selectClosure + ) else { + return nil + } + + let view = SwapAssetsOperationViewController( + presenter: presenter, + keyboardAppearanceStrategy: EventDrivenKeyboardStrategy(events: [.viewDidAppear]), + createViewClosure: { SwapAssetsOperationViewLayout() }, + localizableTitle: title, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + + return view + } + + private static func createPresenter( + stateObservable: AssetListModelObservable, + viewModelFactory: AssetListAssetViewModelFactoryProtocol, + chainAsset: ChainAsset?, + selectClosure: @escaping (ChainAsset) -> Void + ) -> SwapAssetsOperationPresenter? { + let westmintChainId = KnowChainId.westmint + let chainRegistry = ChainRegistryFacade.sharedRegistry + + guard let connection = chainRegistry.getConnection(for: westmintChainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: westmintChainId), + let chainModel = chainRegistry.getChain(for: westmintChainId) else { + return nil + } + + let operationQueue = OperationManagerFacade.sharedDefaultQueue + + let assetConversionOperationFactory = AssetHubSwapOperationFactory( + chain: chainModel, + runtimeService: runtimeService, + connection: connection, + operationQueue: operationQueue + ) + + let interactor = SwapAssetsOperationInteractor( + stateObservable: stateObservable, + chainAsset: chainAsset, + assetConversionOperationFactory: assetConversionOperationFactory, + logger: Logger.shared, + operationQueue: operationQueue + ) + + let presenter = SwapAssetsOperationPresenter( + selectClosure: selectClosure, + interactor: interactor, + viewModelFactory: viewModelFactory, + localizationManager: LocalizationManager.shared, + wireframe: SwapAssetsOperationWireframe() + ) + + interactor.presenter = presenter + + return presenter + } +} diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewLayout.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewLayout.swift new file mode 100644 index 0000000000..7546b940f2 --- /dev/null +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewLayout.swift @@ -0,0 +1,26 @@ +import UIKit + +final class SwapAssetsOperationViewLayout: BaseAssetsSearchViewLayout { + let activityIndicator: UIActivityIndicatorView = .create { view in + view.style = .medium + view.tintColor = R.color.colorIconPrimary() + view.hidesWhenStopped = true + } + + override func createSearchView() -> SearchViewProtocol { + let view = TopCustomSearchView() + view.searchBar.textField.autocorrectionType = .no + view.searchBar.textField.autocapitalizationType = .none + return view + } + + override func setup() { + backgroundColor = R.color.colorSecondaryScreenBackground() + super.setup() + + addSubview(activityIndicator) + activityIndicator.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } +} diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index a0bf97d4fe..972f892200 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1387,3 +1387,5 @@ "swaps.setup.asset.max" = "Max:"; "common.alert.external.link.disclaimer.title" = "You are leaving Nova Wallet"; "common.alert.external.link.disclaimer.message" = "You will be redirected to %@"; +"swaps.pay.token.selection.title" = "Token to pay"; +"swaps.receive.token.selection.title" = "Token to receive"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 14e7d6f1be..999d2cb026 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1388,3 +1388,5 @@ "swaps.setup.asset.max" = "Максимум:"; "common.alert.external.link.disclaimer.title" = "Вы покидаете Nova Wallet"; "common.alert.external.link.disclaimer.message" = "Вы будете перенаправлены на сайт %@"; +"swaps.pay.token.selection.title" = "Токен для оплаты"; +"swaps.receive.token.selection.title" = "Токен для получения"; From e705942e0dd09908a7d7c5014675e03cf37c5701 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 6 Oct 2023 12:59:06 +0300 Subject: [PATCH 020/204] add builder --- .../Swaps/Setup/SwapSetupInteractor.swift | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 61e52e5a7e..7e8237b010 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -12,6 +12,7 @@ final class SwapSetupInteractor: AnyCancellableCleaning { private let operationQueue: OperationQueue private var quoteCall: CancellableCall? + private var runtimeOperationCall: CancellableCall? init( assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, @@ -55,17 +56,26 @@ final class SwapSetupInteractor: AnyCancellableCleaning { nil } - private func fee(args _: AssetConversion.CallArgs) { + private func fee(args: AssetConversion.CallArgs) { + clear(cancellable: &runtimeOperationCall) guard let extrinsicService = extrinsicService() else { presenter?.didReceive(error: .fetchFeeFailed(CommonError.undefined)) return } -// let builder = assetConversionExtrinsicService.fetchExtrinsicBuilderClosure( -// for: args, -// codingFactory: runtimeCoderFactory -// ) -// feeProxy.estimateFee(using: extrinsicService, reuseIdentifier: "", setupBy: builder) + let runtimeCoderFactoryOperation = runtimeService.fetchCoderFactoryOperation() + + runtimeCoderFactoryOperation.completionBlock = { [weak self] in + let runtimeCoderFactory = try runtimeCoderFactoryOperation.extractNoCancellableResultData() + let builder = assetConversionExtrinsicService.fetchExtrinsicBuilderClosure( + for: args, + codingFactory: runtimeCoderFactory + ) + self?.feeProxy.estimateFee(using: extrinsicService, reuseIdentifier: "", setupBy: builder) + } + + runtimeOperationCall = runtimeCoderFactoryOperation + operationQueue.addOperation(runtimeCoderFactoryOperation) } } From a11681766cab1c5b9f15367797f234ab740ac3cb Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 6 Oct 2023 18:46:10 +0300 Subject: [PATCH 021/204] add view model factory --- novawallet.xcodeproj/project.pbxproj | 42 +-- .../StackTable/StackTitleMultiValueCell.swift | 11 +- .../AssetList/AssetListWireframe.swift | 4 +- .../Setup/Model/MockViewModelFactory.swift | 88 ------ .../Model/SwapsSetupViewModelFactory.swift | 228 +++++++++++++++ .../Swaps/Setup/SwapSetupInteractor.swift | 38 ++- .../Swaps/Setup/SwapSetupPresenter.swift | 274 ++++++++++++++++-- .../Swaps/Setup/SwapSetupProtocols.swift | 16 +- .../Swaps/Setup/SwapSetupViewController.swift | 24 +- .../Swaps/Setup/SwapSetupViewFactory.swift | 12 +- .../Swaps/Setup/SwapSetupWireframe.swift | 36 ++- .../Swaps/Setup/View/SwapAmountInput.swift | 2 +- .../Setup/View/SwapAmountInputView.swift | 1 + .../Setup/View/SwapSetupViewLayout.swift | 43 --- 14 files changed, 616 insertions(+), 203 deletions(-) delete mode 100644 novawallet/Modules/Swaps/Setup/Model/MockViewModelFactory.swift create mode 100644 novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 88715b9404..7ac270b7d3 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -763,6 +763,8 @@ 77AB55592AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */; }; 77AB555B2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */; }; 77AB555D2AA24BA90058814E /* OperationDetailsPoolRewardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */; }; + 77C9BCBC2ACD1AF500022EA2 /* ViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */; }; + 77C9BCBE2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */; }; 77C9BCC42ACD570100022EA2 /* SwapAssetsOperationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */; }; 77C9BCC62ACD571400022EA2 /* SwapAssetOperationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCC52ACD571400022EA2 /* SwapAssetOperationPresenter.swift */; }; 77C9BCCA2ACD574800022EA2 /* SwapAssetOperationWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCC92ACD574800022EA2 /* SwapAssetOperationWireframe.swift */; }; @@ -771,8 +773,6 @@ 77C9BCD02ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCCF2ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift */; }; 77C9BCD22ACD8AF400022EA2 /* SwapAssetsOperationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCD12ACD8AF400022EA2 /* SwapAssetsOperationViewController.swift */; }; 77C9BCD42ACDF1AD00022EA2 /* SwapAssetListViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCD32ACDF1AD00022EA2 /* SwapAssetListViewModelFactory.swift */; }; - 77C9BCBC2ACD1AF500022EA2 /* ViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */; }; - 77C9BCBE2ACD286100022EA2 /* MockViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBD2ACD286100022EA2 /* MockViewModelFactory.swift */; }; 77CB33CE2A38780700B6709A /* structures_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77CB33CD2A38780700B6709A /* structures_output.json */; }; 77CB33D22A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CB33D12A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift */; }; 77CB33D72A3998FD00B6709A /* Array+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CB33D62A3998FC00B6709A /* Array+Sort.swift */; }; @@ -4764,6 +4764,8 @@ 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashModel.swift; sourceTree = ""; }; 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashViewModel.swift; sourceTree = ""; }; 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsPoolRewardView.swift; sourceTree = ""; }; + 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModels.swift; sourceTree = ""; }; + 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapsSetupViewModelFactory.swift; sourceTree = ""; }; 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationInteractor.swift; sourceTree = ""; }; 77C9BCC52ACD571400022EA2 /* SwapAssetOperationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetOperationPresenter.swift; sourceTree = ""; }; 77C9BCC92ACD574800022EA2 /* SwapAssetOperationWireframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetOperationWireframe.swift; sourceTree = ""; }; @@ -4772,8 +4774,6 @@ 77C9BCCF2ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationViewLayout.swift; sourceTree = ""; }; 77C9BCD12ACD8AF400022EA2 /* SwapAssetsOperationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationViewController.swift; sourceTree = ""; }; 77C9BCD32ACDF1AD00022EA2 /* SwapAssetListViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetListViewModelFactory.swift; sourceTree = ""; }; - 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModels.swift; sourceTree = ""; }; - 77C9BCBD2ACD286100022EA2 /* MockViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockViewModelFactory.swift; sourceTree = ""; }; 77CB33CD2A38780700B6709A /* structures_output.json */ = {isa = PBXFileReference; explicitFileType = text.json; fileEncoding = 4; path = structures_output.json; sourceTree = ""; usesTabs = 0; wrapsLines = 0; }; 77CB33D12A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Web3NameIntegrityVerifierWithCanonicalizationData.swift; sourceTree = ""; }; 77CB33D62A3998FC00B6709A /* Array+Sort.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Sort.swift"; sourceTree = ""; }; @@ -9702,26 +9702,11 @@ path = canonicalization; sourceTree = ""; }; - 77C9BCC22ACD563E00022EA2 /* Swaps */ = { - isa = PBXGroup; - children = ( - 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */, - 77C9BCC52ACD571400022EA2 /* SwapAssetOperationPresenter.swift */, - 77C9BCC92ACD574800022EA2 /* SwapAssetOperationWireframe.swift */, - 77C9BCCB2ACD576B00022EA2 /* SwapAssetsOperationProtocols.swift */, - 77C9BCCD2ACD691300022EA2 /* SwapAssetsOperationViewFactory.swift */, - 77C9BCCF2ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift */, - 77C9BCD12ACD8AF400022EA2 /* SwapAssetsOperationViewController.swift */, - 77C9BCD32ACDF1AD00022EA2 /* SwapAssetListViewModelFactory.swift */, - ); - path = Swaps; - sourceTree = ""; - }; 77C9BCBA2ACD1AE800022EA2 /* Model */ = { isa = PBXGroup; children = ( 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */, - 77C9BCBD2ACD286100022EA2 /* MockViewModelFactory.swift */, + 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */, ); path = Model; sourceTree = ""; @@ -9734,6 +9719,21 @@ path = Swaps; sourceTree = ""; }; + 77C9BCC22ACD563E00022EA2 /* Swaps */ = { + isa = PBXGroup; + children = ( + 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */, + 77C9BCC52ACD571400022EA2 /* SwapAssetOperationPresenter.swift */, + 77C9BCC92ACD574800022EA2 /* SwapAssetOperationWireframe.swift */, + 77C9BCCB2ACD576B00022EA2 /* SwapAssetsOperationProtocols.swift */, + 77C9BCCD2ACD691300022EA2 /* SwapAssetsOperationViewFactory.swift */, + 77C9BCCF2ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift */, + 77C9BCD12ACD8AF400022EA2 /* SwapAssetsOperationViewController.swift */, + 77C9BCD32ACDF1AD00022EA2 /* SwapAssetListViewModelFactory.swift */, + ); + path = Swaps; + sourceTree = ""; + }; 77CB33D32A38894600B6709A /* Integrity */ = { isa = PBXGroup; children = ( @@ -21172,7 +21172,7 @@ F4D0546B2729949100210294 /* MoonbeamMakeSignatureResponse.swift in Sources */, D9046DBA27451ED700C29F2E /* ParallelContributionSource.swift in Sources */, 0CE629DE2AA9B6BF00E250BD /* RewardDestinationViewModelFactory.swift in Sources */, - 77C9BCBE2ACD286100022EA2 /* MockViewModelFactory.swift in Sources */, + 77C9BCBE2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift in Sources */, 844DB624262D9C710025A8F0 /* ErasRewardDistribution.swift in Sources */, 84FAB0632542C8D600319F74 /* ContactItem.swift in Sources */, 06590486EED4050BADDD32C5 /* AccountManagementPresenter.swift in Sources */, diff --git a/novawallet/Common/View/StackTable/StackTitleMultiValueCell.swift b/novawallet/Common/View/StackTable/StackTitleMultiValueCell.swift index a14caf8ef6..709d328fef 100644 --- a/novawallet/Common/View/StackTable/StackTitleMultiValueCell.swift +++ b/novawallet/Common/View/StackTable/StackTitleMultiValueCell.swift @@ -71,8 +71,13 @@ extension StackTitleMultiValueCell { ) } - // TODO: Skeleton - func bind(loadableViewModel: LoadableViewModelState) { - loadableViewModel.value.map(bind) + func bind(loadableViewModel: LoadableViewModelState) { + switch loadableViewModel { + case let .cached(value), let .loaded(value): + rowContentView.valueView.valueTop.text = value + case .loading: + // TODO: Skeleton + break + } } } diff --git a/novawallet/Modules/AssetList/AssetListWireframe.swift b/novawallet/Modules/AssetList/AssetListWireframe.swift index 3d1ebef65f..1518b2c2e7 100644 --- a/novawallet/Modules/AssetList/AssetListWireframe.swift +++ b/novawallet/Modules/AssetList/AssetListWireframe.swift @@ -128,7 +128,9 @@ final class AssetListWireframe: AssetListWireframeProtocol { } func showSwapTokens(from view: AssetListViewProtocol?) { - guard let swapTokensView = SwapSetupViewFactory.createView() else { + guard let swapTokensView = SwapSetupViewFactory.createView( + assetListObservable: assetListModelObservable + ) else { return } diff --git a/novawallet/Modules/Swaps/Setup/Model/MockViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/MockViewModelFactory.swift deleted file mode 100644 index a97fd2f2f2..0000000000 --- a/novawallet/Modules/Swaps/Setup/Model/MockViewModelFactory.swift +++ /dev/null @@ -1,88 +0,0 @@ -import SoraFoundation - -final class MockViewModelFactory { - func buttonState() -> ButtonState { - .init( - title: .init { - R.string.localizable.swapsSetupAssetActionSelectReceive(preferredLanguages: $0.rLanguages) - }, - enabled: false - ) - } - - func payTitleModel(locale: Locale) -> TitleHorizontalMultiValueView.Model { - TitleHorizontalMultiValueView.Model( - title: - R.string.localizable.swapsSetupAssetSelectPayTitle(preferredLanguages: locale.rLanguages), - subtitle: - R.string.localizable.swapsSetupAssetMax( - preferredLanguages: locale.rLanguages - ), - value: "100 DOT" - ) - } - - func payModel() -> SwapAssetInputViewModel { - let dotImage = RemoteImageViewModel(url: URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/icons/chains/white/Polkadot.svg")!) - let hubImage = RemoteImageViewModel(url: URL(string: "https://parachains.info/images/parachains/1688559044_assethub.svg")!) - return .asset(SwapsAssetViewModel( - symbol: "DOT", - imageViewModel: dotImage, - hub: .init( - name: "Polkadot Asset Hub", - icon: hubImage - ) - )) - } - - func payPriceModel() -> String? { - "$0" - } - - func receiveTitleModel(locale: Locale) -> TitleHorizontalMultiValueView.Model { - TitleHorizontalMultiValueView.Model( - title: - R.string.localizable.swapsSetupAssetSelectReceiveTitle(preferredLanguages: locale.rLanguages), - subtitle: "", - value: "" - ) - } - - func receiveModel(locale: Locale) -> SwapAssetInputViewModel { - .empty(emptyReceiveViewModel(locale: locale)) - } - - func payAmount( - locale: Locale, - balanceViewModelFactory: BalanceViewModelFactoryFacadeProtocol - ) -> AmountInputViewModelProtocol { - let targetAssetInfo = AssetBalanceDisplayInfo( - displayPrecision: 2, - assetPrecision: 10, - symbol: "DOT", - symbolValueSeparator: "", - symbolPosition: .suffix, - icon: nil - ) - return balanceViewModelFactory.createBalanceInputViewModel( - targetAssetInfo: targetAssetInfo, - amount: nil - ).value(for: locale) - } - - func emptyPayViewModel(locale: Locale) -> EmptySwapsAssetViewModel { - EmptySwapsAssetViewModel( - imageViewModel: StaticImageViewModel(image: R.image.iconAddSwapAmount()!), - title: R.string.localizable.swapsSetupAssetPayTitle(preferredLanguages: locale.rLanguages), - subtitle: R.string.localizable.swapsSetupAssetSelectSubtitle(preferredLanguages: locale.rLanguages) - ) - } - - func emptyReceiveViewModel(locale: Locale) -> EmptySwapsAssetViewModel { - EmptySwapsAssetViewModel( - imageViewModel: StaticImageViewModel(image: R.image.iconAddSwapAmount()!), - title: R.string.localizable.swapsSetupAssetReceiveTitle(preferredLanguages: locale.rLanguages), - subtitle: R.string.localizable.swapsSetupAssetSelectSubtitle(preferredLanguages: locale.rLanguages) - ) - } -} diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift new file mode 100644 index 0000000000..39adaab328 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift @@ -0,0 +1,228 @@ +import SoraFoundation +import BigInt + +struct RateParams { + let assetDisplayInfoIn: AssetBalanceDisplayInfo + let assetDisplayInfoOut: AssetBalanceDisplayInfo + let amountIn: BigUInt + let amountOut: BigUInt +} + +protocol SwapsSetupViewModelFactoryProtocol { + func buttonState( + assetIn: ChainAssetId?, + assetOut: ChainAssetId?, + amountIn: Decimal?, + amountOut: Decimal? + ) -> ButtonState + func payTitleViewModel( + assetDisplayInfo: AssetBalanceDisplayInfo?, + maxValue: BigUInt?, + locale: Locale + ) -> TitleHorizontalMultiValueView.Model + func payAssetViewModel(chainAsset: ChainAsset?, locale: Locale) -> SwapAssetInputViewModel + func inputPriceViewModel( + assetDisplayInfo: AssetBalanceDisplayInfo, + amount: Decimal?, + priceData: PriceData?, + locale: Locale + ) -> String? + func receiveTitleViewModel(locale: Locale) -> TitleHorizontalMultiValueView.Model + func receiveAssetViewModel(chainAsset: ChainAsset?, locale: Locale) -> SwapAssetInputViewModel + func amountInputViewModel(chainAsset: ChainAsset, amount: Decimal?, locale: Locale) -> AmountInputViewModelProtocol + func rate(from params: RateParams, locale: Locale) -> String +} + +final class SwapsSetupViewModelFactory { + let balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol + let networkViewModelFactory: NetworkViewModelFactoryProtocol + + init( + balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol, + networkViewModelFactory: NetworkViewModelFactoryProtocol + ) { + self.balanceViewModelFactoryFacade = balanceViewModelFactoryFacade + self.networkViewModelFactory = networkViewModelFactory + } + + private static func buttonTitle( + assetIn: ChainAssetId?, + assetOut: ChainAssetId?, + amountIn: Decimal?, + amountOut: Decimal?, + locale: Locale + ) -> String { + switch (assetIn, assetOut) { + case (nil, nil), (nil, _): + return R.string.localizable.swapsSetupAssetActionSelectPay(preferredLanguages: locale.rLanguages) + case (_, nil): + return R.string.localizable.swapsSetupAssetActionSelectReceive(preferredLanguages: locale.rLanguages) + default: + if amountIn == nil || amountOut == nil { + return R.string.localizable.swapsSetupAssetActionEnterAmount(preferredLanguages: locale.rLanguages) + } else { + return R.string.localizable.commonContinue(preferredLanguages: locale.rLanguages) + } + } + } + + private func assetViewModel(chainAsset: ChainAsset) -> SwapsAssetViewModel { + let networkViewModel = networkViewModelFactory.createViewModel(from: chainAsset.chain) + let assetIcon: ImageViewModelProtocol = chainAsset.asset.icon.map { RemoteImageViewModel(url: $0) } ?? + StaticImageViewModel(image: R.image.iconDefaultToken()!) + + return SwapsAssetViewModel( + symbol: chainAsset.asset.symbol, + imageViewModel: assetIcon, + hub: networkViewModel + ) + } + + private func emptyPayAssetViewModel(locale: Locale) -> EmptySwapsAssetViewModel { + EmptySwapsAssetViewModel( + imageViewModel: StaticImageViewModel(image: R.image.iconAddSwapAmount()!), + title: R.string.localizable.swapsSetupAssetPayTitle(preferredLanguages: locale.rLanguages), + subtitle: R.string.localizable.swapsSetupAssetSelectSubtitle(preferredLanguages: locale.rLanguages) + ) + } + + private func emptyReceiveAssetViewModel(locale: Locale) -> EmptySwapsAssetViewModel { + EmptySwapsAssetViewModel( + imageViewModel: StaticImageViewModel(image: R.image.iconAddSwapAmount()!), + title: R.string.localizable.swapsSetupAssetReceiveTitle(preferredLanguages: locale.rLanguages), + subtitle: R.string.localizable.swapsSetupAssetSelectSubtitle(preferredLanguages: locale.rLanguages) + ) + } +} + +extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { + func buttonState( + assetIn: ChainAssetId?, + assetOut: ChainAssetId?, + amountIn: Decimal?, + amountOut: Decimal? + ) -> ButtonState { + let dataFullFilled = assetIn != nil && assetOut != nil && amountIn != nil && amountOut != nil + return .init( + title: .init { + Self.buttonTitle( + assetIn: assetIn, + assetOut: assetOut, + amountIn: amountIn, + amountOut: amountOut, + locale: $0 + ) + }, + enabled: dataFullFilled + ) + } + + func payTitleViewModel( + assetDisplayInfo: AssetBalanceDisplayInfo?, + maxValue: BigUInt?, + locale: Locale + ) -> TitleHorizontalMultiValueView.Model { + let title = R.string.localizable.swapsSetupAssetSelectPayTitle( + preferredLanguages: locale.rLanguages + ) + + if let assetDisplayInfo = assetDisplayInfo, let maxValue = maxValue { + let amountDecimal = Decimal.fromSubstrateAmount( + maxValue, + precision: Int16(assetDisplayInfo.displayPrecision) + ) ?? 0 + let maxValueString = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: assetDisplayInfo, + value: amountDecimal + ).value(for: locale) + + return .init( + title: title, + subtitle: + R.string.localizable.swapsSetupAssetMax( + preferredLanguages: locale.rLanguages + ), + value: maxValueString + ) + } else { + return .init( + title: + R.string.localizable.swapsSetupAssetSelectPayTitle( + preferredLanguages: locale.rLanguages + ), + subtitle: "", + value: "" + ) + } + } + + func payAssetViewModel(chainAsset: ChainAsset?, locale: Locale) -> SwapAssetInputViewModel { + chainAsset.map { .asset(assetViewModel(chainAsset: $0)) } ?? .empty(emptyPayAssetViewModel(locale: locale)) + } + + func inputPriceViewModel( + assetDisplayInfo: AssetBalanceDisplayInfo, + amount: Decimal?, + priceData: PriceData?, + locale: Locale + ) -> String? { + guard + let amount = amount, + let priceData = priceData else { + return nil + } + return balanceViewModelFactoryFacade.priceFromAmount( + targetAssetInfo: assetDisplayInfo, + amount: amount, + priceData: priceData + ).value(for: locale) + } + + func receiveTitleViewModel(locale: Locale) -> TitleHorizontalMultiValueView.Model { + TitleHorizontalMultiValueView.Model( + title: + R.string.localizable.swapsSetupAssetSelectReceiveTitle(preferredLanguages: locale.rLanguages), + subtitle: "", + value: "" + ) + } + + func receiveAssetViewModel(chainAsset: ChainAsset?, locale: Locale) -> SwapAssetInputViewModel { + chainAsset.map { .asset(assetViewModel(chainAsset: $0)) } ?? .empty(emptyReceiveAssetViewModel(locale: locale)) + } + + func rate(from params: RateParams, locale: Locale) -> String { + guard + let amountOutDecimal = Decimal.fromSubstrateAmount( + params.amountOut, + precision: params.assetDisplayInfoOut.assetPrecision + ), + let amountInDecimal = Decimal.fromSubstrateAmount( + params.amountIn, + precision: params.assetDisplayInfoIn.assetPrecision + ), + amountInDecimal != 0 else { + return "" + } + + let difference = amountOutDecimal / amountInDecimal + + let amountIn = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: params.assetDisplayInfoIn, + value: 1 + ).value(for: locale) + let amountOut = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: params.assetDisplayInfoOut, + value: difference ?? 0 + ).value(for: locale) + + return "\(amountIn) ≈ \(amountOut)" + } + + func amountInputViewModel(chainAsset: ChainAsset, amount: Decimal?, locale: Locale) -> AmountInputViewModelProtocol { + balanceViewModelFactoryFacade.createBalanceInputViewModel( + targetAssetInfo: chainAsset.assetDisplayInfo, + amount: amount + ).value(for: locale) + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 7e8237b010..b5ab019b3e 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -35,16 +35,17 @@ final class SwapSetupInteractor: AnyCancellableCleaning { let wrapper = assetConversionOperationFactory.quote(for: args) wrapper.targetOperation.completionBlock = { [weak self] in - guard self?.quoteCall === wrapper else { - return - } - do { - let result = try wrapper.targetOperation.extractNoCancellableResultData() - DispatchQueue.main.async { + DispatchQueue.main.async { + guard self?.quoteCall === wrapper else { + return + } + do { + let result = try wrapper.targetOperation.extractNoCancellableResultData() + self?.presenter?.didReceive(quote: result) + } catch { + self?.presenter?.didReceive(error: .quote(error)) } - } catch { - self?.presenter?.didReceive(error: .quote(error)) } } @@ -66,12 +67,21 @@ final class SwapSetupInteractor: AnyCancellableCleaning { let runtimeCoderFactoryOperation = runtimeService.fetchCoderFactoryOperation() runtimeCoderFactoryOperation.completionBlock = { [weak self] in - let runtimeCoderFactory = try runtimeCoderFactoryOperation.extractNoCancellableResultData() - let builder = assetConversionExtrinsicService.fetchExtrinsicBuilderClosure( - for: args, - codingFactory: runtimeCoderFactory - ) - self?.feeProxy.estimateFee(using: extrinsicService, reuseIdentifier: "", setupBy: builder) + guard let self = self else { + return + } + do { + let runtimeCoderFactory = try runtimeCoderFactoryOperation.extractNoCancellableResultData() + let builder = self.assetConversionExtrinsicService.fetchExtrinsicBuilderClosure( + for: args, + codingFactory: runtimeCoderFactory + ) + self.feeProxy.estimateFee(using: extrinsicService, reuseIdentifier: "", setupBy: builder) + } catch { + DispatchQueue.main.async { + self.presenter?.didReceive(error: .fetchFeeFailed(error)) + } + } } runtimeOperationCall = runtimeCoderFactoryOperation diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 186c4e0421..32d541a696 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -6,57 +6,283 @@ final class SwapSetupPresenter { weak var view: SwapSetupViewProtocol? let wireframe: SwapSetupWireframeProtocol let interactor: SwapSetupInteractorInputProtocol - let balanceViewModelFactory: BalanceViewModelFactoryFacadeProtocol + let viewModelFactory: SwapsSetupViewModelFactoryProtocol + + private var assetBalance: AssetBalance? + private var payChainAsset: ChainAsset? + private var payAssetPriceData: PriceData? + private var receiveAssetPriceData: PriceData? + private var receiveChainAsset: ChainAsset? + private var payAmountInput: AmountInputResult? + private var receiveAmountInput: Decimal? + private var quote: AssetConversion.Quote? + private var direction: AssetConversion.Direction? init( interactor: SwapSetupInteractorInputProtocol, wireframe: SwapSetupWireframeProtocol, - balanceViewModelFactory: BalanceViewModelFactoryFacadeProtocol, + viewModelFactory: SwapsSetupViewModelFactoryProtocol, localizationManager: LocalizationManagerProtocol ) { self.interactor = interactor self.wireframe = wireframe - self.balanceViewModelFactory = balanceViewModelFactory + self.viewModelFactory = viewModelFactory self.localizationManager = localizationManager } -} -extension SwapSetupPresenter: SwapSetupPresenterProtocol { - func setup() { - let mock = MockViewModelFactory() - let buttonState = mock.buttonState() + private func estimateFee() {} + + private func quote(amount: BigUInt, direction: AssetConversion.Direction) { + guard let assetIn = payChainAsset?.chainAssetId, + let assetOut = receiveChainAsset?.chainAssetId else { + return + } + self.direction = direction + interactor.calculateQuote(for: .init( + assetIn: assetIn, + assetOut: assetOut, + amount: amount, + direction: direction + )) + } + + private func provideButtonState() { + let buttonState = viewModelFactory.buttonState( + assetIn: payChainAsset?.chainAssetId, + assetOut: receiveChainAsset?.chainAssetId, + amountIn: absoluteValue(for: payAmountInput), + amountOut: receiveAmountInput + ) view?.didReceiveButtonState( title: buttonState.title.value(for: selectedLocale), enabled: buttonState.enabled ) - view?.didReceiveTitle(payViewModel: mock.payTitleModel(locale: selectedLocale)) - view?.didReceiveInputChainAsset(payViewModel: mock.payModel()) - view?.didReceiveAmount(payInputViewModel: mock.payAmount( - locale: selectedLocale, - balanceViewModelFactory: balanceViewModelFactory - )) - view?.didReceiveAmountInputPrice(payViewModel: mock.payPriceModel()) - view?.didReceiveTitle(receiveViewModel: mock.receiveTitleModel(locale: selectedLocale)) - view?.didReceiveInputChainAsset(receiveViewModel: mock.receiveModel(locale: selectedLocale)) } - // TODO: navigate to select token screen - func selectPayToken() {} + private func providePayTitle() { + let payTitleViewModel = viewModelFactory.payTitleViewModel( + assetDisplayInfo: payChainAsset?.assetDisplayInfo, + maxValue: assetBalance?.transferable, + locale: selectedLocale + ) + view?.didReceiveTitle(payViewModel: payTitleViewModel) + } + + private func providePayAssetViewModel() { + let payAssetViewModel = viewModelFactory.payAssetViewModel( + chainAsset: payChainAsset, + locale: selectedLocale + ) + view?.didReceiveInputChainAsset(payViewModel: payAssetViewModel) + } + + private func providePayInputPriceViewModel() { + guard let assetDisplayInfo = payChainAsset?.assetDisplayInfo else { + view?.didReceiveAmountInputPrice(payViewModel: nil) + return + } + let inputPriceViewModel = viewModelFactory.inputPriceViewModel( + assetDisplayInfo: assetDisplayInfo, + amount: absoluteValue(for: payAmountInput), + priceData: payAssetPriceData, + locale: selectedLocale + ) + view?.didReceiveAmountInputPrice(payViewModel: inputPriceViewModel) + } + + private func provideReceiveTitle() { + let receiveTitleViewModel = viewModelFactory.receiveTitleViewModel(locale: selectedLocale) + view?.didReceiveTitle(receiveViewModel: receiveTitleViewModel) + } + + private func provideReceiveAssetViewModel() { + let receiveAssetViewModel = viewModelFactory.receiveAssetViewModel( + chainAsset: receiveChainAsset, + locale: selectedLocale + ) + view?.didReceiveInputChainAsset(receiveViewModel: receiveAssetViewModel) + } - // TODO: navigate to select token screen - func selectReceiveToken() {} + private func provideReceiveInputPriceViewModel() { + guard let assetDisplayInfo = receiveChainAsset?.assetDisplayInfo else { + view?.didReceiveAmountInputPrice(receiveViewModel: nil) + return + } + let inputPriceViewModel = viewModelFactory.inputPriceViewModel( + assetDisplayInfo: assetDisplayInfo, + amount: receiveAmountInput, + priceData: receiveAssetPriceData, + locale: selectedLocale + ) + view?.didReceiveAmountInputPrice(receiveViewModel: inputPriceViewModel) + } + + private func providePayAmountInputViewModel() { + guard let payChainAsset = payChainAsset else { + return + } + let amountInputViewModel = viewModelFactory.amountInputViewModel( + chainAsset: payChainAsset, + amount: absoluteValue(for: payAmountInput), + locale: selectedLocale + ) + view?.didReceiveAmount(payInputViewModel: amountInputViewModel) + } + + private func provideReceiveAmountInputViewModel() { + guard let receiveChainAsset = receiveChainAsset else { + return + } + let amountInputViewModel = viewModelFactory.amountInputViewModel( + chainAsset: receiveChainAsset, + amount: receiveAmountInput, + locale: selectedLocale + ) + view?.didReceiveAmount(receiveInputViewModel: amountInputViewModel) + } + + private func absoluteValue(for input: AmountInputResult?) -> Decimal? { + guard + let input = input, + let assetBalance = assetBalance, + let payChainAsset = payChainAsset else { + return nil + } + guard let transferrableBalanceDecimal = + Decimal.fromSubstrateAmount( + assetBalance.transferable, + precision: payChainAsset.asset.displayInfo.assetPrecision + ) else { + return nil + } + + return input.absoluteValue(from: transferrableBalanceDecimal) + } + + private func providePayAssetViews() { + providePayTitle() + providePayAssetViewModel() + providePayInputPriceViewModel() + providePayAmountInputViewModel() + } + + private func provideReceiveAssetViews() { + provideReceiveTitle() + provideReceiveAssetViewModel() + provideReceiveInputPriceViewModel() + provideReceiveAmountInputViewModel() + } + + private func provideRateViewModel() { + guard + let assetDisplayInfoIn = payChainAsset?.assetDisplayInfo, + let assetDisplayInfoOut = receiveChainAsset?.assetDisplayInfo, + let quote = quote else { + view?.didReceiveRate(viewModel: .loading) + return + } + let rateViewModel = viewModelFactory.rate(from: .init( + assetDisplayInfoIn: assetDisplayInfoIn, + assetDisplayInfoOut: assetDisplayInfoOut, + amountIn: quote.amountIn, + amountOut: quote.amountOut + ), locale: selectedLocale) + + view?.didReceiveRate(viewModel: .loaded(value: rateViewModel)) + } +} + +extension SwapSetupPresenter: SwapSetupPresenterProtocol { + func setup() { + providePayAssetViews() + provideReceiveAssetViews() + provideButtonState() + interactor.setup() + } + + func selectPayToken() { + wireframe.showPayTokenSelection(from: view) { [weak self] chainAsset in + self?.payChainAsset = chainAsset + self?.providePayAssetViews() + } + } + + func selectReceiveToken() { + wireframe.showReceiveTokenSelection(from: view) { [weak self] chainAsset in + self?.receiveChainAsset = chainAsset + self?.provideReceiveAssetViews() + } + } + + func updatePayAmount(_ amount: Decimal?) { + payAmountInput = amount.map { .absolute($0) } + + if + let chainAsset = payChainAsset, + let amount = amount, + let amountInPlank = amount.toSubstrateAmount(precision: chainAsset.assetDisplayInfo.assetPrecision) { + quote(amount: amountInPlank, direction: .sell) + } + } + + func updateReceiveAmount(_ amount: Decimal?) { + receiveAmountInput = amount + if + let chainAsset = receiveChainAsset, + let amount = amount, + let amountInPlank = amount.toSubstrateAmount(precision: chainAsset.assetDisplayInfo.assetPrecision) { + quote(amount: amountInPlank, direction: .buy) + } + } // TODO: implement - func swap() {} + func swap() { + Swift.swap(&payChainAsset, &receiveChainAsset) + providePayAssetViews() + provideReceiveAssetViews() + provideButtonState() + } // TODO: navigate to confirm screen func proceed() {} } extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { - func didReceive(error _: SwapSetupError) {} + func didReceive(error: SwapSetupError) { + print("=================", error) + } - func didReceive(quote _: AssetConversion.Quote) {} + func didReceive(quote: AssetConversion.Quote) { + self.quote = quote + + guard + let payChainAsset = payChainAsset, + let receiveChainAsset = receiveChainAsset, + quote.assetIn == payChainAsset.chainAssetId, + quote.assetOut == receiveChainAsset.chainAssetId else { + return + } + + switch direction { + case .buy: + let payAmount = Decimal.fromSubstrateAmount( + quote.amountIn, + precision: Int16(payChainAsset.asset.precision) + ) ?? 0 + payAmountInput = .absolute(payAmount) + providePayInputPriceViewModel() + case .sell: + receiveAmountInput = Decimal.fromSubstrateAmount( + quote.amountOut, + precision: Int16(receiveChainAsset.asset.precision) + ) ?? 0 + provideReceiveAmountInputViewModel() + default: + break + } + + provideRateViewModel() + } func didReceive(fee _: BigUInt?) {} } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 7fae05b665..6d17c548b1 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -10,7 +10,7 @@ protocol SwapSetupViewProtocol: ControllerBackedProtocol { func didReceiveAmount(receiveInputViewModel inputViewModel: AmountInputViewModelProtocol) func didReceiveAmountInputPrice(receiveViewModel: String?) func didReceiveTitle(receiveViewModel viewModel: TitleHorizontalMultiValueView.Model) - func didReceiveRate(viewModel: LoadableViewModelState) + func didReceiveRate(viewModel: LoadableViewModelState) func didReceiveNetworkFee(viewModel: LoadableViewModelState) } @@ -20,9 +20,12 @@ protocol SwapSetupPresenterProtocol: AnyObject { func selectReceiveToken() func proceed() func swap() + func updatePayAmount(_ amount: Decimal?) + func updateReceiveAmount(_ amount: Decimal?) } protocol SwapSetupInteractorInputProtocol: AnyObject { + func setup() func calculateQuote(for args: AssetConversion.QuoteArgs) } @@ -32,7 +35,16 @@ protocol SwapSetupInteractorOutputProtocol: AnyObject { func didReceive(error: SwapSetupError) } -protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, ErrorPresentable {} +protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, ErrorPresentable { + func showPayTokenSelection( + from view: ControllerBackedProtocol?, + completionHandler: @escaping (ChainAsset) -> Void + ) + func showReceiveTokenSelection( + from view: ControllerBackedProtocol?, + completionHandler: @escaping (ChainAsset) -> Void + ) +} enum SwapSetupError: Error { case quote(Error) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 1300ea57f5..a8ee8b1335 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -53,6 +53,18 @@ final class SwapSetupViewController: UIViewController, ViewHolder { action: #selector(swapAction), for: .touchUpInside ) + + rootView.payAmountInputView.textInputView.addTarget( + self, + action: #selector(payAmountChangeAction), + for: .editingChanged + ) + + rootView.payAmountInputView.textInputView.addTarget( + self, + action: #selector(receiveAmountChangeAction), + for: .editingChanged + ) } private func setupLocalization() { @@ -77,6 +89,16 @@ final class SwapSetupViewController: UIViewController, ViewHolder { @objc private func swapAction() { presenter.swap() } + + @objc private func payAmountChangeAction() { + let amount = rootView.payAmountInputView.textInputView.inputViewModel?.decimalAmount + presenter.updatePayAmount(amount) + } + + @objc private func receiveAmountChangeAction() { + let amount = rootView.receiveAmountInputView.textInputView.inputViewModel?.decimalAmount + presenter.updateReceiveAmount(amount) + } } extension SwapSetupViewController: SwapSetupViewProtocol { @@ -126,7 +148,7 @@ extension SwapSetupViewController: SwapSetupViewProtocol { rootView.receiveAmountInputView.bind(priceViewModel: viewModel) } - func didReceiveRate(viewModel: LoadableViewModelState) { + func didReceiveRate(viewModel: LoadableViewModelState) { rootView.rateCell.bind(loadableViewModel: viewModel) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index c563dfbd8d..3995dd50a8 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -3,25 +3,29 @@ import SoraFoundation import RobinHood struct SwapSetupViewFactory { - static func createView() -> SwapSetupViewProtocol? { + static func createView(assetListObservable: AssetListModelObservable) -> SwapSetupViewProtocol? { guard let currencyManager = CurrencyManager.shared else { return nil } - let balanceViewModelFactory = BalanceViewModelFactoryFacade( + let balanceViewModelFactoryFacade = BalanceViewModelFactoryFacade( priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager)) guard let interactor = createInteractor() else { return nil } - let wireframe = SwapSetupWireframe() + let wireframe = SwapSetupWireframe(assetListObservable: assetListObservable) + let viewModelFactory = SwapsSetupViewModelFactory( + balanceViewModelFactoryFacade: balanceViewModelFactoryFacade, + networkViewModelFactory: NetworkViewModelFactory() + ) let presenter = SwapSetupPresenter( interactor: interactor, wireframe: wireframe, - balanceViewModelFactory: balanceViewModelFactory, + viewModelFactory: viewModelFactory, localizationManager: LocalizationManager.shared ) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index d397c0756f..88b3182948 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -1,3 +1,37 @@ import Foundation -final class SwapSetupWireframe: SwapSetupWireframeProtocol {} +final class SwapSetupWireframe: SwapSetupWireframeProtocol { + let assetListObservable: AssetListModelObservable + + init(assetListObservable: AssetListModelObservable) { + self.assetListObservable = assetListObservable + } + + func showPayTokenSelection( + from view: ControllerBackedProtocol?, + completionHandler: @escaping (ChainAsset) -> Void + ) { + guard let selectTokenView = SwapAssetsOperationViewFactory.createSelectPayTokenView( + for: assetListObservable, + selectClosure: completionHandler + ) else { + return + } + + view?.controller.navigationController?.pushViewController(selectTokenView.controller, animated: true) + } + + func showReceiveTokenSelection( + from view: ControllerBackedProtocol?, + completionHandler: @escaping (ChainAsset) -> Void + ) { + guard let selectTokenView = SwapAssetsOperationViewFactory.createSelectReceiveTokenView( + for: assetListObservable, + selectClosure: completionHandler + ) else { + return + } + + view?.controller.navigationController?.pushViewController(selectTokenView.controller, animated: true) + } +} diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift b/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift index ad1d8a99b4..cfb0d2d247 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift @@ -97,6 +97,7 @@ final class SwapAmountInput: BackgroundedContentControl { private func configureLocalHandlers() { addTarget(self, action: #selector(actionTouchUpInside), for: .touchUpInside) + textField.delegate = self } private func configureContentViewIfNeeded() { @@ -136,7 +137,6 @@ extension SwapAmountInput: AmountInputViewModelObserver { extension SwapAmountInput { func bind(inputViewModel: AmountInputViewModelProtocol) { - textField.isHidden = false self.inputViewModel?.observable.remove(observer: self) inputViewModel.observable.add(observer: self) diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift index 1ea0ebe2f9..62a5860860 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift @@ -121,6 +121,7 @@ final class SwapAmountInputView: RoundedView { extension SwapAmountInputView { func bind(assetViewModel: SwapsAssetViewModel) { + textInputView.isHidden = false assetControl.bind(assetViewModel: assetViewModel) setNeedsLayout() } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index 4dfa9c18b5..a299601f18 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -105,50 +105,7 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { @objc private func detailsControlAction() { detailsTableView.isHidden = !detailsHeaderCell.actionControl.isActivated - detailsHeaderCell.actionControl.invalidateLayout() detailsHeaderCell.setNeedsLayout() } -} - override func setupStyle() { - backgroundColor = R.color.colorSecondaryScreenBackground() - } - - override func setupLayout() { - super.setupLayout() - - stackView.layoutMargins = UIEdgeInsets(top: 12, left: 16, bottom: 0, right: 16) - - addSubview(actionButton) - actionButton.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) - make.bottom.equalTo(safeAreaLayoutGuide).inset(UIConstants.actionBottomInset) - make.height.equalTo(UIConstants.actionHeight) - } - - addArrangedSubview(payAmountView, spacingAfter: 8) - payAmountView.snp.makeConstraints { - $0.height.equalTo(18) - } - addArrangedSubview(payAmountInputView, spacingAfter: 24) - payAmountInputView.snp.makeConstraints { - $0.height.equalTo(64) - } - addArrangedSubview(receiveAmountView, spacingAfter: 8) - receiveAmountView.snp.makeConstraints { - $0.height.equalTo(18) - } - addArrangedSubview(receiveAmountInputView) - receiveAmountInputView.snp.makeConstraints { - $0.height.equalTo(64) - } - - addSubview(switchButton) - switchButton.snp.makeConstraints { - $0.height.equalTo(switchButton.snp.width) - $0.top.equalTo(payAmountInputView.snp.bottom).offset(9) - $0.bottom.equalTo(receiveAmountInputView.snp.top).offset(-9) - $0.centerX.equalTo(payAmountInputView.snp.centerX) - } - } } From 5c412dd5a311d1a333ed3b1512b3305cc865700d Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 6 Oct 2023 20:34:01 +0300 Subject: [PATCH 022/204] add fee --- .../Model/SwapsSetupViewModelFactory.swift | 33 +++++- .../Swaps/Setup/SwapSetupInteractor.swift | 79 ++++++++++++- .../Swaps/Setup/SwapSetupPresenter.swift | 110 ++++++++++++++++-- .../Swaps/Setup/SwapSetupProtocols.swift | 14 +++ .../Swaps/Setup/SwapSetupViewController.swift | 2 +- .../Swaps/Setup/SwapSetupViewFactory.swift | 7 +- .../Swaps/Setup/View/SwapAmountInput.swift | 4 +- 7 files changed, 228 insertions(+), 21 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift index 39adaab328..db3279e533 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift @@ -30,7 +30,13 @@ protocol SwapsSetupViewModelFactoryProtocol { func receiveTitleViewModel(locale: Locale) -> TitleHorizontalMultiValueView.Model func receiveAssetViewModel(chainAsset: ChainAsset?, locale: Locale) -> SwapAssetInputViewModel func amountInputViewModel(chainAsset: ChainAsset, amount: Decimal?, locale: Locale) -> AmountInputViewModelProtocol - func rate(from params: RateParams, locale: Locale) -> String + func rateViewModel(from params: RateParams, locale: Locale) -> String + func feeViewModel( + amount: BigUInt, + assetDisplayInfo: AssetBalanceDisplayInfo, + priceData: PriceData?, + locale: Locale + ) -> BalanceViewModelProtocol } final class SwapsSetupViewModelFactory { @@ -191,7 +197,7 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { chainAsset.map { .asset(assetViewModel(chainAsset: $0)) } ?? .empty(emptyReceiveAssetViewModel(locale: locale)) } - func rate(from params: RateParams, locale: Locale) -> String { + func rateViewModel(from params: RateParams, locale: Locale) -> String { guard let amountOutDecimal = Decimal.fromSubstrateAmount( params.amountOut, @@ -219,10 +225,31 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { return "\(amountIn) ≈ \(amountOut)" } - func amountInputViewModel(chainAsset: ChainAsset, amount: Decimal?, locale: Locale) -> AmountInputViewModelProtocol { + func amountInputViewModel( + chainAsset: ChainAsset, + amount: Decimal?, + locale: Locale + ) -> AmountInputViewModelProtocol { balanceViewModelFactoryFacade.createBalanceInputViewModel( targetAssetInfo: chainAsset.assetDisplayInfo, amount: amount ).value(for: locale) } + + func feeViewModel( + amount: BigUInt, + assetDisplayInfo: AssetBalanceDisplayInfo, + priceData: PriceData?, + locale: Locale + ) -> BalanceViewModelProtocol { + let amountDecimal = Decimal.fromSubstrateAmount( + amount, + precision: assetDisplayInfo.assetPrecision + ) ?? 0 + return balanceViewModelFactoryFacade.balanceFromPrice( + targetAssetInfo: assetDisplayInfo, + amount: amountDecimal, + priceData: priceData + ).value(for: locale) + } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index b5ab019b3e..49b14b9b25 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -2,17 +2,24 @@ import UIKit import RobinHood import BigInt -final class SwapSetupInteractor: AnyCancellableCleaning { +final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning { weak var presenter: SwapSetupInteractorOutputProtocol? let assetConversionOperationFactory: AssetConversionOperationFactoryProtocol let assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol let runtimeService: RuntimeProviderProtocol let feeProxy: ExtrinsicFeeProxyProtocol let extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol + let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol + let currencyManager: CurrencyManagerProtocol + let selectedAccount: MetaAccountModel private let operationQueue: OperationQueue private var quoteCall: CancellableCall? private var runtimeOperationCall: CancellableCall? + private var extrinsicService: ExtrinsicServiceProtocol? + private var accountId: AccountId? + + private var priceProviders: [ChainAssetId: StreamableProvider] = [:] init( assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, @@ -20,6 +27,9 @@ final class SwapSetupInteractor: AnyCancellableCleaning { runtimeService: RuntimeProviderProtocol, feeProxy: ExtrinsicFeeProxyProtocol, extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + currencyManager: CurrencyManagerProtocol, + selectedAccount: MetaAccountModel, operationQueue: OperationQueue ) { self.assetConversionOperationFactory = assetConversionOperationFactory @@ -27,9 +37,25 @@ final class SwapSetupInteractor: AnyCancellableCleaning { self.runtimeService = runtimeService self.feeProxy = feeProxy self.extrinsicServiceFactory = extrinsicServiceFactory + self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory + self.currencyManager = currencyManager + self.selectedAccount = selectedAccount self.operationQueue = operationQueue } + private func performPriceSubscription(chainAsset: ChainAsset) { + clear(streamableProvider: &priceProviders[chainAsset.chainAssetId]) + + guard let priceId = chainAsset.asset.priceId else { + return + } + + priceProviders[chainAsset.chainAssetId] = subscribeToPrice( + for: priceId, + currency: currencyManager.selectedCurrency + ) + } + private func quote(args: AssetConversion.QuoteArgs) { clear(cancellable: "eCall) @@ -53,14 +79,22 @@ final class SwapSetupInteractor: AnyCancellableCleaning { operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) } - private func extrinsicService() -> ExtrinsicServiceProtocol? { - nil + private func update(chainModel: ChainModel) { + guard let metaChainAccountResponse = selectedAccount.fetchMetaChainAccount( + for: chainModel.accountRequest() + ) else { + return + } + extrinsicService = extrinsicServiceFactory.createService( + account: metaChainAccountResponse.chainAccount, + chain: chainModel + ) + accountId = metaChainAccountResponse.chainAccount.accountId } private func fee(args: AssetConversion.CallArgs) { clear(cancellable: &runtimeOperationCall) - guard let extrinsicService = extrinsicService() else { - presenter?.didReceive(error: .fetchFeeFailed(CommonError.undefined)) + guard let extrinsicService = extrinsicService else { return } @@ -97,6 +131,30 @@ extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { func calculateQuote(for args: AssetConversion.QuoteArgs) { quote(args: args) } + + func set(chainModel: ChainModel) { + update(chainModel: chainModel) + } + + func calculateFee(for args: FeeArgs) { + guard let receiver = accountId else { + return + } + fee(args: .init( + assetIn: args.assetIn, + amountIn: args.amountIn, + assetOut: args.assetOut, + amountOut: args.amountOut, + receiver: receiver, + direction: args.direction, + slippage: .percent(of: args.slippage) + )) + } + + func performSubscriptions(chainAsset: ChainAsset) { + // TODO: Add subscription to balance + performPriceSubscription(chainAsset: chainAsset) + } } extension SwapSetupInteractor: ExtrinsicFeeProxyDelegate { @@ -110,3 +168,14 @@ extension SwapSetupInteractor: ExtrinsicFeeProxyDelegate { } } } + +extension SwapSetupInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { + func handlePrice(result: Result, priceId: AssetModel.PriceId) { + switch result { + case let .success(priceData): + presenter?.didReceive(price: priceData, priceId: priceId) + case let .failure(error): + presenter?.didReceive(error: .price(error, priceId)) + } + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 32d541a696..0708893c87 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -17,6 +17,7 @@ final class SwapSetupPresenter { private var receiveAmountInput: Decimal? private var quote: AssetConversion.Quote? private var direction: AssetConversion.Direction? + private var fee: BigUInt? init( interactor: SwapSetupInteractorInputProtocol, @@ -30,8 +31,6 @@ final class SwapSetupPresenter { self.localizationManager = localizationManager } - private func estimateFee() {} - private func quote(amount: BigUInt, direction: AssetConversion.Direction) { guard let assetIn = payChainAsset?.chainAssetId, let assetOut = receiveChainAsset?.chainAssetId else { @@ -144,13 +143,12 @@ final class SwapSetupPresenter { private func absoluteValue(for input: AmountInputResult?) -> Decimal? { guard let input = input, - let assetBalance = assetBalance, let payChainAsset = payChainAsset else { return nil } guard let transferrableBalanceDecimal = Decimal.fromSubstrateAmount( - assetBalance.transferable, + assetBalance?.transferable ?? 0, precision: payChainAsset.asset.displayInfo.assetPrecision ) else { return nil @@ -181,7 +179,7 @@ final class SwapSetupPresenter { view?.didReceiveRate(viewModel: .loading) return } - let rateViewModel = viewModelFactory.rate(from: .init( + let rateViewModel = viewModelFactory.rateViewModel(from: .init( assetDisplayInfoIn: assetDisplayInfoIn, assetDisplayInfoOut: assetDisplayInfoOut, amountIn: quote.amountIn, @@ -190,6 +188,67 @@ final class SwapSetupPresenter { view?.didReceiveRate(viewModel: .loaded(value: rateViewModel)) } + + private func provideFeeViewModel() { + // TODO: chainAsset from user choice + guard let payChainAsset = payChainAsset, receiveChainAsset != nil else { + return + } + guard let fee = fee else { + view?.didReceiveNetworkFee(viewModel: .loading) + return + } + let viewModel = viewModelFactory.feeViewModel( + amount: fee, + assetDisplayInfo: payChainAsset.assetDisplayInfo, + priceData: payAssetPriceData, + locale: selectedLocale + ) + + view?.didReceiveNetworkFee(viewModel: .loaded(value: viewModel)) + } + + private func estimateFee() { + guard + let payChainAsset = payChainAsset, + let receiveChainAsset = receiveChainAsset, + let payInPlank = absoluteValue(for: payAmountInput)?.toSubstrateAmount( + precision: Int16(payChainAsset.asset.precision)), + let receiveInPlank = receiveAmountInput?.toSubstrateAmount(precision: Int16(receiveChainAsset.asset.precision)) + else { + return + } + + interactor.calculateFee(for: .init( + assetIn: payChainAsset.chainAssetId, + amountIn: payInPlank, + assetOut: receiveChainAsset.chainAssetId, + amountOut: receiveInPlank, + direction: .sell, + slippage: 1 + )) + } + + private func refreshQuote() { + guard + let payChainAsset = payChainAsset, + let receiveChainAsset = receiveChainAsset, + let payInPlank = absoluteValue(for: payAmountInput)?.toSubstrateAmount( + precision: Int16(payChainAsset.asset.precision)), + let receiveInPlank = receiveAmountInput?.toSubstrateAmount(precision: Int16(receiveChainAsset.asset.precision)) + else { + return + } + + switch direction { + case .buy: + quote(amount: receiveInPlank, direction: .buy) + case .sell: + quote(amount: payInPlank, direction: .sell) + default: + break + } + } } extension SwapSetupPresenter: SwapSetupPresenterProtocol { @@ -204,13 +263,17 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { wireframe.showPayTokenSelection(from: view) { [weak self] chainAsset in self?.payChainAsset = chainAsset self?.providePayAssetViews() + self?.interactor.set(chainModel: chainAsset.chain) + self?.interactor.performSubscriptions(chainAsset: chainAsset) } } func selectReceiveToken() { wireframe.showReceiveTokenSelection(from: view) { [weak self] chainAsset in + self?.interactor.set(chainModel: chainAsset.chain) self?.receiveChainAsset = chainAsset self?.provideReceiveAssetViews() + self?.interactor.performSubscriptions(chainAsset: chainAsset) } } @@ -222,20 +285,22 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { let amount = amount, let amountInPlank = amount.toSubstrateAmount(precision: chainAsset.assetDisplayInfo.assetPrecision) { quote(amount: amountInPlank, direction: .sell) + estimateFee() } } func updateReceiveAmount(_ amount: Decimal?) { receiveAmountInput = amount + if let chainAsset = receiveChainAsset, let amount = amount, let amountInPlank = amount.toSubstrateAmount(precision: chainAsset.assetDisplayInfo.assetPrecision) { quote(amount: amountInPlank, direction: .buy) + estimateFee() } } - // TODO: implement func swap() { Swift.swap(&payChainAsset, &receiveChainAsset) providePayAssetViews() @@ -249,12 +314,23 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { func didReceive(error: SwapSetupError) { - print("=================", error) + switch error { + case .quote: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.refreshQuote() + } + case .fetchFeeFailed: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.estimateFee() + } + case let .price(_, priceId): + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.estimateFee() + } + } } func didReceive(quote: AssetConversion.Quote) { - self.quote = quote - guard let payChainAsset = payChainAsset, let receiveChainAsset = receiveChainAsset, @@ -262,6 +338,7 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { quote.assetOut == receiveChainAsset.chainAssetId else { return } + self.quote = quote switch direction { case .buy: @@ -284,7 +361,20 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { provideRateViewModel() } - func didReceive(fee _: BigUInt?) {} + func didReceive(fee: BigUInt?) { + self.fee = fee + provideFeeViewModel() + } + + func didReceive(price: PriceData?, priceId: AssetModel.PriceId) { + if payChainAsset?.asset.priceId == priceId { + payAssetPriceData = price + providePayInputPriceViewModel() + } else if receiveChainAsset?.asset.priceId == priceId { + receiveAssetPriceData = price + provideReceiveInputPriceViewModel() + } + } } extension SwapSetupPresenter: Localizable { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 6d17c548b1..da9989b126 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -26,13 +26,17 @@ protocol SwapSetupPresenterProtocol: AnyObject { protocol SwapSetupInteractorInputProtocol: AnyObject { func setup() + func set(chainModel: ChainModel) func calculateQuote(for args: AssetConversion.QuoteArgs) + func calculateFee(for args: FeeArgs) + func performSubscriptions(chainAsset: ChainAsset) } protocol SwapSetupInteractorOutputProtocol: AnyObject { func didReceive(quote: AssetConversion.Quote) func didReceive(fee: BigUInt?) func didReceive(error: SwapSetupError) + func didReceive(price: PriceData?, priceId: AssetModel.PriceId) } protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, ErrorPresentable { @@ -49,4 +53,14 @@ protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryabl enum SwapSetupError: Error { case quote(Error) case fetchFeeFailed(Error) + case price(Error, AssetModel.PriceId) +} + +struct FeeArgs { + let assetIn: ChainAssetId + let amountIn: BigUInt + let assetOut: ChainAssetId + let amountOut: BigUInt + let direction: AssetConversion.Direction + let slippage: BigUInt } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index a8ee8b1335..10756ada26 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -60,7 +60,7 @@ final class SwapSetupViewController: UIViewController, ViewHolder { for: .editingChanged ) - rootView.payAmountInputView.textInputView.addTarget( + rootView.receiveAmountInputView.textInputView.addTarget( self, action: #selector(receiveAmountChangeAction), for: .editingChanged diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index 3995dd50a8..4794c04205 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -46,7 +46,9 @@ struct SwapSetupViewFactory { guard let connection = chainRegistry.getConnection(for: westmintChainId), let runtimeService = chainRegistry.getRuntimeProvider(for: westmintChainId), - let chainModel = chainRegistry.getChain(for: westmintChainId) else { + let chainModel = chainRegistry.getChain(for: westmintChainId), + let currencyManager = CurrencyManager.shared, + let selectedAccount = SelectedWalletSettings.shared.value else { return nil } @@ -70,6 +72,9 @@ struct SwapSetupViewFactory { runtimeService: runtimeService, feeProxy: ExtrinsicFeeProxy(), extrinsicServiceFactory: extrinsicServiceFactory, + priceLocalSubscriptionFactory: PriceProviderFactory.shared, + currencyManager: currencyManager, + selectedAccount: selectedAccount, operationQueue: operationQueue ) diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift b/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift index cfb0d2d247..e4627878ca 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift @@ -131,7 +131,9 @@ extension SwapAmountInput: AmountInputViewModelObserver { func amountInputDidChange() { textField.text = inputViewModel?.displayAmount - sendActions(for: .editingChanged) + if textField.isEditing { + sendActions(for: .editingChanged) + } } } From 8afc956b8396299db7ec946ccd846282bb919822 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 6 Oct 2023 20:38:14 +0300 Subject: [PATCH 023/204] fixes --- .../View/StackTable/StackTableCollapsableHeaderCell.swift | 2 +- .../Modules/Swaps/Setup/View/SwapSetupViewLayout.swift | 2 +- novawallet/ru.lproj/Localizable.strings | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/novawallet/Common/View/StackTable/StackTableCollapsableHeaderCell.swift b/novawallet/Common/View/StackTable/StackTableCollapsableHeaderCell.swift index e2b5b3daa3..d8ebf04def 100644 --- a/novawallet/Common/View/StackTable/StackTableCollapsableHeaderCell.swift +++ b/novawallet/Common/View/StackTable/StackTableCollapsableHeaderCell.swift @@ -2,7 +2,7 @@ import UIKit import SnapKit import SoraUI -final class CollapsableView: UIView { +final class CollapsableViewHeader: UIView { var titleLabel = UILabel(style: .footnoteSecondary, textAlignment: .left, numberOfLines: 1) var actionControl: ActionTitleControl = .create { $0.indicator = ResizableImageActionIndicator(size: .init(width: 24, height: 24)) diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index a299601f18..d1df41de50 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -27,7 +27,7 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { $0.imageWithTitleView?.iconImage = R.image.iconActionSwap() } - let detailsHeaderCell: CollapsableView = .create { + let detailsHeaderCell: CollapsableViewHeader = .create { $0.actionControl.addTarget(self, action: #selector(detailsControlAction), for: .valueChanged) $0.actionControl.imageView.isUserInteractionEnabled = false } diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 13fe9bfcdd..5048b5b358 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -475,7 +475,7 @@ "staking.story.validator.page.1" = "Валидатор обеспечивает работу ноды блокчейна 24/7 и обязан иметь необходимое количество стейка (общий стейк самого валидатора и его номинаторов), чтобы быть избранным сетью. Валидаторы должны поддерживать производительность и надежность своих нод, за что они получают вознаграждения. Валидатор — это полноценная работа, существуют профильные компании, которые специализируются на валидировании в блокчейн сетях."; "staking.story.validator.page.2" = "Любой может стать валидатором и запустить ноду блокчейна, однако это требует определённых технических знаний и ответственности. Сети Polkadot и Kusama запустили программу Thousand Validators Programme (Программа Тысячи Валидаторов), чтобы помочь начинающим. Более того, сеть всегда будет стремиться вознаграждать тех валидаторов, чей суммарный стейк меньше (но достаточен чтобы быть избранным в сети), для поддержки децентрализации."; "staking.story.reward.title" = "Получение наград"; -"staking.story.reward.page.1" = "Вознаграждения за стейкинг доступны для выплаты в конце каждой эры (6 часов в Kusama и 24 часа в Polkadot). Сеть хранит ож��даемые вознаграждения в течении 84 эр и в большинстве случаев валидаторы сами выплачивают всем награды. Однако, валидаторы могут забыть это сделать или с ними может что-то случиться, поэтому номинаторы могут выплатить свои награды самостоятельно."; +"staking.story.reward.page.1" = "Вознаграждения за стейкинг доступны для выплаты в конце каждой эры (6 часов в Kusama и 24 часа в Polkadot). Сеть хранит ожидаемые вознаграждения в течении 84 эр и в большинстве случаев валидаторы сами выплачивают всем награды. Однако, валидаторы могут забыть это сделать или с ними может что-то случиться, поэтому номинаторы могут выплатить свои награды самостоятельно."; "staking.story.reward.page.2" = "Несмотря на то, что обычно вознаграждения выплачиваются валидаторами, Nova Wallet помогает узнать о вознаграждениях, срок выплаты которых близок к истечению, с помощью предупреждений. Предупреждения об этом и других важных событиях появятся на главном экране стейкинга."; "common.cancel.operation.message" = "Вы уверены, что хотите отменить операцию?"; "common.cancel.operation.action" = "Отменить операцию"; @@ -792,7 +792,7 @@ "parachain.staking.alert.collators.change" = "Один из ваших коллаторов не выбран в текущем раунде"; "parachain.staking.change.collator" = "Смена коллатора"; "parastk.manage.collators" = "Управление коллаторами"; -"parastk.pending.revoke.message" = "Вы не можете добавить стейк в коллатора, для котор��го вы разблокируете все токены."; +"parastk.pending.revoke.message" = "Вы не можете добавить стейк в коллатора, для которого вы разблокируете все токены."; "parastk.cant.bond.more.title" = "Невозможно добавить стейк в выбранного коллатора"; "parastk.not.active.collator.message" = "Выбранный коллатор намерен прекратить участие в стейкинге."; "parastk.not.active.collator.title" = "Невозможно застейкать с выбранным коллатором"; @@ -948,7 +948,7 @@ "common.on" = "Вкл"; "yield.boost.threshold" = "Порог Boost"; "yield.boost.setup.updated.period.details" = "чтобы автоматически, %@ (раньше: %@), отправлять все мои переводимые токены выше"; -"yield.boost.setup.new.period.details" = "чтобы автоматически, %@, отправлять ��се мои переводимые токены выше"; +"yield.boost.setup.new.period.details" = "чтобы автоматически, %@, отправлять все мои переводимые токены выше"; "yield.boost.setup.reward.comparison.title" = "Я хочу стейкать"; "yield.boost.setup.collator.title" = "Для моего коллатора"; "with.yield.boost" = "с Yield Boost"; From 29bb6994cd4e1463afc05bf478405be9b826b444 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Mon, 9 Oct 2023 14:35:49 +0300 Subject: [PATCH 024/204] PR fixes --- novawallet/Common/Model/BigRational.swift | 2 +- .../StackTitleMultiValueEditCell.swift | 55 +++++++++++-------- .../Model/AssetConversion.swift | 2 +- .../Swaps/SwapAssetOperationWireframe.swift | 2 +- .../Swaps/Setup/SwapSetupInteractor.swift | 49 ++++++++++------- .../Swaps/Setup/SwapSetupPresenter.swift | 36 ++++++------ .../Swaps/Setup/SwapSetupProtocols.swift | 15 +++-- .../Swaps/Setup/SwapSetupViewController.swift | 16 ++++++ .../Swaps/Setup/SwapSetupWireframe.swift | 12 +++- .../Setup/View/SwapSetupViewLayout.swift | 2 +- 10 files changed, 113 insertions(+), 78 deletions(-) diff --git a/novawallet/Common/Model/BigRational.swift b/novawallet/Common/Model/BigRational.swift index c561509cb4..1d12890064 100644 --- a/novawallet/Common/Model/BigRational.swift +++ b/novawallet/Common/Model/BigRational.swift @@ -1,7 +1,7 @@ import Foundation import BigInt -struct BigRational { +struct BigRational: Hashable { let numerator: BigUInt let denominator: BigUInt diff --git a/novawallet/Common/View/StackTable/StackTitleMultiValueEditCell.swift b/novawallet/Common/View/StackTable/StackTitleMultiValueEditCell.swift index ee85983c6c..2f01d1ff32 100644 --- a/novawallet/Common/View/StackTable/StackTitleMultiValueEditCell.swift +++ b/novawallet/Common/View/StackTable/StackTitleMultiValueEditCell.swift @@ -1,12 +1,12 @@ import Foundation import UIKit +import SoraUI +import Kingfisher -final class StackTitleMultiValueEditCell: RowView>> { - var titleLabel: UILabel { rowContentView.titleView.detailsLabel } - var titleImageView: UIImageView { rowContentView.titleView.imageView } - var topValueImageView: UIImageView { rowContentView.valueView.fView.imageView } - var topValueLabel: UILabel { rowContentView.valueView.fView.detailsLabel } - var bottomValueLabel: UILabel { rowContentView.valueView.sView } +final class StackTitleMultiValueEditCell: RowView>> { + var titleButton: RoundedButton { rowContentView.titleView } + var valueTopButton: RoundedButton { rowContentView.valueView.fView } + var valueBottomLabel: UILabel { rowContentView.valueView.sView } convenience init() { self.init(frame: CGRect(origin: .zero, size: CGSize(width: 340, height: 44.0))) @@ -26,23 +26,29 @@ final class StackTitleMultiValueEditCell: RowView, for _: TransactionFeeId) { - switch result { - case let .success(dispatchInfo): - let fee = BigUInt(dispatchInfo.fee) - presenter?.didReceive(fee: fee) - case let .failure(error): - presenter?.didReceive(error: .fetchFeeFailed(error)) + DispatchQueue.main.async { + switch result { + case let .success(dispatchInfo): + let fee = BigUInt(dispatchInfo.fee) + self.presenter?.didReceive(fee: fee) + case let .failure(error): + self.presenter?.didReceive(error: .fetchFeeFailed(error)) + } } } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 0708893c87..953ad984d6 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -209,24 +209,15 @@ final class SwapSetupPresenter { } private func estimateFee() { - guard - let payChainAsset = payChainAsset, - let receiveChainAsset = receiveChainAsset, - let payInPlank = absoluteValue(for: payAmountInput)?.toSubstrateAmount( - precision: Int16(payChainAsset.asset.precision)), - let receiveInPlank = receiveAmountInput?.toSubstrateAmount(precision: Int16(receiveChainAsset.asset.precision)) - else { + guard let quote = quote else { return } - interactor.calculateFee(for: .init( - assetIn: payChainAsset.chainAssetId, - amountIn: payInPlank, - assetOut: receiveChainAsset.chainAssetId, - amountOut: receiveInPlank, - direction: .sell, - slippage: 1 - )) + // TODO: Remove hardcode slippage and direction + interactor.calculateFee( + for: quote, + slippage: .init(direction: .sell, slippage: 1) + ) } private func refreshQuote() { @@ -263,17 +254,15 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { wireframe.showPayTokenSelection(from: view) { [weak self] chainAsset in self?.payChainAsset = chainAsset self?.providePayAssetViews() - self?.interactor.set(chainModel: chainAsset.chain) - self?.interactor.performSubscriptions(chainAsset: chainAsset) + self?.interactor.update(payChainAsset: chainAsset) } } func selectReceiveToken() { wireframe.showReceiveTokenSelection(from: view) { [weak self] chainAsset in - self?.interactor.set(chainModel: chainAsset.chain) self?.receiveChainAsset = chainAsset self?.provideReceiveAssetViews() - self?.interactor.performSubscriptions(chainAsset: chainAsset) + self?.interactor.update(receiveChainAsset: chainAsset) } } @@ -308,6 +297,15 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideButtonState() } + // TODO: show editing fee + func showFeeActions() {} + + // TODO: show fee information + func showFeeInfo() {} + + // TODO: show rate information + func showRateInfo() {} + // TODO: navigate to confirm screen func proceed() {} } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index da9989b126..fcbd43c6fe 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -22,14 +22,17 @@ protocol SwapSetupPresenterProtocol: AnyObject { func swap() func updatePayAmount(_ amount: Decimal?) func updateReceiveAmount(_ amount: Decimal?) + func showFeeActions() + func showFeeInfo() + func showRateInfo() } protocol SwapSetupInteractorInputProtocol: AnyObject { func setup() - func set(chainModel: ChainModel) + func update(receiveChainAsset: ChainAsset) + func update(payChainAsset: ChainAsset) func calculateQuote(for args: AssetConversion.QuoteArgs) - func calculateFee(for args: FeeArgs) - func performSubscriptions(chainAsset: ChainAsset) + func calculateFee(for quote: AssetConversion.Quote, slippage: SwapSlippage) } protocol SwapSetupInteractorOutputProtocol: AnyObject { @@ -56,11 +59,7 @@ enum SwapSetupError: Error { case price(Error, AssetModel.PriceId) } -struct FeeArgs { - let assetIn: ChainAssetId - let amountIn: BigUInt - let assetOut: ChainAssetId - let amountOut: BigUInt +struct SwapSlippage { let direction: AssetConversion.Direction let slippage: BigUInt } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 10756ada26..7ef1569686 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -65,6 +65,10 @@ final class SwapSetupViewController: UIViewController, ViewHolder { action: #selector(receiveAmountChangeAction), for: .editingChanged ) + + rootView.rateCell.addTarget(self, action: #selector(rateInfoAction), for: .touchUpInside) + rootView.networkFeeCell.valueTopButton.addTarget(self, action: #selector(changeNetworkFeeAction), for: .touchUpInside) + rootView.networkFeeCell.titleButton.addTarget(self, action: #selector(networkFeeInfoAction), for: .touchUpInside) } private func setupLocalization() { @@ -99,6 +103,18 @@ final class SwapSetupViewController: UIViewController, ViewHolder { let amount = rootView.receiveAmountInputView.textInputView.inputViewModel?.decimalAmount presenter.updateReceiveAmount(amount) } + + @objc private func changeNetworkFeeAction() { + presenter.showFeeActions() + } + + @objc private func networkFeeInfoAction() { + presenter.showFeeInfo() + } + + @objc private func rateInfoAction() { + presenter.showRateInfo() + } } extension SwapSetupViewController: SwapSetupViewProtocol { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index 88b3182948..edc822ec5d 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -18,7 +18,11 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { return } - view?.controller.navigationController?.pushViewController(selectTokenView.controller, animated: true) + let navigationController = NovaNavigationController( + rootViewController: selectTokenView.controller + ) + + view?.controller.present(navigationController, animated: true, completion: nil) } func showReceiveTokenSelection( @@ -32,6 +36,10 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { return } - view?.controller.navigationController?.pushViewController(selectTokenView.controller, animated: true) + let navigationController = NovaNavigationController( + rootViewController: selectTokenView.controller + ) + + view?.controller.present(navigationController, animated: true, completion: nil) } } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index d1df41de50..ea1a0db89c 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -99,7 +99,7 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { preferredLanguages: locale.rLanguages ) rateCell.titleLabel.text = R.string.localizable.swapsSetupDetailsRate(preferredLanguages: locale.rLanguages) - networkFeeCell.titleLabel.text = R.string.localizable.commonNetwork(preferredLanguages: locale.rLanguages) + networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetwork(preferredLanguages: locale.rLanguages) } @objc From 2a9a3c7fac8f666d21f6a54bd1206612d41040e0 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 10 Oct 2023 01:53:50 +0300 Subject: [PATCH 025/204] add shimmering, fix collapse animation --- novawallet.xcodeproj/project.pbxproj | 24 ++- .../View/CollapsableContainerView.swift | 177 ++++++++++++++++++ .../StackTableCollapsableHeaderCell.swift | 45 ----- .../StackTable/StackTitleMultiValueCell.swift | 57 +++++- .../Swaps/Setup/SwapSetupPresenter.swift | 82 +++++--- .../Swaps/Setup/SwapSetupProtocols.swift | 1 + .../Swaps/Setup/SwapSetupViewController.swift | 22 ++- .../Setup/View/SwapAmountInputView.swift | 2 +- .../Swaps/Setup/View/SwapAssetControl.swift | 4 +- .../Swaps/Setup/View/SwapAssetView.swift | 2 + .../Swaps/Setup/View/SwapDetailsView.swift | 15 ++ .../Setup/View/SwapNetworkFeeView.swift} | 75 ++++++-- .../Swaps/Setup/View/SwapRateView.swift | 97 ++++++++++ .../Setup/View/SwapSetupViewLayout.swift | 38 +--- 14 files changed, 507 insertions(+), 134 deletions(-) create mode 100644 novawallet/Common/View/CollapsableContainerView.swift delete mode 100644 novawallet/Common/View/StackTable/StackTableCollapsableHeaderCell.swift create mode 100644 novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift rename novawallet/{Common/View/StackTable/StackTitleMultiValueEditCell.swift => Modules/Swaps/Setup/View/SwapNetworkFeeView.swift} (53%) create mode 100644 novawallet/Modules/Swaps/Setup/View/SwapRateView.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 7ac270b7d3..5ebc3a5f67 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -701,6 +701,9 @@ 775F194D2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775F194C2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift */; }; 775F19512A5811FA009915B6 /* StartStakingParachainInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775F19502A5811FA009915B6 /* StartStakingParachainInteractor.swift */; }; 775F19532A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775F19522A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift */; }; + 77740BBC2AD4A7B800E8C06F /* CollapsableContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBB2AD4A7B800E8C06F /* CollapsableContainerView.swift */; }; + 77740BBE2AD4A7F500E8C06F /* SwapDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */; }; + 77740BC02AD4A80D00E8C06F /* SwapRateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */; }; 77799ADD2A74219A00B7E564 /* ButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799ADC2A74219A00B7E564 /* ButtonState.swift */; }; 77799ADF2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799ADE2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift */; }; 77799AE52A792AE700B7E564 /* StakingTypeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799AE42A792AE700B7E564 /* StakingTypeViewModel.swift */; }; @@ -792,8 +795,7 @@ 77EA2A292A333C1500B0670B /* arrays_input.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A1F2A333C1500B0670B /* arrays_input.json */; }; 77EA2A2A2A333C1500B0670B /* weird_input.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A202A333C1500B0670B /* weird_input.json */; }; 77EA2A2B2A333C1500B0670B /* structures_input.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A212A333C1500B0670B /* structures_input.json */; }; - 77ECB4702ACEEE2E0015CE9F /* StackTitleMultiValueEditCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77ECB46F2ACEEE2D0015CE9F /* StackTitleMultiValueEditCell.swift */; }; - 77ECB4722ACEF12E0015CE9F /* StackTableCollapsableHeaderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77ECB4712ACEF12E0015CE9F /* StackTableCollapsableHeaderCell.swift */; }; + 77ECB4702ACEEE2E0015CE9F /* SwapNetworkFeeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77ECB46F2ACEEE2D0015CE9F /* SwapNetworkFeeView.swift */; }; 77ED167A2A0CF41700E1FC8C /* StakingRewardFiltersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77ED16792A0CF41600E1FC8C /* StakingRewardFiltersViewModel.swift */; }; 77ED167C2A0CF42E00E1FC8C /* StakingRewardFiltersPeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77ED167B2A0CF42E00E1FC8C /* StakingRewardFiltersPeriod.swift */; }; 77ED167E2A0D0AE900E1FC8C /* Lenses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77ED167D2A0D0AE900E1FC8C /* Lenses.swift */; }; @@ -4702,6 +4704,9 @@ 775F194C2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoRelaychainPresenter.swift; sourceTree = ""; }; 775F19502A5811FA009915B6 /* StartStakingParachainInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingParachainInteractor.swift; sourceTree = ""; }; 775F19522A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoParachainPresenter.swift; sourceTree = ""; }; + 77740BBB2AD4A7B800E8C06F /* CollapsableContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsableContainerView.swift; sourceTree = ""; }; + 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapDetailsView.swift; sourceTree = ""; }; + 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRateView.swift; sourceTree = ""; }; 77799ADC2A74219A00B7E564 /* ButtonState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonState.swift; sourceTree = ""; }; 77799ADE2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoRelaychainWireframe.swift; sourceTree = ""; }; 77799AE42A792AE700B7E564 /* StakingTypeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingTypeViewModel.swift; sourceTree = ""; }; @@ -4794,8 +4799,7 @@ 77EA2A1F2A333C1500B0670B /* arrays_input.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = arrays_input.json; sourceTree = ""; }; 77EA2A202A333C1500B0670B /* weird_input.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = weird_input.json; sourceTree = ""; }; 77EA2A212A333C1500B0670B /* structures_input.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = structures_input.json; sourceTree = ""; }; - 77ECB46F2ACEEE2D0015CE9F /* StackTitleMultiValueEditCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackTitleMultiValueEditCell.swift; sourceTree = ""; }; - 77ECB4712ACEF12E0015CE9F /* StackTableCollapsableHeaderCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackTableCollapsableHeaderCell.swift; sourceTree = ""; }; + 77ECB46F2ACEEE2D0015CE9F /* SwapNetworkFeeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeView.swift; sourceTree = ""; }; 77ED16792A0CF41600E1FC8C /* StakingRewardFiltersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardFiltersViewModel.swift; sourceTree = ""; }; 77ED167B2A0CF42E00E1FC8C /* StakingRewardFiltersPeriod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardFiltersPeriod.swift; sourceTree = ""; }; 77ED167D2A0D0AE900E1FC8C /* Lenses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lenses.swift; sourceTree = ""; }; @@ -9545,6 +9549,9 @@ 774091FD2ACC054B00172516 /* SwapAssetControl.swift */, 774091FF2ACC1BE400172516 /* SwapAmountInput.swift */, CDD43373A9159E0A592077BB /* SwapSetupViewLayout.swift */, + 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */, + 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */, + 77ECB46F2ACEEE2D0015CE9F /* SwapNetworkFeeView.swift */, ); path = View; sourceTree = ""; @@ -12203,10 +12210,8 @@ 8489A6CF27FD5B9E0040C066 /* StackActionView.swift */, 8489A6D127FD5FB80040C066 /* StackActionCell.swift */, 844D2A3F281B0ED70049CF5E /* StackTableHeaderCell.swift */, - 77ECB4712ACEF12E0015CE9F /* StackTableCollapsableHeaderCell.swift */, 844D2A41281B24510049CF5E /* StackUrlCell.swift */, 844D2A43281B28FB0049CF5E /* StackTitleMultiValueCell.swift */, - 77ECB46F2ACEEE2D0015CE9F /* StackTitleMultiValueEditCell.swift */, 845AADA22902D1EA00B5AE96 /* StackTitleValueDiffCell.swift */, 847012652982AE5700F29C87 /* StackTableView+Cell.swift */, 849D14C92994D9BC0048E947 /* StackIconTitleValueCell.swift */, @@ -13574,6 +13579,7 @@ 7796C7022A17846B00D56094 /* EmptyCellContentView.swift */, 842D8B772A4098C300660005 /* ShimmeringLabel.swift */, 88A95FA728FAA99D00BE26F3 /* DAppView.swift */, + 77740BBB2AD4A7B800E8C06F /* CollapsableContainerView.swift */, ); path = View; sourceTree = ""; @@ -19587,7 +19593,6 @@ 84FBECF92927403100FBEB83 /* EvmAssetContractId.swift in Sources */, 842EBB2F28909A7900B952D8 /* RoundedIconTitleHeaderView.swift in Sources */, 8472979A260B3095009B86D0 /* InitBondSelectValidatorsStartWireframe.swift in Sources */, - 77ECB4722ACEF12E0015CE9F /* StackTableCollapsableHeaderCell.swift in Sources */, 77A6F5CD2A31C4AA004AFD1A /* Web3TransferRecipientIntegrityVerifierFactory.swift in Sources */, 77A6F5CF2A31C4D4004AFD1A /* Web3TransferRecipientRepositoryFactory.swift in Sources */, 84BAD215293B0E8A00C55C49 /* UITableView+Section.swift in Sources */, @@ -20468,7 +20473,7 @@ 88C5F082297F0706001CCADE /* ReleaseVersion.swift in Sources */, 849013E224A9288B008F705E /* Language.swift in Sources */, 840D92A1278D8D6F0007B979 /* DAppBrowserStateError.swift in Sources */, - 77ECB4702ACEEE2E0015CE9F /* StackTitleMultiValueEditCell.swift in Sources */, + 77ECB4702ACEEE2E0015CE9F /* SwapNetworkFeeView.swift in Sources */, 84FC190B29B7DB9F00BCCAA5 /* ExtrinsicServiceTypes.swift in Sources */, 849707A128F3E0AC00DD0A02 /* ReferendumVoterLocal.swift in Sources */, 774091FC2ACC053000172516 /* SwapAssetView.swift in Sources */, @@ -21113,6 +21118,7 @@ F419FD7A273D05B00061652C /* SettingsSection.swift in Sources */, DAEE468553039B3600F64A0E /* AccountManagementWireframe.swift in Sources */, F4E117B9264BAA81006F03B0 /* ControllerAccountConfirmationVM.swift in Sources */, + 77740BBC2AD4A7B800E8C06F /* CollapsableContainerView.swift in Sources */, AE74EBCA25F90CA500C494E7 /* URL+Helpers.swift in Sources */, 8468B87024F63D2000B76BC6 /* AddAccount+UsernameSetupWireframe.swift in Sources */, 84754CA42513E6DC00854599 /* PrimitiveContextWrapper.swift in Sources */, @@ -22570,6 +22576,7 @@ C644308270C29AC6F90CFEA6 /* ReferendumDetailsWireframe.swift in Sources */, 7D2906130F25492872637EFC /* ReferendumDetailsPresenter.swift in Sources */, 5E3B1E6B9E94848B186FD4D1 /* ReferendumDetailsInteractor.swift in Sources */, + 77740BC02AD4A80D00E8C06F /* SwapRateView.swift in Sources */, 488E4467895040EA85FDCC79 /* ReferendumDetailsViewController.swift in Sources */, 845B811D28F44A700040CE84 /* ReferendumActionLocal.swift in Sources */, 77E255692A16148000B644C3 /* StakingRewardsFilter.swift in Sources */, @@ -22825,6 +22832,7 @@ AD877F7F77F9CB862DC7D5B3 /* DelegationReferendumVotersWireframe.swift in Sources */, 0DD3DB85B0E7FD5692F58787 /* DelegationReferendumVotersPresenter.swift in Sources */, 8A2486E62C3915CB6D1FDED8 /* DelegationReferendumVotersInteractor.swift in Sources */, + 77740BBE2AD4A7F500E8C06F /* SwapDetailsView.swift in Sources */, 0F073C8161B9852DEB1D40CD /* DelegationReferendumVotersViewController.swift in Sources */, D274117F06B12F955073D35B /* DelegationReferendumVotersViewLayout.swift in Sources */, E6AB0111B3E1297242D5DBDE /* DelegationReferendumVotersViewFactory.swift in Sources */, diff --git a/novawallet/Common/View/CollapsableContainerView.swift b/novawallet/Common/View/CollapsableContainerView.swift new file mode 100644 index 0000000000..94b17c7d4e --- /dev/null +++ b/novawallet/Common/View/CollapsableContainerView.swift @@ -0,0 +1,177 @@ +import SoraUI +import SnapKit + +protocol CollapsableNetworkInfoViewDelegate: AnyObject { + func animateAlongsideWithInfo(sender: AnyObject?) + func didChangeExpansion(isExpanded: Bool, sender: AnyObject) +} + +class CollapsableContainerView: UIView { + private enum Constants { + static let headerHeight: CGFloat = 32 + static let rowHeight: CGFloat = 44 + static let contentMargins = UIEdgeInsets(top: 4, left: 16, bottom: 4, right: 16) + static let stackViewBottomInset: CGFloat = 4 + } + + let backgroundView = BlockBackgroundView() + + let networkInfoContainer: UIView = .create { + $0.backgroundColor = .clear + $0.clipsToBounds = true + } + + let contentView: UIView = .create { + $0.backgroundColor = .clear + } + + let titleControl: ActionTitleControl = .create { + $0.indicator = ResizableImageActionIndicator(size: .init(width: 24, height: 24)) + $0.imageView.image = R.image.iconLinkChevron()?.tinted(with: R.color.colorTextSecondary()!) + $0.identityIconAngle = CGFloat.pi / 2.0 + $0.activationIconAngle = -CGFloat.pi / 2.0 + $0.titleLabel.apply(style: .footnoteSecondary) + $0.titleLabel.textAlignment = .left + $0.titleLabel.numberOfLines = 1 + $0.layoutType = .flexible + $0.horizontalSpacing = 0 + $0.imageView.isUserInteractionEnabled = false + $0.activate(animated: false) + } + + let stackView: UIStackView = .create { + $0.axis = .vertical + $0.distribution = .fill + $0.alignment = .fill + $0.spacing = 0.0 + $0.layoutMargins = UIEdgeInsets(top: 0.0, left: 16.0, bottom: 0.0, right: 16.0) + $0.isLayoutMarginsRelativeArrangement = true + } + + weak var delegate: CollapsableNetworkInfoViewDelegate? + + lazy var expansionAnimator: BlockViewAnimatorProtocol = BlockViewAnimator() + + var expanded: Bool { titleControl.isActivated } + + override init(frame: CGRect) { + super.init(frame: frame) + + setupLayout() + setupHandlers() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setExpanded(_ value: Bool, animated: Bool) { + guard value != expanded else { + return + } + + if value { + titleControl.activate(animated: animated) + } else { + titleControl.deactivate(animated: animated) + } + + applyExpansion(animated: animated) + } + + private func setupHandlers() { + titleControl.addTarget(self, action: #selector(actionToggleExpansion), for: .valueChanged) + } + + var rows: [UIView] { + [] + } + + private func setupLayout() { + addSubview(backgroundView) + + addSubview(titleControl) + titleControl.snp.makeConstraints { make in + make.top.leading.trailing.equalToSuperview() + make.height.equalTo(Constants.headerHeight) + } + + addSubview(networkInfoContainer) + networkInfoContainer.snp.makeConstraints { make in + make.leading.trailing.bottom.equalToSuperview() + make.top.equalTo(titleControl.snp.bottom) + } + + networkInfoContainer.addSubview(contentView) + contentView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + backgroundView.snp.makeConstraints { make in + make.edges.equalTo(networkInfoContainer.snp.edges) + } + + contentView.addSubview(stackView) + stackView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview() + make.top.equalToSuperview() + make.bottom.equalToSuperview().inset(Constants.stackViewBottomInset) + } + + rows.forEach { view in + stackView.addArrangedSubview(view) + + view.translatesAutoresizingMaskIntoConstraints = false + view.snp.makeConstraints { make in + make.height.equalTo(Constants.rowHeight) + } + } + } + + private func applyExpansion(animated: Bool) { + if animated { + expansionAnimator.animate(block: { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.applyExpansionState() + + let animation = CABasicAnimation() + animation.toValue = strongSelf.backgroundView.contentView?.shapePath + strongSelf.backgroundView.contentView?.layer + .add(animation, forKey: #keyPath(CAShapeLayer.path)) + + strongSelf.delegate?.animateAlongsideWithInfo(sender: strongSelf) + }, completionBlock: nil) + } else { + applyExpansionState() + setNeedsLayout() + } + } + + private func applyExpansionState() { + if expanded { + contentView.snp.updateConstraints { make in + make.top.equalToSuperview() + } + + networkInfoContainer.alpha = 1.0 + delegate?.didChangeExpansion(isExpanded: true, sender: self) + } else { + contentView.snp.updateConstraints { make in + make.top.equalToSuperview().offset( + -CGFloat(stackView.arrangedSubviews.count) * Constants.rowHeight - Constants.stackViewBottomInset + ) + } + + networkInfoContainer.alpha = 0.0 + delegate?.didChangeExpansion(isExpanded: false, sender: self) + } + } + + @objc func actionToggleExpansion() { + applyExpansion(animated: true) + } +} diff --git a/novawallet/Common/View/StackTable/StackTableCollapsableHeaderCell.swift b/novawallet/Common/View/StackTable/StackTableCollapsableHeaderCell.swift deleted file mode 100644 index d8ebf04def..0000000000 --- a/novawallet/Common/View/StackTable/StackTableCollapsableHeaderCell.swift +++ /dev/null @@ -1,45 +0,0 @@ -import UIKit -import SnapKit -import SoraUI - -final class CollapsableViewHeader: UIView { - var titleLabel = UILabel(style: .footnoteSecondary, textAlignment: .left, numberOfLines: 1) - var actionControl: ActionTitleControl = .create { - $0.indicator = ResizableImageActionIndicator(size: .init(width: 24, height: 24)) - $0.imageView.image = R.image.iconLinkChevron()?.tinted(with: R.color.colorTextSecondary()!) - $0.identityIconAngle = CGFloat.pi / 2.0 - $0.activationIconAngle = -CGFloat.pi / 2.0 - $0.titleLabel.text = nil - $0.horizontalSpacing = 0 - } - - override init(frame: CGRect) { - super.init(frame: frame) - - backgroundColor = .clear - - configure() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override var intrinsicContentSize: CGSize { - CGSize(width: UIView.noIntrinsicMetric, height: 24) - } - - private func configure() { - let contentView = UIView.hStack([ - titleLabel, - FlexibleSpaceView(), - actionControl - ]) - - addSubview(contentView) - contentView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - } -} diff --git a/novawallet/Common/View/StackTable/StackTitleMultiValueCell.swift b/novawallet/Common/View/StackTable/StackTitleMultiValueCell.swift index 709d328fef..7a21a140fe 100644 --- a/novawallet/Common/View/StackTable/StackTitleMultiValueCell.swift +++ b/novawallet/Common/View/StackTable/StackTitleMultiValueCell.swift @@ -1,10 +1,23 @@ import Foundation import UIKit +import SoraUI -final class StackTitleMultiValueCell: RowView> { +final class StackTitleMultiValueCell: RowView>, SkeletonableView { var titleLabel: UILabel { rowContentView.titleView.detailsLabel } var topValueLabel: UILabel { rowContentView.valueView.valueTop } var bottomValueLabel: UILabel { rowContentView.valueView.valueBottom } + var skeletonView: SkrullableView? + + private var isLoading: Bool = false + + override func layoutSubviews() { + super.layoutSubviews() + + if isLoading { + updateLoadingState() + skeletonView?.restartSkrulling() + } + } convenience init() { self.init(frame: CGRect(origin: .zero, size: CGSize(width: 340, height: 44.0))) @@ -74,10 +87,48 @@ extension StackTitleMultiValueCell { func bind(loadableViewModel: LoadableViewModelState) { switch loadableViewModel { case let .cached(value), let .loaded(value): + isLoading = false rowContentView.valueView.valueTop.text = value + invalidateLayout() case .loading: - // TODO: Skeleton - break + isLoading = true + invalidateLayout() } } } + +extension StackTitleMultiValueCell { + func createSkeletons(for spaceSize: CGSize) -> [Skeletonable] { + let size = CGSize(width: 68, height: 8) + let offset = CGPoint( + x: spaceSize.width - size.width, + y: spaceSize.height / 2.0 - size.height / 2.0 + ) + + let row = SingleSkeleton.createRow( + on: self, + containerView: self, + spaceSize: spaceSize, + offset: offset, + size: size + ) + + return [row] + } + + var skeletonSuperview: UIView { + self + } + + var hidingViews: [UIView] { + [rowContentView.valueView] + } + + func didStartSkeleton() { + isLoading = true + } + + func didStopSkeleton() { + isLoading = false + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 953ad984d6..7fa65f2dc6 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -15,9 +15,9 @@ final class SwapSetupPresenter { private var receiveChainAsset: ChainAsset? private var payAmountInput: AmountInputResult? private var receiveAmountInput: Decimal? - private var quote: AssetConversion.Quote? private var direction: AssetConversion.Direction? private var fee: BigUInt? + private var quote: AssetConversion.Quote? init( interactor: SwapSetupInteractorInputProtocol, @@ -31,11 +31,12 @@ final class SwapSetupPresenter { self.localizationManager = localizationManager } - private func quote(amount: BigUInt, direction: AssetConversion.Direction) { - guard let assetIn = payChainAsset?.chainAssetId, - let assetOut = receiveChainAsset?.chainAssetId else { - return - } + private func quote( + amount: BigUInt, + assetIn: ChainAssetId, + assetOut: ChainAssetId, + direction: AssetConversion.Direction + ) { self.direction = direction interactor.calculateQuote(for: .init( assetIn: assetIn, @@ -171,6 +172,10 @@ final class SwapSetupPresenter { provideReceiveAmountInputViewModel() } + private func provideDetailsViewModel(isAvailable: Bool) { + view?.didReceiveDetailsState(isAvailable: isAvailable) + } + private func provideRateViewModel() { guard let assetDisplayInfoIn = payChainAsset?.assetDisplayInfo, @@ -190,7 +195,6 @@ final class SwapSetupPresenter { } private func provideFeeViewModel() { - // TODO: chainAsset from user choice guard let payChainAsset = payChainAsset, receiveChainAsset != nil else { return } @@ -223,22 +227,45 @@ final class SwapSetupPresenter { private func refreshQuote() { guard let payChainAsset = payChainAsset, - let receiveChainAsset = receiveChainAsset, - let payInPlank = absoluteValue(for: payAmountInput)?.toSubstrateAmount( - precision: Int16(payChainAsset.asset.precision)), - let receiveInPlank = receiveAmountInput?.toSubstrateAmount(precision: Int16(receiveChainAsset.asset.precision)) - else { + let receiveChainAsset = receiveChainAsset else { return } + var isCalculating: Bool = false switch direction { case .buy: - quote(amount: receiveInPlank, direction: .buy) + if let receiveInPlank = receiveAmountInput?.toSubstrateAmount(precision: Int16(receiveChainAsset.asset.precision)), receiveInPlank > 0 { + quote( + amount: receiveInPlank, + assetIn: payChainAsset.chainAssetId, + assetOut: receiveChainAsset.chainAssetId, + direction: .buy + ) + isCalculating = true + } else { + payAmountInput = nil + providePayAmountInputViewModel() + } case .sell: - quote(amount: payInPlank, direction: .sell) + if let payInPlank = absoluteValue(for: payAmountInput)?.toSubstrateAmount( + precision: Int16(payChainAsset.asset.precision)), payInPlank > 0 { + quote( + amount: payInPlank, + assetIn: payChainAsset.chainAssetId, + assetOut: receiveChainAsset.chainAssetId, + direction: .sell + ) + isCalculating = true + } else { + receiveAmountInput = nil + provideReceiveAmountInputViewModel() + } default: break } + provideDetailsViewModel(isAvailable: isCalculating) + provideRateViewModel() + provideFeeViewModel() } } @@ -246,6 +273,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { func setup() { providePayAssetViews() provideReceiveAssetViews() + provideDetailsViewModel(isAvailable: false) provideButtonState() interactor.setup() } @@ -254,6 +282,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { wireframe.showPayTokenSelection(from: view) { [weak self] chainAsset in self?.payChainAsset = chainAsset self?.providePayAssetViews() + self?.refreshQuote() self?.interactor.update(payChainAsset: chainAsset) } } @@ -262,32 +291,21 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { wireframe.showReceiveTokenSelection(from: view) { [weak self] chainAsset in self?.receiveChainAsset = chainAsset self?.provideReceiveAssetViews() + self?.refreshQuote() self?.interactor.update(receiveChainAsset: chainAsset) } } func updatePayAmount(_ amount: Decimal?) { payAmountInput = amount.map { .absolute($0) } - - if - let chainAsset = payChainAsset, - let amount = amount, - let amountInPlank = amount.toSubstrateAmount(precision: chainAsset.assetDisplayInfo.assetPrecision) { - quote(amount: amountInPlank, direction: .sell) - estimateFee() - } + direction = .sell + refreshQuote() } func updateReceiveAmount(_ amount: Decimal?) { receiveAmountInput = amount - - if - let chainAsset = receiveChainAsset, - let amount = amount, - let amountInPlank = amount.toSubstrateAmount(precision: chainAsset.assetDisplayInfo.assetPrecision) { - quote(amount: amountInPlank, direction: .buy) - estimateFee() - } + direction = .buy + refreshQuote() } func swap() { @@ -295,6 +313,8 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { providePayAssetViews() provideReceiveAssetViews() provideButtonState() + quote = nil + refreshQuote() } // TODO: show editing fee @@ -336,6 +356,7 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { quote.assetOut == receiveChainAsset.chainAssetId else { return } + self.quote = quote switch direction { @@ -357,6 +378,7 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { } provideRateViewModel() + estimateFee() } func didReceive(fee: BigUInt?) { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index fcbd43c6fe..848b648a63 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -12,6 +12,7 @@ protocol SwapSetupViewProtocol: ControllerBackedProtocol { func didReceiveTitle(receiveViewModel viewModel: TitleHorizontalMultiValueView.Model) func didReceiveRate(viewModel: LoadableViewModelState) func didReceiveNetworkFee(viewModel: LoadableViewModelState) + func didReceiveDetailsState(isAvailable: Bool) } protocol SwapSetupPresenterProtocol: AnyObject { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 7ef1569686..fca870ea59 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -66,9 +66,21 @@ final class SwapSetupViewController: UIViewController, ViewHolder { for: .editingChanged ) - rootView.rateCell.addTarget(self, action: #selector(rateInfoAction), for: .touchUpInside) - rootView.networkFeeCell.valueTopButton.addTarget(self, action: #selector(changeNetworkFeeAction), for: .touchUpInside) - rootView.networkFeeCell.titleButton.addTarget(self, action: #selector(networkFeeInfoAction), for: .touchUpInside) + rootView.rateCell.titleButton.addTarget( + self, + action: #selector(rateInfoAction), + for: .touchUpInside + ) + rootView.networkFeeCell.valueTopButton.addTarget( + self, + action: #selector(changeNetworkFeeAction), + for: .touchUpInside + ) + rootView.networkFeeCell.titleButton.addTarget( + self, + action: #selector(networkFeeInfoAction), + for: .touchUpInside + ) } private func setupLocalization() { @@ -164,6 +176,10 @@ extension SwapSetupViewController: SwapSetupViewProtocol { rootView.receiveAmountInputView.bind(priceViewModel: viewModel) } + func didReceiveDetailsState(isAvailable: Bool) { + rootView.detailsView.isHidden = !isAvailable + } + func didReceiveRate(viewModel: LoadableViewModelState) { rootView.rateCell.bind(loadableViewModel: viewModel) } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift index 62a5860860..c7a91a735e 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift @@ -127,8 +127,8 @@ extension SwapAmountInputView { } func bind(emptyViewModel: EmptySwapsAssetViewModel) { - assetControl.bind(emptyViewModel: emptyViewModel) textInputView.isHidden = true + assetControl.bind(emptyViewModel: emptyViewModel) setNeedsLayout() } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift b/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift index a19e832559..b67b5e73d6 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift @@ -137,7 +137,7 @@ extension SwapAssetControl { network: assetViewModel.hub.name, icon: assetViewModel.hub.icon ) - invalidateIntrinsicContentSize() + invalidateLayout() } func bind(emptyViewModel: EmptySwapsAssetViewModel) { @@ -149,6 +149,6 @@ extension SwapAssetControl { network: emptyViewModel.subtitle, icon: nil ) - invalidateIntrinsicContentSize() + invalidateLayout() } } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAssetView.swift b/novawallet/Modules/Swaps/Setup/View/SwapAssetView.swift index 51a9a7454f..47dcb4955f 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAssetView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAssetView.swift @@ -25,6 +25,8 @@ final class SwapAssetView: GenericPairValueView>> { - var titleButton: RoundedButton { rowContentView.titleView } - var valueTopButton: RoundedButton { rowContentView.valueView.fView } - var valueBottomLabel: UILabel { rowContentView.valueView.sView } +final class SwapNetworkFeeView: GenericTitleValueView>, SkeletonableView { + var titleButton: RoundedButton { titleView } + var valueTopButton: RoundedButton { valueView.fView } + var valueBottomLabel: UILabel { valueView.sView } + var skeletonView: SkrullableView? - convenience init() { - self.init(frame: CGRect(origin: .zero, size: CGSize(width: 340, height: 44.0))) + private var isLoading: Bool = false + + override func layoutSubviews() { + super.layoutSubviews() + + if isLoading { + updateLoadingState() + skeletonView?.restartSkrulling() + } } override init(frame: CGRect) { @@ -45,24 +53,63 @@ final class StackTitleMultiValueEditCell: RowView) { - loadableViewModel.value.map(bind) + switch loadableViewModel { + case let .cached(value), let .loaded(value): + isLoading = false + stopLoadingIfNeeded() + bind(viewModel: value) + case .loading: + isLoading = true + startLoadingIfNeeded() + } + } +} + +extension SwapNetworkFeeView { + func createSkeletons(for spaceSize: CGSize) -> [Skeletonable] { + let size = CGSize(width: 68, height: 8) + let offset = CGPoint( + x: spaceSize.width - size.width, + y: spaceSize.height / 2.0 - size.height / 2.0 + ) + + let row = SingleSkeleton.createRow( + on: self, + containerView: self, + spaceSize: spaceSize, + offset: offset, + size: size + ) + + return [row] + } + + var skeletonSuperview: UIView { + self + } + + var hidingViews: [UIView] { + [valueView] + } + + func didStartSkeleton() { + isLoading = true + } + + func didStopSkeleton() { + isLoading = false } } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapRateView.swift b/novawallet/Modules/Swaps/Setup/View/SwapRateView.swift new file mode 100644 index 0000000000..a3afdf0430 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/View/SwapRateView.swift @@ -0,0 +1,97 @@ +import UIKit +import SoraUI + +final class SwapRateView: GenericTitleValueView, SkeletonableView { + var titleButton: RoundedButton { titleView } + var valueLabel: UILabel { valueView } + var skeletonView: SkrullableView? + + private var isLoading: Bool = false + + override func layoutSubviews() { + super.layoutSubviews() + + if isLoading { + updateLoadingState() + skeletonView?.restartSkrulling() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .clear + + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + titleButton.applyIconStyle() + titleButton.imageWithTitleView?.iconImage = R.image.iconInfoFilled()?.tinted( + with: R.color.colorIconSecondary()! + ) + titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() + titleButton.imageWithTitleView?.titleFont = .regularFootnote + titleButton.imageWithTitleView?.spacingBetweenLabelAndIcon = 4 + titleButton.imageWithTitleView?.layoutType = .horizontalLabelFirst + titleButton.contentInsets = .init(top: 8, left: 0, bottom: 8, right: 0) + + valueLabel.textColor = R.color.colorTextPrimary() + valueLabel.font = .regularFootnote + } +} + +extension SwapRateView { + func bind(loadableViewModel: LoadableViewModelState) { + switch loadableViewModel { + case let .cached(value), let .loaded(value): + stopLoadingIfNeeded() + isLoading = false + valueView.text = value + case .loading: + startLoadingIfNeeded() + isLoading = true + } + } +} + +extension SwapRateView { + func createSkeletons(for spaceSize: CGSize) -> [Skeletonable] { + let size = CGSize(width: 68, height: 8) + let offset = CGPoint( + x: spaceSize.width - size.width, + y: spaceSize.height / 2.0 - size.height / 2.0 + ) + + let row = SingleSkeleton.createRow( + on: self, + containerView: self, + spaceSize: spaceSize, + offset: offset, + size: size + ) + + return [row] + } + + var skeletonSuperview: UIView { + self + } + + var hidingViews: [UIView] { + [valueView] + } + + func didStartSkeleton() { + isLoading = true + } + + func didStopSkeleton() { + isLoading = false + } +} diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index ea1a0db89c..72d26e1758 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -27,26 +27,16 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { $0.imageWithTitleView?.iconImage = R.image.iconActionSwap() } - let detailsHeaderCell: CollapsableViewHeader = .create { - $0.actionControl.addTarget(self, action: #selector(detailsControlAction), for: .valueChanged) - $0.actionControl.imageView.isUserInteractionEnabled = false - } + let detailsView = SwapDetailsView() - let detailsTableView: StackTableView = .create { - $0.cellHeight = 44 - $0.hasSeparators = true - $0.contentInsets = UIEdgeInsets(top: 4, left: 16, bottom: 4, right: 16) - $0.isHidden = true + var rateCell: SwapRateView { + detailsView.rateCell } - let rateCell: StackTitleMultiValueCell = .create { - $0.titleLabel.apply(style: .footnoteSecondary) - $0.rowContentView.titleView.iconWidth = 16 - $0.rowContentView.titleView.imageView.image = R.image.iconInfoFilledAccent() + var networkFeeCell: SwapNetworkFeeView { + detailsView.networkFeeCell } - let networkFeeCell = StackTitleMultiValueEditCell() - override func setupStyle() { backgroundColor = R.color.colorSecondaryScreenBackground() } @@ -80,10 +70,7 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { $0.height.equalTo(64) } - addArrangedSubview(detailsHeaderCell, spacingAfter: 8) - addArrangedSubview(detailsTableView) - detailsTableView.addArrangedSubview(rateCell) - detailsTableView.addArrangedSubview(networkFeeCell) + addArrangedSubview(detailsView, spacingAfter: 8) addSubview(switchButton) switchButton.snp.makeConstraints { @@ -95,17 +82,12 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { } func setup(locale: Locale) { - detailsHeaderCell.titleLabel.text = R.string.localizable.swapsSetupDetailsTitle( + detailsView.titleControl.titleLabel.text = R.string.localizable.swapsSetupDetailsTitle( preferredLanguages: locale.rLanguages ) - rateCell.titleLabel.text = R.string.localizable.swapsSetupDetailsRate(preferredLanguages: locale.rLanguages) + rateCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupDetailsRate(preferredLanguages: locale.rLanguages) networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetwork(preferredLanguages: locale.rLanguages) - } - - @objc - private func detailsControlAction() { - detailsTableView.isHidden = !detailsHeaderCell.actionControl.isActivated - detailsHeaderCell.actionControl.invalidateLayout() - detailsHeaderCell.setNeedsLayout() + rateCell.setNeedsLayout() + detailsView.setNeedsLayout() } } From 9a4c4393dcb39a139073580278d000400d980672 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 10 Oct 2023 02:23:47 +0300 Subject: [PATCH 026/204] cleanup --- .../Common/View/CollapsableContainerView.swift | 1 - .../Modules/Swaps/Setup/SwapSetupPresenter.swift | 13 ++++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/novawallet/Common/View/CollapsableContainerView.swift b/novawallet/Common/View/CollapsableContainerView.swift index 94b17c7d4e..1d891afaa1 100644 --- a/novawallet/Common/View/CollapsableContainerView.swift +++ b/novawallet/Common/View/CollapsableContainerView.swift @@ -122,7 +122,6 @@ class CollapsableContainerView: UIView { rows.forEach { view in stackView.addArrangedSubview(view) - view.translatesAutoresizingMaskIntoConstraints = false view.snp.makeConstraints { make in make.height.equalTo(Constants.rowHeight) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 7fa65f2dc6..11f2a1b411 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -242,10 +242,9 @@ final class SwapSetupPresenter { direction: .buy ) isCalculating = true - } else { - payAmountInput = nil - providePayAmountInputViewModel() } + payAmountInput = nil + providePayAmountInputViewModel() case .sell: if let payInPlank = absoluteValue(for: payAmountInput)?.toSubstrateAmount( precision: Int16(payChainAsset.asset.precision)), payInPlank > 0 { @@ -256,10 +255,10 @@ final class SwapSetupPresenter { direction: .sell ) isCalculating = true - } else { - receiveAmountInput = nil - provideReceiveAmountInputViewModel() } + + receiveAmountInput = nil + provideReceiveAmountInputViewModel() default: break } @@ -366,7 +365,7 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { precision: Int16(payChainAsset.asset.precision) ) ?? 0 payAmountInput = .absolute(payAmount) - providePayInputPriceViewModel() + providePayAmountInputViewModel() case .sell: receiveAmountInput = Decimal.fromSubstrateAmount( quote.amountOut, From f87d04805d916a0659fc5419f7d521ab7c502400 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 10 Oct 2023 13:41:48 +0300 Subject: [PATCH 027/204] PR fixes --- .../Model/AssetConversion.swift | 6 +- .../Swaps/SwapAssetsOperationInteractor.swift | 2 +- .../Model/SwapsSetupViewModelFactory.swift | 9 +- .../Swaps/Setup/Model/ViewModels.swift | 5 + .../Swaps/Setup/SwapSetupInteractor.swift | 33 +++-- .../Swaps/Setup/SwapSetupPresenter.swift | 119 +++++++++--------- .../Swaps/Setup/SwapSetupProtocols.swift | 14 ++- .../Swaps/Setup/SwapSetupViewController.swift | 2 +- .../Swaps/Setup/SwapSetupWireframe.swift | 4 + .../Swaps/Setup/View/SwapNetworkFeeView.swift | 14 ++- 10 files changed, 120 insertions(+), 88 deletions(-) diff --git a/novawallet/Modules/AssetConversion/Model/AssetConversion.swift b/novawallet/Modules/AssetConversion/Model/AssetConversion.swift index 8ea2128f92..1be3d0c80f 100644 --- a/novawallet/Modules/AssetConversion/Model/AssetConversion.swift +++ b/novawallet/Modules/AssetConversion/Model/AssetConversion.swift @@ -7,7 +7,7 @@ enum AssetConversion { case buy } - struct QuoteArgs { + struct QuoteArgs: Equatable { let assetIn: ChainAssetId let assetOut: ChainAssetId let amount: BigUInt @@ -45,3 +45,7 @@ enum AssetConversion { let slippage: BigRational } } + +extension AssetConversion.CallArgs { + var identifier: String { "\(hashValue)" } +} diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift index 24ce76df6a..16098fcd58 100644 --- a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift @@ -87,7 +87,7 @@ final class SwapAssetsOperationInteractor: AnyCancellableCleaning { guard let availableDirections = self?.availableDirections else { return false } - return availableDirections[chainAsset.chainAssetId]?.isEmpty == false + return availableDirections.contains(where: { $0.value.contains(chainAsset.chainAssetId) }) } builder = .init( diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift index db3279e533..266404549a 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift @@ -36,7 +36,7 @@ protocol SwapsSetupViewModelFactoryProtocol { assetDisplayInfo: AssetBalanceDisplayInfo, priceData: PriceData?, locale: Locale - ) -> BalanceViewModelProtocol + ) -> SwapFeeViewModel } final class SwapsSetupViewModelFactory { @@ -241,15 +241,18 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { assetDisplayInfo: AssetBalanceDisplayInfo, priceData: PriceData?, locale: Locale - ) -> BalanceViewModelProtocol { + ) -> SwapFeeViewModel { let amountDecimal = Decimal.fromSubstrateAmount( amount, precision: assetDisplayInfo.assetPrecision ) ?? 0 - return balanceViewModelFactoryFacade.balanceFromPrice( + let balanceViewModel = balanceViewModelFactoryFacade.balanceFromPrice( targetAssetInfo: assetDisplayInfo, amount: amountDecimal, priceData: priceData ).value(for: locale) + + // TODO: provide isEditable + return .init(isEditable: false, balanceViewModel: balanceViewModel) } } diff --git a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift index 5ed2476be8..afd5a0e8da 100644 --- a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift +++ b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift @@ -14,3 +14,8 @@ enum SwapAssetInputViewModel { case asset(SwapsAssetViewModel) case empty(EmptySwapsAssetViewModel) } + +struct SwapFeeViewModel { + var isEditable: Bool + var balanceViewModel: BalanceViewModelProtocol +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 166782ad1b..7bdc30fcc5 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -60,7 +60,7 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning clear(cancellable: "eCall) let wrapper = assetConversionOperationFactory.quote(for: args) - wrapper.targetOperation.completionBlock = { [weak self] in + wrapper.targetOperation.completionBlock = { [weak self, args] in DispatchQueue.main.async { guard self?.quoteCall === wrapper else { return @@ -68,9 +68,9 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning do { let result = try wrapper.targetOperation.extractNoCancellableResultData() - self?.presenter?.didReceive(quote: result) + self?.presenter?.didReceive(quote: result, for: args) } catch { - self?.presenter?.didReceive(error: .quote(error)) + self?.presenter?.didReceive(error: .quote(error, args)) } } } @@ -110,10 +110,14 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning for: args, codingFactory: runtimeCoderFactory ) - self.feeProxy.estimateFee(using: extrinsicService, reuseIdentifier: "\(args.hashValue)", setupBy: builder) + self.feeProxy.estimateFee( + using: extrinsicService, + reuseIdentifier: args.identifier, + setupBy: builder + ) } catch { DispatchQueue.main.async { - self.presenter?.didReceive(error: .fetchFeeFailed(error)) + self.presenter?.didReceive(error: .fetchFeeFailed(error, args.identifier)) } } } @@ -142,15 +146,16 @@ extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { performPriceSubscription(chainAsset: payChainAsset) } + @discardableResult func calculateFee( for quote: AssetConversion.Quote, slippage: SwapSlippage - ) { + ) -> TransactionFeeId? { guard let receiver = accountId else { - return + return nil } - fee(args: .init( + let args = AssetConversion.CallArgs( assetIn: quote.assetIn, amountIn: quote.amountIn, assetOut: quote.assetOut, @@ -158,19 +163,23 @@ extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { receiver: receiver, direction: slippage.direction, slippage: .percent(of: slippage.slippage) - )) + ) + + fee(args: args) + + return args.identifier } } extension SwapSetupInteractor: ExtrinsicFeeProxyDelegate { - func didReceiveFee(result: Result, for _: TransactionFeeId) { + func didReceiveFee(result: Result, for transactionId: TransactionFeeId) { DispatchQueue.main.async { switch result { case let .success(dispatchInfo): let fee = BigUInt(dispatchInfo.fee) - self.presenter?.didReceive(fee: fee) + self.presenter?.didReceive(fee: fee, transactionId: transactionId) case let .failure(error): - self.presenter?.didReceive(error: .fetchFeeFailed(error)) + self.presenter?.didReceive(error: .fetchFeeFailed(error, transactionId)) } } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 11f2a1b411..a6bfa5cf95 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -15,9 +15,15 @@ final class SwapSetupPresenter { private var receiveChainAsset: ChainAsset? private var payAmountInput: AmountInputResult? private var receiveAmountInput: Decimal? - private var direction: AssetConversion.Direction? private var fee: BigUInt? private var quote: AssetConversion.Quote? + private var quoteArgs: AssetConversion.QuoteArgs? { + didSet { + provideDetailsViewModel(isAvailable: quoteArgs != nil) + } + } + + private var feeIdentifier: String? init( interactor: SwapSetupInteractorInputProtocol, @@ -31,21 +37,6 @@ final class SwapSetupPresenter { self.localizationManager = localizationManager } - private func quote( - amount: BigUInt, - assetIn: ChainAssetId, - assetOut: ChainAssetId, - direction: AssetConversion.Direction - ) { - self.direction = direction - interactor.calculateQuote(for: .init( - assetIn: assetIn, - assetOut: assetOut, - amount: amount, - direction: direction - )) - } - private func provideButtonState() { let buttonState = viewModelFactory.buttonState( assetIn: payChainAsset?.chainAssetId, @@ -218,43 +209,49 @@ final class SwapSetupPresenter { } // TODO: Remove hardcode slippage and direction - interactor.calculateFee( + feeIdentifier = interactor.calculateFee( for: quote, slippage: .init(direction: .sell, slippage: 1) ) } - private func refreshQuote() { + private func refreshQuote(direction: AssetConversion.Direction) { guard let payChainAsset = payChainAsset, let receiveChainAsset = receiveChainAsset else { return } - var isCalculating: Bool = false + quote = nil switch direction { case .buy: if let receiveInPlank = receiveAmountInput?.toSubstrateAmount(precision: Int16(receiveChainAsset.asset.precision)), receiveInPlank > 0 { - quote( - amount: receiveInPlank, + let quoteArgs = AssetConversion.QuoteArgs( assetIn: payChainAsset.chainAssetId, assetOut: receiveChainAsset.chainAssetId, - direction: .buy + amount: receiveInPlank, + direction: direction ) - isCalculating = true + self.quoteArgs = quoteArgs + interactor.calculateQuote(for: quoteArgs) + } else { + quoteArgs = nil } payAmountInput = nil providePayAmountInputViewModel() case .sell: if let payInPlank = absoluteValue(for: payAmountInput)?.toSubstrateAmount( precision: Int16(payChainAsset.asset.precision)), payInPlank > 0 { - quote( - amount: payInPlank, + let quoteArgs = AssetConversion.QuoteArgs( assetIn: payChainAsset.chainAssetId, assetOut: receiveChainAsset.chainAssetId, - direction: .sell + amount: payInPlank, + direction: direction ) - isCalculating = true + self.quoteArgs = quoteArgs + interactor.calculateQuote(for: quoteArgs) + } else { + quoteArgs = nil } receiveAmountInput = nil @@ -262,7 +259,7 @@ final class SwapSetupPresenter { default: break } - provideDetailsViewModel(isAvailable: isCalculating) + provideRateViewModel() provideFeeViewModel() } @@ -278,33 +275,31 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } func selectPayToken() { - wireframe.showPayTokenSelection(from: view) { [weak self] chainAsset in + wireframe.showPayTokenSelection(from: view, chainAsset: receiveChainAsset) { [weak self] chainAsset in self?.payChainAsset = chainAsset self?.providePayAssetViews() - self?.refreshQuote() + self?.refreshQuote(direction: .sell) self?.interactor.update(payChainAsset: chainAsset) } } func selectReceiveToken() { - wireframe.showReceiveTokenSelection(from: view) { [weak self] chainAsset in + wireframe.showReceiveTokenSelection(from: view, chainAsset: payChainAsset) { [weak self] chainAsset in self?.receiveChainAsset = chainAsset self?.provideReceiveAssetViews() - self?.refreshQuote() + self?.refreshQuote(direction: .buy) self?.interactor.update(receiveChainAsset: chainAsset) } } func updatePayAmount(_ amount: Decimal?) { payAmountInput = amount.map { .absolute($0) } - direction = .sell - refreshQuote() + refreshQuote(direction: .sell) } func updateReceiveAmount(_ amount: Decimal?) { receiveAmountInput = amount - direction = .buy - refreshQuote() + refreshQuote(direction: .buy) } func swap() { @@ -312,8 +307,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { providePayAssetViews() provideReceiveAssetViews() provideButtonState() - quote = nil - refreshQuote() + refreshQuote(direction: quoteArgs?.direction ?? .sell) } // TODO: show editing fee @@ -332,11 +326,17 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { func didReceive(error: SwapSetupError) { switch error { - case .quote: + case let .quote(_, args): + guard args == quoteArgs else { + return + } wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.refreshQuote() + self?.refreshQuote(direction: args.direction) + } + case let .fetchFeeFailed(_, id): + guard id == feeIdentifier else { + return } - case .fetchFeeFailed: wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in self?.estimateFee() } @@ -347,30 +347,30 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { } } - func didReceive(quote: AssetConversion.Quote) { - guard - let payChainAsset = payChainAsset, - let receiveChainAsset = receiveChainAsset, - quote.assetIn == payChainAsset.chainAssetId, - quote.assetOut == receiveChainAsset.chainAssetId else { + func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) { + guard quoteArgs == self.quoteArgs else { return } self.quote = quote - switch direction { + switch quoteArgs.direction { case .buy: - let payAmount = Decimal.fromSubstrateAmount( - quote.amountIn, - precision: Int16(payChainAsset.asset.precision) - ) ?? 0 - payAmountInput = .absolute(payAmount) + let payAmount = payChainAsset.map { + Decimal.fromSubstrateAmount( + quote.amountIn, + precision: Int16($0.asset.precision) + ) ?? 0 + } + payAmountInput = payAmount.map { .absolute($0) } providePayAmountInputViewModel() case .sell: - receiveAmountInput = Decimal.fromSubstrateAmount( - quote.amountOut, - precision: Int16(receiveChainAsset.asset.precision) - ) ?? 0 + receiveAmountInput = receiveChainAsset.map { + Decimal.fromSubstrateAmount( + quote.amountOut, + precision: Int16($0.asset.precision) + ) ?? 0 + } provideReceiveAmountInputViewModel() default: break @@ -380,7 +380,10 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { estimateFee() } - func didReceive(fee: BigUInt?) { + func didReceive(fee: BigUInt?, transactionId: TransactionFeeId) { + guard feeIdentifier == transactionId else { + return + } self.fee = fee provideFeeViewModel() } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 848b648a63..7833d4b257 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -11,7 +11,7 @@ protocol SwapSetupViewProtocol: ControllerBackedProtocol { func didReceiveAmountInputPrice(receiveViewModel: String?) func didReceiveTitle(receiveViewModel viewModel: TitleHorizontalMultiValueView.Model) func didReceiveRate(viewModel: LoadableViewModelState) - func didReceiveNetworkFee(viewModel: LoadableViewModelState) + func didReceiveNetworkFee(viewModel: LoadableViewModelState) func didReceiveDetailsState(isAvailable: Bool) } @@ -33,12 +33,12 @@ protocol SwapSetupInteractorInputProtocol: AnyObject { func update(receiveChainAsset: ChainAsset) func update(payChainAsset: ChainAsset) func calculateQuote(for args: AssetConversion.QuoteArgs) - func calculateFee(for quote: AssetConversion.Quote, slippage: SwapSlippage) + func calculateFee(for quote: AssetConversion.Quote, slippage: SwapSlippage) -> TransactionFeeId? } protocol SwapSetupInteractorOutputProtocol: AnyObject { - func didReceive(quote: AssetConversion.Quote) - func didReceive(fee: BigUInt?) + func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) + func didReceive(fee: BigUInt?, transactionId: TransactionFeeId) func didReceive(error: SwapSetupError) func didReceive(price: PriceData?, priceId: AssetModel.PriceId) } @@ -46,17 +46,19 @@ protocol SwapSetupInteractorOutputProtocol: AnyObject { protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, ErrorPresentable { func showPayTokenSelection( from view: ControllerBackedProtocol?, + chainAsset: ChainAsset?, completionHandler: @escaping (ChainAsset) -> Void ) func showReceiveTokenSelection( from view: ControllerBackedProtocol?, + chainAsset: ChainAsset?, completionHandler: @escaping (ChainAsset) -> Void ) } enum SwapSetupError: Error { - case quote(Error) - case fetchFeeFailed(Error) + case quote(Error, AssetConversion.QuoteArgs) + case fetchFeeFailed(Error, TransactionFeeId) case price(Error, AssetModel.PriceId) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index fca870ea59..159119d33d 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -184,7 +184,7 @@ extension SwapSetupViewController: SwapSetupViewProtocol { rootView.rateCell.bind(loadableViewModel: viewModel) } - func didReceiveNetworkFee(viewModel: LoadableViewModelState) { + func didReceiveNetworkFee(viewModel: LoadableViewModelState) { rootView.networkFeeCell.bind(loadableViewModel: viewModel) } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index edc822ec5d..b80c56af15 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -9,10 +9,12 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { func showPayTokenSelection( from view: ControllerBackedProtocol?, + chainAsset: ChainAsset?, completionHandler: @escaping (ChainAsset) -> Void ) { guard let selectTokenView = SwapAssetsOperationViewFactory.createSelectPayTokenView( for: assetListObservable, + chainAsset: chainAsset, selectClosure: completionHandler ) else { return @@ -27,10 +29,12 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { func showReceiveTokenSelection( from view: ControllerBackedProtocol?, + chainAsset: ChainAsset?, completionHandler: @escaping (ChainAsset) -> Void ) { guard let selectTokenView = SwapAssetsOperationViewFactory.createSelectReceiveTokenView( for: assetListObservable, + chainAsset: chainAsset, selectClosure: completionHandler ) else { return diff --git a/novawallet/Modules/Swaps/Setup/View/SwapNetworkFeeView.swift b/novawallet/Modules/Swaps/Setup/View/SwapNetworkFeeView.swift index d9ded04a6e..1aa64435ae 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapNetworkFeeView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapNetworkFeeView.swift @@ -8,6 +8,7 @@ final class SwapNetworkFeeView: GenericTitleValueView) { + func bind(loadableViewModel: LoadableViewModelState) { switch loadableViewModel { case let .cached(value), let .loaded(value): isLoading = false From 331c0f67c656d008884de7f6c705097049e5359b Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 11 Oct 2023 09:02:27 +0300 Subject: [PATCH 028/204] fix animation, accessory view, button state --- .../View/CollapsableContainerView.swift | 30 +++++++----------- novawallet/Common/View/UIFactory.swift | 31 +++++++++++++++++++ .../Model/SwapsSetupViewModelFactory.swift | 2 +- .../Swaps/Setup/SwapSetupPresenter.swift | 6 ++++ .../Swaps/Setup/SwapSetupViewController.swift | 19 ++++++++++-- .../Setup/View/SwapSetupViewLayout.swift | 8 +++-- 6 files changed, 71 insertions(+), 25 deletions(-) diff --git a/novawallet/Common/View/CollapsableContainerView.swift b/novawallet/Common/View/CollapsableContainerView.swift index 1d891afaa1..8421b618ec 100644 --- a/novawallet/Common/View/CollapsableContainerView.swift +++ b/novawallet/Common/View/CollapsableContainerView.swift @@ -67,10 +67,6 @@ class CollapsableContainerView: UIView { } func setExpanded(_ value: Bool, animated: Bool) { - guard value != expanded else { - return - } - if value { titleControl.activate(animated: animated) } else { @@ -89,8 +85,6 @@ class CollapsableContainerView: UIView { } private func setupLayout() { - addSubview(backgroundView) - addSubview(titleControl) titleControl.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() @@ -108,8 +102,10 @@ class CollapsableContainerView: UIView { make.edges.equalToSuperview() } + contentView.addSubview(backgroundView) + backgroundView.snp.makeConstraints { make in - make.edges.equalTo(networkInfoContainer.snp.edges) + make.edges.equalToSuperview() } contentView.addSubview(stackView) @@ -131,18 +127,18 @@ class CollapsableContainerView: UIView { private func applyExpansion(animated: Bool) { if animated { expansionAnimator.animate(block: { [weak self] in - guard let strongSelf = self else { + guard let self = self else { return } - strongSelf.applyExpansionState() + self.applyExpansionState() let animation = CABasicAnimation() - animation.toValue = strongSelf.backgroundView.contentView?.shapePath - strongSelf.backgroundView.contentView?.layer + animation.toValue = self.backgroundView.contentView?.shapePath + self.backgroundView.contentView?.layer .add(animation, forKey: #keyPath(CAShapeLayer.path)) - strongSelf.delegate?.animateAlongsideWithInfo(sender: strongSelf) + self.delegate?.animateAlongsideWithInfo(sender: self) }, completionBlock: nil) } else { applyExpansionState() @@ -153,19 +149,17 @@ class CollapsableContainerView: UIView { private func applyExpansionState() { if expanded { contentView.snp.updateConstraints { make in - make.top.equalToSuperview() + make.top.bottom.equalToSuperview().offset(0) } - - networkInfoContainer.alpha = 1.0 + layoutIfNeeded() delegate?.didChangeExpansion(isExpanded: true, sender: self) } else { contentView.snp.updateConstraints { make in - make.top.equalToSuperview().offset( + make.top.bottom.equalToSuperview().offset( -CGFloat(stackView.arrangedSubviews.count) * Constants.rowHeight - Constants.stackViewBottomInset ) } - - networkInfoContainer.alpha = 0.0 + layoutIfNeeded() delegate?.didChangeExpansion(isExpanded: false, sender: self) } } diff --git a/novawallet/Common/View/UIFactory.swift b/novawallet/Common/View/UIFactory.swift index f611448937..588b41de3d 100644 --- a/novawallet/Common/View/UIFactory.swift +++ b/novawallet/Common/View/UIFactory.swift @@ -548,3 +548,34 @@ final class UIFactory: UIFactoryProtocol { return view } } + +extension UIFactory { + func createDoneAccessoryView( + target: Any?, + selector: Selector, + locale: Locale + ) -> UIToolbar { + let frame = CGRect( + x: 0.0, + y: 0.0, + width: UIScreen.main.bounds.width, + height: UIConstants.accessoryBarHeight + ) + + let toolBar = UIToolbar(frame: frame) + + let doneTitle = R.string.localizable.commonDone(preferredLanguages: locale.rLanguages) + let doneAction = ViewSelectorAction( + title: doneTitle, + selector: selector + ) + + return createActionsAccessoryView( + for: toolBar, + actions: [], + doneAction: doneAction, + target: target, + spacing: 0 + ) + } +} diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift index 266404549a..1ff095a08e 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift @@ -253,6 +253,6 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { ).value(for: locale) // TODO: provide isEditable - return .init(isEditable: false, balanceViewModel: balanceViewModel) + return .init(isEditable: true, balanceViewModel: balanceViewModel) } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index a6bfa5cf95..f7afb66244 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -278,6 +278,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { wireframe.showPayTokenSelection(from: view, chainAsset: receiveChainAsset) { [weak self] chainAsset in self?.payChainAsset = chainAsset self?.providePayAssetViews() + self?.provideButtonState() self?.refreshQuote(direction: .sell) self?.interactor.update(payChainAsset: chainAsset) } @@ -287,6 +288,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { wireframe.showReceiveTokenSelection(from: view, chainAsset: payChainAsset) { [weak self] chainAsset in self?.receiveChainAsset = chainAsset self?.provideReceiveAssetViews() + self?.provideButtonState() self?.refreshQuote(direction: .buy) self?.interactor.update(receiveChainAsset: chainAsset) } @@ -295,11 +297,13 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { func updatePayAmount(_ amount: Decimal?) { payAmountInput = amount.map { .absolute($0) } refreshQuote(direction: .sell) + provideButtonState() } func updateReceiveAmount(_ amount: Decimal?) { receiveAmountInput = amount refreshQuote(direction: .buy) + provideButtonState() } func swap() { @@ -378,6 +382,7 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { provideRateViewModel() estimateFee() + provideButtonState() } func didReceive(fee: BigUInt?, transactionId: TransactionFeeId) { @@ -386,6 +391,7 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { } self.fee = fee provideFeeViewModel() + provideButtonState() } func didReceive(price: PriceData?, priceId: AssetModel.PriceId) { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 159119d33d..186b96314b 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -53,19 +53,16 @@ final class SwapSetupViewController: UIViewController, ViewHolder { action: #selector(swapAction), for: .touchUpInside ) - rootView.payAmountInputView.textInputView.addTarget( self, action: #selector(payAmountChangeAction), for: .editingChanged ) - rootView.receiveAmountInputView.textInputView.addTarget( self, action: #selector(receiveAmountChangeAction), for: .editingChanged ) - rootView.rateCell.titleButton.addTarget( self, action: #selector(rateInfoAction), @@ -86,6 +83,18 @@ final class SwapSetupViewController: UIViewController, ViewHolder { private func setupLocalization() { title = R.string.localizable.walletAssetsSwap(preferredLanguages: selectedLocale.rLanguages) rootView.setup(locale: selectedLocale) + setupAccessoryView() + } + + private func setupAccessoryView() { + let accessoryView = + UIFactory.default.createDoneAccessoryView( + target: self, + selector: #selector(doneAction), + locale: selectedLocale + ) + rootView.payAmountInputView.textInputView.textField.inputAccessoryView = accessoryView + rootView.receiveAmountInputView.textInputView.textField.inputAccessoryView = accessoryView } @objc private func selectPayTokenAction() { @@ -127,6 +136,10 @@ final class SwapSetupViewController: UIViewController, ViewHolder { @objc private func rateInfoAction() { presenter.showRateInfo() } + + @objc private func doneAction() { + view.endEditing(true) + } } extension SwapSetupViewController: SwapSetupViewProtocol { diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index 72d26e1758..fae53d9e14 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -27,7 +27,9 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { $0.imageWithTitleView?.iconImage = R.image.iconActionSwap() } - let detailsView = SwapDetailsView() + let detailsView: SwapDetailsView = .create { + $0.setExpanded(false, animated: false) + } var rateCell: SwapRateView { detailsView.rateCell @@ -87,7 +89,7 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { ) rateCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupDetailsRate(preferredLanguages: locale.rLanguages) networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetwork(preferredLanguages: locale.rLanguages) - rateCell.setNeedsLayout() - detailsView.setNeedsLayout() + rateCell.titleButton.invalidateLayout() + networkFeeCell.titleButton.invalidateLayout() } } From a4b9b453f0c9a0b56ad32704e5e88c999bca2889 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 11 Oct 2023 09:18:48 +0300 Subject: [PATCH 029/204] check fee identifier before calculation --- .../Swaps/Setup/SwapSetupInteractor.swift | 26 +++++------------- .../Swaps/Setup/SwapSetupPresenter.swift | 27 +++++++++++++++---- .../Swaps/Setup/SwapSetupProtocols.swift | 8 ++---- 3 files changed, 30 insertions(+), 31 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 7bdc30fcc5..494f6333f3 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -144,30 +144,16 @@ extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { func update(payChainAsset: ChainAsset) { update(chainModel: payChainAsset.chain) performPriceSubscription(chainAsset: payChainAsset) + + let metaAccount = selectedAccount.fetchMetaChainAccount(for: payChainAsset.chain.accountRequest()) + let accountId = metaAccount?.chainAccount.accountId + presenter?.didReceive(payAccountId: accountId) } - @discardableResult func calculateFee( - for quote: AssetConversion.Quote, - slippage: SwapSlippage - ) -> TransactionFeeId? { - guard let receiver = accountId else { - return nil - } - - let args = AssetConversion.CallArgs( - assetIn: quote.assetIn, - amountIn: quote.amountIn, - assetOut: quote.assetOut, - amountOut: quote.amountOut, - receiver: receiver, - direction: slippage.direction, - slippage: .percent(of: slippage.slippage) - ) - + args: AssetConversion.CallArgs + ) { fee(args: args) - - return args.identifier } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index f7afb66244..c9ae6a4e23 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -24,6 +24,7 @@ final class SwapSetupPresenter { } private var feeIdentifier: String? + private var accountId: AccountId? init( interactor: SwapSetupInteractorInputProtocol, @@ -204,15 +205,27 @@ final class SwapSetupPresenter { } private func estimateFee() { - guard let quote = quote else { + guard let quote = quote, let accountId = accountId else { return } - // TODO: Remove hardcode slippage and direction - feeIdentifier = interactor.calculateFee( - for: quote, - slippage: .init(direction: .sell, slippage: 1) + // TODO: Provide slippage and direction + let args = AssetConversion.CallArgs( + assetIn: quote.assetIn, + amountIn: quote.amountIn, + assetOut: quote.assetOut, + amountOut: quote.amountOut, + receiver: accountId, + direction: .sell, + slippage: .percent(of: 1) ) + + guard args.identifier != feeIdentifier else { + return + } + + feeIdentifier = args.identifier + interactor.calculateFee(args: args) } private func refreshQuote(direction: AssetConversion.Direction) { @@ -403,6 +416,10 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { provideReceiveInputPriceViewModel() } } + + func didReceive(payAccountId: AccountId?) { + accountId = payAccountId + } } extension SwapSetupPresenter: Localizable { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 7833d4b257..1ec8bf174c 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -33,7 +33,7 @@ protocol SwapSetupInteractorInputProtocol: AnyObject { func update(receiveChainAsset: ChainAsset) func update(payChainAsset: ChainAsset) func calculateQuote(for args: AssetConversion.QuoteArgs) - func calculateFee(for quote: AssetConversion.Quote, slippage: SwapSlippage) -> TransactionFeeId? + func calculateFee(args: AssetConversion.CallArgs) -> Void } protocol SwapSetupInteractorOutputProtocol: AnyObject { @@ -41,6 +41,7 @@ protocol SwapSetupInteractorOutputProtocol: AnyObject { func didReceive(fee: BigUInt?, transactionId: TransactionFeeId) func didReceive(error: SwapSetupError) func didReceive(price: PriceData?, priceId: AssetModel.PriceId) + func didReceive(payAccountId: AccountId?) } protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, ErrorPresentable { @@ -61,8 +62,3 @@ enum SwapSetupError: Error { case fetchFeeFailed(Error, TransactionFeeId) case price(Error, AssetModel.PriceId) } - -struct SwapSlippage { - let direction: AssetConversion.Direction - let slippage: BigUInt -} From 903e10046b695def098420c08856e2a4540d755c Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 11 Oct 2023 09:59:55 +0300 Subject: [PATCH 030/204] add control opacity, change swap button size, use ViewModelFactory from assets screen --- novawallet.xcodeproj/project.pbxproj | 4 -- .../Swaps/SwapAssetListViewModelFactory.swift | 47 ------------------- .../SwapAssetsOperationViewFactory.swift | 2 +- .../Swaps/Setup/View/SwapAssetControl.swift | 1 + .../Setup/View/SwapSetupViewLayout.swift | 10 ++-- 5 files changed, 8 insertions(+), 56 deletions(-) delete mode 100644 novawallet/Modules/AssetsSearch/Swaps/SwapAssetListViewModelFactory.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 5ebc3a5f67..be2733f968 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -775,7 +775,6 @@ 77C9BCCE2ACD691300022EA2 /* SwapAssetsOperationViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCCD2ACD691300022EA2 /* SwapAssetsOperationViewFactory.swift */; }; 77C9BCD02ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCCF2ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift */; }; 77C9BCD22ACD8AF400022EA2 /* SwapAssetsOperationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCD12ACD8AF400022EA2 /* SwapAssetsOperationViewController.swift */; }; - 77C9BCD42ACDF1AD00022EA2 /* SwapAssetListViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCD32ACDF1AD00022EA2 /* SwapAssetListViewModelFactory.swift */; }; 77CB33CE2A38780700B6709A /* structures_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77CB33CD2A38780700B6709A /* structures_output.json */; }; 77CB33D22A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CB33D12A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift */; }; 77CB33D72A3998FD00B6709A /* Array+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CB33D62A3998FC00B6709A /* Array+Sort.swift */; }; @@ -4778,7 +4777,6 @@ 77C9BCCD2ACD691300022EA2 /* SwapAssetsOperationViewFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationViewFactory.swift; sourceTree = ""; }; 77C9BCCF2ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationViewLayout.swift; sourceTree = ""; }; 77C9BCD12ACD8AF400022EA2 /* SwapAssetsOperationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationViewController.swift; sourceTree = ""; }; - 77C9BCD32ACDF1AD00022EA2 /* SwapAssetListViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetListViewModelFactory.swift; sourceTree = ""; }; 77CB33CD2A38780700B6709A /* structures_output.json */ = {isa = PBXFileReference; explicitFileType = text.json; fileEncoding = 4; path = structures_output.json; sourceTree = ""; usesTabs = 0; wrapsLines = 0; }; 77CB33D12A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Web3NameIntegrityVerifierWithCanonicalizationData.swift; sourceTree = ""; }; 77CB33D62A3998FC00B6709A /* Array+Sort.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Sort.swift"; sourceTree = ""; }; @@ -9736,7 +9734,6 @@ 77C9BCCD2ACD691300022EA2 /* SwapAssetsOperationViewFactory.swift */, 77C9BCCF2ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift */, 77C9BCD12ACD8AF400022EA2 /* SwapAssetsOperationViewController.swift */, - 77C9BCD32ACDF1AD00022EA2 /* SwapAssetListViewModelFactory.swift */, ); path = Swaps; sourceTree = ""; @@ -22358,7 +22355,6 @@ 2F21134DE157A4B98ED309E2 /* AssetsSearchViewController.swift in Sources */, 8407716828CE8A1B007DBD24 /* ParaStkYieldBoostSetupInteractor+Children.swift in Sources */, 73B9C322A5033A4534238B25 /* AssetsSearchViewLayout.swift in Sources */, - 77C9BCD42ACDF1AD00022EA2 /* SwapAssetListViewModelFactory.swift in Sources */, 77ED167E2A0D0AE900E1FC8C /* Lenses.swift in Sources */, 6BBD025775841F8B055CA367 /* AssetsSearchViewFactory.swift in Sources */, 88C5F07E297EE7BC001CCADE /* InAppUpdatesRepository.swift in Sources */, diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetListViewModelFactory.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetListViewModelFactory.swift deleted file mode 100644 index d770718dd9..0000000000 --- a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetListViewModelFactory.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation - -final class SwapAssetListViewModelFactory: AssetListAssetViewModelFactory { - override func formatPrice(amount: Decimal, priceData: PriceData?, locale: Locale) -> String { - guard amount > 0 else { - return "" - } - - let formattedPrice = super.formatPrice( - amount: amount, - priceData: priceData, - locale: locale - ) - return wrap(price: formattedPrice) - } - - override func createBalanceState( - assetAccountInfo: AssetListAssetAccountInfo, - connected: Bool, - locale: Locale - ) -> (LoadableViewModelState, LoadableViewModelState) { - let (balanceState, priceState) = super.createBalanceState( - assetAccountInfo: assetAccountInfo, - connected: connected, - locale: locale - ) - guard let balance = assetAccountInfo.balance, balance > 0 else { - return (balanceState, priceState) - } - - switch priceState { - case .loading: - return (balanceState, priceState) - case let .cached(value): - return (balanceState, .cached(value: wrap(price: value))) - case let .loaded(value): - return (balanceState, .loaded(value: wrap(price: value))) - } - } - - private func wrap(price: String) -> String { - guard !price.isEmpty else { - return price - } - return "~\(price)" - } -} diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift index 28a1ccd005..8eaaf64f6a 100644 --- a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift @@ -51,7 +51,7 @@ enum SwapAssetsOperationViewFactory { } let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) - let viewModelFactory = SwapAssetListViewModelFactory( + let viewModelFactory = AssetListAssetViewModelFactory( priceAssetInfoFactory: priceAssetInfoFactory, assetFormatterFactory: AssetBalanceFormatterFactory(), percentFormatter: NumberFormatter.signedPercent.localizableResource(), diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift b/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift index b67b5e73d6..4a1fae4079 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift @@ -100,6 +100,7 @@ final class SwapAssetControl: BackgroundedContentControl { } contentView?.addSubview(assetView) + changesContentOpacityWhenHighlighted = true } private func lazyIconViewOrCreateIfNeeded() -> AssetIconView { diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index fae53d9e14..5da6e250af 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -77,8 +77,8 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { addSubview(switchButton) switchButton.snp.makeConstraints { $0.height.equalTo(switchButton.snp.width) - $0.top.equalTo(payAmountInputView.snp.bottom).offset(9) - $0.bottom.equalTo(receiveAmountInputView.snp.top).offset(-9) + $0.top.equalTo(payAmountInputView.snp.bottom).offset(4) + $0.bottom.equalTo(receiveAmountInputView.snp.top).offset(-4) $0.centerX.equalTo(payAmountInputView.snp.centerX) } } @@ -87,8 +87,10 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { detailsView.titleControl.titleLabel.text = R.string.localizable.swapsSetupDetailsTitle( preferredLanguages: locale.rLanguages ) - rateCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupDetailsRate(preferredLanguages: locale.rLanguages) - networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetwork(preferredLanguages: locale.rLanguages) + rateCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupDetailsRate( + preferredLanguages: locale.rLanguages) + networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetwork( + preferredLanguages: locale.rLanguages) rateCell.titleButton.invalidateLayout() networkFeeCell.titleButton.invalidateLayout() } From 680a0d4692134ce13a8f648c65ac62570758991d Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 11 Oct 2023 21:35:19 +0300 Subject: [PATCH 031/204] max button, info sheets --- novawallet.xcodeproj/project.pbxproj | 4 + .../TitleDetailsSheetLayout.swift | 29 ++++++ .../TitleDetailsSheetViewFactory.swift | 29 ++++++ .../Model/SwapsSetupViewModelFactory.swift | 2 +- .../Swaps/Setup/SwapSetupInteractor.swift | 96 +++++++++++++------ .../Swaps/Setup/SwapSetupPresenter.swift | 62 +++++++++++- .../Swaps/Setup/SwapSetupProtocols.swift | 11 ++- .../Swaps/Setup/SwapSetupViewController.swift | 9 ++ .../Swaps/Setup/SwapSetupViewFactory.swift | 1 + .../Swaps/Setup/SwapSetupWireframe.swift | 24 +++++ .../Swaps/Setup/View/SwapMaxButtonView.swift | 36 +++++++ .../Setup/View/SwapSetupViewLayout.swift | 6 +- novawallet/en.lproj/Localizable.strings | 2 + novawallet/ru.lproj/Localizable.strings | 2 + 14 files changed, 274 insertions(+), 39 deletions(-) create mode 100644 novawallet/Modules/Swaps/Setup/View/SwapMaxButtonView.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index be2733f968..343f52d9cc 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -704,6 +704,7 @@ 77740BBC2AD4A7B800E8C06F /* CollapsableContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBB2AD4A7B800E8C06F /* CollapsableContainerView.swift */; }; 77740BBE2AD4A7F500E8C06F /* SwapDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */; }; 77740BC02AD4A80D00E8C06F /* SwapRateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */; }; + 77740BC22AD69E3400E8C06F /* SwapMaxButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BC12AD69E3400E8C06F /* SwapMaxButtonView.swift */; }; 77799ADD2A74219A00B7E564 /* ButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799ADC2A74219A00B7E564 /* ButtonState.swift */; }; 77799ADF2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799ADE2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift */; }; 77799AE52A792AE700B7E564 /* StakingTypeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799AE42A792AE700B7E564 /* StakingTypeViewModel.swift */; }; @@ -4706,6 +4707,7 @@ 77740BBB2AD4A7B800E8C06F /* CollapsableContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsableContainerView.swift; sourceTree = ""; }; 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapDetailsView.swift; sourceTree = ""; }; 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRateView.swift; sourceTree = ""; }; + 77740BC12AD69E3400E8C06F /* SwapMaxButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapMaxButtonView.swift; sourceTree = ""; }; 77799ADC2A74219A00B7E564 /* ButtonState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonState.swift; sourceTree = ""; }; 77799ADE2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoRelaychainWireframe.swift; sourceTree = ""; }; 77799AE42A792AE700B7E564 /* StakingTypeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingTypeViewModel.swift; sourceTree = ""; }; @@ -9550,6 +9552,7 @@ 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */, 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */, 77ECB46F2ACEEE2D0015CE9F /* SwapNetworkFeeView.swift */, + 77740BC12AD69E3400E8C06F /* SwapMaxButtonView.swift */, ); path = View; sourceTree = ""; @@ -22157,6 +22160,7 @@ B6DB30A8D1BF84158CAC635D /* OperationDetailsInteractor.swift in Sources */, C9931414951375760E5D1C57 /* OperationDetailsViewController.swift in Sources */, 84FBED0129277CD700FBEB83 /* EvmAssetBalanceUpdatingService.swift in Sources */, + 77740BC22AD69E3400E8C06F /* SwapMaxButtonView.swift in Sources */, 84ADA61029B9E2E800EB687E /* MultiExtrinsicRetryable.swift in Sources */, 67684F7576ED0252C1050CA5 /* OperationDetailsViewLayout.swift in Sources */, 77F189442A49974A00E8B933 /* UITextView+bind.swift in Sources */, diff --git a/novawallet/Modules/MessageSheet/TitleDetails/TitleDetailsSheetLayout.swift b/novawallet/Modules/MessageSheet/TitleDetails/TitleDetailsSheetLayout.swift index dc028268cf..96134c8d71 100644 --- a/novawallet/Modules/MessageSheet/TitleDetails/TitleDetailsSheetLayout.swift +++ b/novawallet/Modules/MessageSheet/TitleDetails/TitleDetailsSheetLayout.swift @@ -83,3 +83,32 @@ final class TitleDetailsSheetViewLayout: UIView { } } } + +extension TitleDetailsSheetViewLayout { + func contentHeight(model: TitleDetailsSheetViewModel, locale: Locale) -> CGFloat { + let titleHeight = height(for: titleLabel, with: model.title.value(for: locale)) + let messageHeight = height(for: detailsLabel, with: model.message.value(for: locale)) + let topOffset: CGFloat = 10 + let buttonHeight: CGFloat + if model.mainAction != nil || model.secondaryAction != nil { + let bottomOffset: CGFloat = 16 + buttonHeight = UIConstants.actionHeight + bottomOffset + } else { + buttonHeight = 0 + } + + return topOffset + titleHeight + 10 + messageHeight + buttonHeight + } + + private func height(for label: UILabel, with text: String) -> CGFloat { + let width = UIScreen.main.bounds.width - UIConstants.horizontalInset * 2 + let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude) + let boundingBox = text.boundingRect( + with: constraintRect, + options: .usesLineFragmentOrigin, + attributes: [.font: label.font], + context: nil + ) + return boundingBox.height + } +} diff --git a/novawallet/Modules/MessageSheet/TitleDetails/TitleDetailsSheetViewFactory.swift b/novawallet/Modules/MessageSheet/TitleDetails/TitleDetailsSheetViewFactory.swift index aeefb4a164..4415716417 100644 --- a/novawallet/Modules/MessageSheet/TitleDetails/TitleDetailsSheetViewFactory.swift +++ b/novawallet/Modules/MessageSheet/TitleDetails/TitleDetailsSheetViewFactory.swift @@ -24,4 +24,33 @@ struct TitleDetailsSheetViewFactory { return view } + + static func createSelfSizedView( + from viewModel: TitleDetailsSheetViewModel, + allowsSwipeDown: Bool = true + ) -> MessageSheetViewProtocol { + let wireframe = MessageSheetWireframe() + + let presenter = MessageSheetPresenter(wireframe: wireframe) + + let view = TitleDetailsSheetViewController( + presenter: presenter, + viewModel: viewModel, + localizationManager: LocalizationManager.shared + ) + + view.allowsSwipeDown = allowsSwipeDown + let height = view.rootView.contentHeight( + model: viewModel, + locale: LocalizationManager.shared.selectedLocale + ) + + view.preferredContentSize = .init( + width: UIView.noIntrinsicMetric, + height: height + ) + presenter.view = view + + return view + } } diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift index 1ff095a08e..817aca234a 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift @@ -135,7 +135,7 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { if let assetDisplayInfo = assetDisplayInfo, let maxValue = maxValue { let amountDecimal = Decimal.fromSubstrateAmount( maxValue, - precision: Int16(assetDisplayInfo.displayPrecision) + precision: Int16(assetDisplayInfo.assetPrecision) ) ?? 0 let maxValueString = balanceViewModelFactoryFacade.amountFromValue( targetAssetInfo: assetDisplayInfo, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 494f6333f3..c54c633aba 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -10,6 +10,7 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning let feeProxy: ExtrinsicFeeProxyProtocol let extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol + let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol let currencyManager: CurrencyManagerProtocol let selectedAccount: MetaAccountModel @@ -17,9 +18,10 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning private var quoteCall: CancellableCall? private var runtimeOperationCall: CancellableCall? private var extrinsicService: ExtrinsicServiceProtocol? - private var accountId: AccountId? - private var priceProviders: [ChainAssetId: StreamableProvider] = [:] + private var payPriceProvider: StreamableProvider? + private var receivePriceProvider: StreamableProvider? + private var balanceProvider: StreamableProvider? init( assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, @@ -28,6 +30,7 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning feeProxy: ExtrinsicFeeProxyProtocol, extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, currencyManager: CurrencyManagerProtocol, selectedAccount: MetaAccountModel, operationQueue: OperationQueue @@ -38,24 +41,35 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning self.feeProxy = feeProxy self.extrinsicServiceFactory = extrinsicServiceFactory self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory + self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory self.currencyManager = currencyManager self.selectedAccount = selectedAccount self.operationQueue = operationQueue } - private func performPriceSubscription(chainAsset: ChainAsset) { - clear(streamableProvider: &priceProviders[chainAsset.chainAssetId]) - + private func priceSubscription(chainAsset: ChainAsset) -> StreamableProvider? { guard let priceId = chainAsset.asset.priceId else { - return + return nil } - priceProviders[chainAsset.chainAssetId] = subscribeToPrice( + return subscribeToPrice( for: priceId, currency: currencyManager.selectedCurrency ) } + private func assetBalanceSubscription(chainAsset: ChainAsset) -> StreamableProvider? { + guard let accountId = chainAccountResponse(for: chainAsset)?.accountId else { + return nil + } + let chainAssetId = chainAsset.chainAssetId + return subscribeToAssetBalanceProvider( + for: accountId, + chainId: chainAssetId.chainId, + assetId: chainAssetId.assetId + ) + } + private func quote(args: AssetConversion.QuoteArgs) { clear(cancellable: "eCall) @@ -79,19 +93,6 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) } - private func update(chainModel: ChainModel) { - guard let metaChainAccountResponse = selectedAccount.fetchMetaChainAccount( - for: chainModel.accountRequest() - ) else { - return - } - extrinsicService = extrinsicServiceFactory.createService( - account: metaChainAccountResponse.chainAccount, - chain: chainModel - ) - accountId = metaChainAccountResponse.chainAccount.accountId - } - private func fee(args: AssetConversion.CallArgs) { clear(cancellable: &runtimeOperationCall) guard let extrinsicService = extrinsicService else { @@ -125,6 +126,11 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning runtimeOperationCall = runtimeCoderFactoryOperation operationQueue.addOperation(runtimeCoderFactoryOperation) } + + func chainAccountResponse(for chainAsset: ChainAsset) -> ChainAccountResponse? { + let metaChainAccountResponse = selectedAccount.fetchMetaChainAccount(for: chainAsset.chain.accountRequest()) + return metaChainAccountResponse?.chainAccount + } } extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { @@ -137,17 +143,26 @@ extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { } func update(receiveChainAsset: ChainAsset) { - update(chainModel: receiveChainAsset.chain) - performPriceSubscription(chainAsset: receiveChainAsset) + clear(streamableProvider: &receivePriceProvider) + receivePriceProvider = priceSubscription(chainAsset: receiveChainAsset) } func update(payChainAsset: ChainAsset) { - update(chainModel: payChainAsset.chain) - performPriceSubscription(chainAsset: payChainAsset) - - let metaAccount = selectedAccount.fetchMetaChainAccount(for: payChainAsset.chain.accountRequest()) - let accountId = metaAccount?.chainAccount.accountId - presenter?.didReceive(payAccountId: accountId) + clear(streamableProvider: &payPriceProvider) + payPriceProvider = priceSubscription(chainAsset: payChainAsset) + + clear(streamableProvider: &balanceProvider) + balanceProvider = assetBalanceSubscription(chainAsset: payChainAsset) + + if let chainAccount = chainAccountResponse(for: payChainAsset) { + extrinsicService = extrinsicServiceFactory.createService( + account: chainAccount, + chain: payChainAsset.chain + ) + presenter?.didReceive(payAccountId: chainAccount.accountId) + } else { + presenter?.didReceive(payAccountId: nil) + } } func calculateFee( @@ -181,3 +196,28 @@ extension SwapSetupInteractor: PriceLocalStorageSubscriber, PriceLocalSubscripti } } } + +extension SwapSetupInteractor: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { + func handleAssetBalance( + result: Result, + accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) { + let chainAssetId = ChainAssetId(chainId: chainId, assetId: assetId) + switch result { + case let .success(balance): + let balance = balance ?? .createZero( + for: .init(chainId: chainId, assetId: assetId), + accountId: accountId + ) + presenter?.didReceive( + balance: balance, + for: chainAssetId, + accountId: accountId + ) + case let .failure(error): + presenter?.didReceive(error: .assetBalance(error, chainAssetId, accountId)) + } + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index c9ae6a4e23..8ceefcb233 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -327,14 +327,51 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { refreshQuote(direction: quoteArgs?.direction ?? .sell) } + func selectMaxPayAmount() { + payAmountInput = .rate(1) + providePayAssetViews() + refreshQuote(direction: .sell) + provideButtonState() + } + // TODO: show editing fee func showFeeActions() {} - // TODO: show fee information - func showFeeInfo() {} + func showFeeInfo() { + let title = LocalizableResource { + R.string.localizable.commonNetwork( + preferredLanguages: $0.rLanguages + ) + } + let details = LocalizableResource { + R.string.localizable.swapsNetworkFeeDescription( + preferredLanguages: $0.rLanguages + ) + } + wireframe.showInfo( + from: view, + title: title, + details: details + ) + } - // TODO: show rate information - func showRateInfo() {} + func showRateInfo() { + let title = LocalizableResource { + R.string.localizable.swapsSetupDetailsRate( + preferredLanguages: $0.rLanguages + ) + } + let details = LocalizableResource { + R.string.localizable.swapsRateDescription( + preferredLanguages: $0.rLanguages + ) + } + wireframe.showInfo( + from: view, + title: title, + details: details + ) + } // TODO: navigate to confirm screen func proceed() {} @@ -361,6 +398,15 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in self?.estimateFee() } + case let .assetBalance(_, chainAssetId, accountId): + guard accountId == self.accountId, + let payChainAsset = payChainAsset, + payChainAsset.chainAssetId == chainAssetId else { + return + } + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.update(payChainAsset: payChainAsset) + } } } @@ -420,6 +466,14 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { func didReceive(payAccountId: AccountId?) { accountId = payAccountId } + + func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId: AccountId) { + guard accountId == self.accountId, payChainAsset?.chainAssetId == chainAsset else { + return + } + assetBalance = balance + providePayTitle() + } } extension SwapSetupPresenter: Localizable { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 1ec8bf174c..8393a12ce9 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -1,4 +1,5 @@ import BigInt +import SoraFoundation protocol SwapSetupViewProtocol: ControllerBackedProtocol { func didReceiveButtonState(title: String, enabled: Bool) @@ -26,6 +27,7 @@ protocol SwapSetupPresenterProtocol: AnyObject { func showFeeActions() func showFeeInfo() func showRateInfo() + func selectMaxPayAmount() } protocol SwapSetupInteractorInputProtocol: AnyObject { @@ -33,7 +35,7 @@ protocol SwapSetupInteractorInputProtocol: AnyObject { func update(receiveChainAsset: ChainAsset) func update(payChainAsset: ChainAsset) func calculateQuote(for args: AssetConversion.QuoteArgs) - func calculateFee(args: AssetConversion.CallArgs) -> Void + func calculateFee(args: AssetConversion.CallArgs) } protocol SwapSetupInteractorOutputProtocol: AnyObject { @@ -42,6 +44,7 @@ protocol SwapSetupInteractorOutputProtocol: AnyObject { func didReceive(error: SwapSetupError) func didReceive(price: PriceData?, priceId: AssetModel.PriceId) func didReceive(payAccountId: AccountId?) + func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId: AccountId) } protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, ErrorPresentable { @@ -55,10 +58,16 @@ protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryabl chainAsset: ChainAsset?, completionHandler: @escaping (ChainAsset) -> Void ) + func showInfo( + from view: ControllerBackedProtocol?, + title: LocalizableResource, + details: LocalizableResource + ) } enum SwapSetupError: Error { case quote(Error, AssetConversion.QuoteArgs) case fetchFeeFailed(Error, TransactionFeeId) case price(Error, AssetModel.PriceId) + case assetBalance(Error, ChainAssetId, AccountId) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 186b96314b..41d9fd674f 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -38,6 +38,11 @@ final class SwapSetupViewController: UIViewController, ViewHolder { action: #selector(selectPayTokenAction), for: .touchUpInside ) + rootView.payAmountView.button.addTarget( + self, + action: #selector(payMaxAction), + for: .touchUpInside + ) rootView.receiveAmountInputView.assetControl.addTarget( self, action: #selector(selectReceiveTokenAction), @@ -137,6 +142,10 @@ final class SwapSetupViewController: UIViewController, ViewHolder { presenter.showRateInfo() } + @objc private func payMaxAction() { + presenter.selectMaxPayAmount() + } + @objc private func doneAction() { view.endEditing(true) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index 4794c04205..e20538832a 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -73,6 +73,7 @@ struct SwapSetupViewFactory { feeProxy: ExtrinsicFeeProxy(), extrinsicServiceFactory: extrinsicServiceFactory, priceLocalSubscriptionFactory: PriceProviderFactory.shared, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, currencyManager: currencyManager, selectedAccount: selectedAccount, operationQueue: operationQueue diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index b80c56af15..bb28b6d2ab 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -1,4 +1,6 @@ import Foundation +import SoraFoundation +import SoraUI final class SwapSetupWireframe: SwapSetupWireframeProtocol { let assetListObservable: AssetListModelObservable @@ -46,4 +48,26 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { view?.controller.present(navigationController, animated: true, completion: nil) } + + func showInfo( + from view: ControllerBackedProtocol?, + title: LocalizableResource, + details: LocalizableResource + ) { + let viewModel = TitleDetailsSheetViewModel( + title: title, + message: details, + mainAction: nil, + secondaryAction: nil + ) + + let bottomSheet = TitleDetailsSheetViewFactory.createSelfSizedView(from: viewModel) + + let factory = ModalSheetPresentationFactory(configuration: ModalSheetPresentationConfiguration.nova) + + bottomSheet.controller.modalTransitioningFactory = factory + bottomSheet.controller.modalPresentationStyle = .custom + + view?.controller.present(bottomSheet.controller, animated: true) + } } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapMaxButtonView.swift b/novawallet/Modules/Swaps/Setup/View/SwapMaxButtonView.swift new file mode 100644 index 0000000000..37f2f76681 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/View/SwapMaxButtonView.swift @@ -0,0 +1,36 @@ +import UIKit +import SoraUI + +typealias SwapSetupTitleButton = ControlView> +final class SwapSetupTitleView: GenericTitleValueView { + var titleLabel: UILabel { titleView } + var button: SwapSetupTitleButton { valueView } + var buttonTitle: UILabel { button.controlContentView.fView } + var buttonValue: UILabel { button.controlContentView.sView } + + override init(frame: CGRect) { + super.init(frame: frame) + + configure() + button.backgroundView?.backgroundColor = .clear + } + + private func configure() { + titleView.apply(style: .footnoteSecondary) + buttonTitle.apply(style: .footnoteAccentText) + buttonValue.apply(style: .footnotePrimary) + button.controlContentView.spacing = 4 + button.controlContentView.stackView.axis = .horizontal + button.contentInsets = UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0) + button.changesContentOpacityWhenHighlighted = true + } +} + +extension SwapSetupTitleView { + func bind(model: TitleHorizontalMultiValueView.Model) { + titleView.text = model.title + buttonTitle.text = model.subtitle + buttonValue.text = model.value + button.invalidateLayout() + } +} diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index 5da6e250af..91632c2387 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -2,11 +2,7 @@ import UIKit import SoraUI final class SwapSetupViewLayout: ScrollableContainerLayoutView { - let payAmountView: TitleHorizontalMultiValueView = .create { - $0.titleView.apply(style: .footnoteSecondary) - $0.detailsTitleLabel.apply(style: .footnoteAccentText) - $0.detailsValueLabel.apply(style: .footnotePrimary) - } + let payAmountView = SwapSetupTitleView(frame: .zero) let payAmountInputView = SwapAmountInputView() diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index b0d695df7a..2731c17641 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1391,3 +1391,5 @@ "swaps.setup.details.title" = "Swap details"; "swaps.pay.token.selection.title" = "Token to pay"; "swaps.receive.token.selection.title" = "Token to receive"; +"swaps.rate.description" = "Exchange rate between two different cryptocurrencies. It represents how much of one cryptocurrency you can get in exchange for a certain amount of another cryptocurrency."; +"swaps.network.fee.description" = "A network fees charged by the blockchain to process and validate any transactions. May vary depending on network conditions or transaction speed."; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 5048b5b358..2d9cc99c1d 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1392,3 +1392,5 @@ "swaps.setup.details.title" = "Детали обмена"; "swaps.pay.token.selection.title" = "Токен для оплаты"; "swaps.receive.token.selection.title" = "Токен для получения"; +"swaps.rate.description" = "Обменный курс между двумя различными криптовалютами. Он представляет, сколько одной криптовалюты вы можете получить в обмен на определенное количество другой криптовалюты."; +"swaps.network.fee.description" = "Это комиссия сети, взимаемая блокчейном за обработку и подтверждение любых транзакций. Она может изменяться в зависимости от условий в сети или скорости выполнения транзакции."; From 2e02f16b697f65b866f1dcc7c4332b54cae7c023 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 11 Oct 2023 22:35:58 +0300 Subject: [PATCH 032/204] fixes --- .../TitleDetailsSheetLayout.swift | 13 +++----- .../Swaps/Setup/SwapSetupInteractor.swift | 31 ++++++++++++------- .../Swaps/Setup/SwapSetupPresenter.swift | 30 ++++++++++++------ .../Swaps/Setup/SwapSetupProtocols.swift | 4 +-- .../Swaps/Setup/SwapSetupViewController.swift | 1 + 5 files changed, 47 insertions(+), 32 deletions(-) diff --git a/novawallet/Modules/MessageSheet/TitleDetails/TitleDetailsSheetLayout.swift b/novawallet/Modules/MessageSheet/TitleDetails/TitleDetailsSheetLayout.swift index 96134c8d71..c009e8700f 100644 --- a/novawallet/Modules/MessageSheet/TitleDetails/TitleDetailsSheetLayout.swift +++ b/novawallet/Modules/MessageSheet/TitleDetails/TitleDetailsSheetLayout.swift @@ -89,15 +89,10 @@ extension TitleDetailsSheetViewLayout { let titleHeight = height(for: titleLabel, with: model.title.value(for: locale)) let messageHeight = height(for: detailsLabel, with: model.message.value(for: locale)) let topOffset: CGFloat = 10 - let buttonHeight: CGFloat - if model.mainAction != nil || model.secondaryAction != nil { - let bottomOffset: CGFloat = 16 - buttonHeight = UIConstants.actionHeight + bottomOffset - } else { - buttonHeight = 0 - } - - return topOffset + titleHeight + 10 + messageHeight + buttonHeight + let bottomOffset: CGFloat = 16 + let hasAnyActionButton = model.mainAction != nil || model.secondaryAction != nil + let buttonHeight: CGFloat = hasAnyActionButton ? UIConstants.actionHeight : 0 + return topOffset + titleHeight + 10 + messageHeight + buttonHeight + bottomOffset } private func height(for label: UILabel, with text: String) -> CGFloat { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index c54c633aba..46236fcf1e 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -142,25 +142,32 @@ extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { quote(args: args) } - func update(receiveChainAsset: ChainAsset) { + func update(receiveChainAsset: ChainAsset?) { clear(streamableProvider: &receivePriceProvider) - receivePriceProvider = priceSubscription(chainAsset: receiveChainAsset) + if let receiveChainAsset = receiveChainAsset { + receivePriceProvider = priceSubscription(chainAsset: receiveChainAsset) + } } - func update(payChainAsset: ChainAsset) { + func update(payChainAsset: ChainAsset?) { clear(streamableProvider: &payPriceProvider) - payPriceProvider = priceSubscription(chainAsset: payChainAsset) - clear(streamableProvider: &balanceProvider) - balanceProvider = assetBalanceSubscription(chainAsset: payChainAsset) - if let chainAccount = chainAccountResponse(for: payChainAsset) { - extrinsicService = extrinsicServiceFactory.createService( - account: chainAccount, - chain: payChainAsset.chain - ) - presenter?.didReceive(payAccountId: chainAccount.accountId) + if let payChainAsset = payChainAsset { + payPriceProvider = priceSubscription(chainAsset: payChainAsset) + balanceProvider = assetBalanceSubscription(chainAsset: payChainAsset) + + if let chainAccount = chainAccountResponse(for: payChainAsset) { + extrinsicService = extrinsicServiceFactory.createService( + account: chainAccount, + chain: payChainAsset.chain + ) + presenter?.didReceive(payAccountId: chainAccount.accountId) + } else { + presenter?.didReceive(payAccountId: nil) + } } else { + extrinsicService = nil presenter?.didReceive(payAccountId: nil) } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 8ceefcb233..16864d3c1a 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -228,7 +228,7 @@ final class SwapSetupPresenter { interactor.calculateFee(args: args) } - private func refreshQuote(direction: AssetConversion.Direction) { + private func refreshQuote(direction: AssetConversion.Direction, forceUpdate: Bool = true) { guard let payChainAsset = payChainAsset, let receiveChainAsset = receiveChainAsset else { @@ -236,6 +236,7 @@ final class SwapSetupPresenter { } quote = nil + switch direction { case .buy: if let receiveInPlank = receiveAmountInput?.toSubstrateAmount(precision: Int16(receiveChainAsset.asset.precision)), receiveInPlank > 0 { @@ -249,9 +250,13 @@ final class SwapSetupPresenter { interactor.calculateQuote(for: quoteArgs) } else { quoteArgs = nil + if forceUpdate { + payAmountInput = nil + providePayAmountInputViewModel() + } else { + refreshQuote(direction: .sell) + } } - payAmountInput = nil - providePayAmountInputViewModel() case .sell: if let payInPlank = absoluteValue(for: payAmountInput)?.toSubstrateAmount( precision: Int16(payChainAsset.asset.precision)), payInPlank > 0 { @@ -265,10 +270,13 @@ final class SwapSetupPresenter { interactor.calculateQuote(for: quoteArgs) } else { quoteArgs = nil + if forceUpdate { + receiveAmountInput = nil + provideReceiveAmountInputViewModel() + } else { + refreshQuote(direction: .buy) + } } - - receiveAmountInput = nil - provideReceiveAmountInputViewModel() default: break } @@ -292,7 +300,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { self?.payChainAsset = chainAsset self?.providePayAssetViews() self?.provideButtonState() - self?.refreshQuote(direction: .sell) + self?.refreshQuote(direction: .sell, forceUpdate: false) self?.interactor.update(payChainAsset: chainAsset) } } @@ -302,7 +310,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { self?.receiveChainAsset = chainAsset self?.provideReceiveAssetViews() self?.provideButtonState() - self?.refreshQuote(direction: .buy) + self?.refreshQuote(direction: .buy, forceUpdate: false) self?.interactor.update(receiveChainAsset: chainAsset) } } @@ -321,10 +329,14 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { func swap() { Swift.swap(&payChainAsset, &receiveChainAsset) + interactor.update(payChainAsset: payChainAsset) + interactor.update(receiveChainAsset: receiveChainAsset) + payAmountInput = nil + receiveAmountInput = nil providePayAssetViews() provideReceiveAssetViews() provideButtonState() - refreshQuote(direction: quoteArgs?.direction ?? .sell) + refreshQuote(direction: .sell, forceUpdate: false) } func selectMaxPayAmount() { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 8393a12ce9..2454a7ec2b 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -32,8 +32,8 @@ protocol SwapSetupPresenterProtocol: AnyObject { protocol SwapSetupInteractorInputProtocol: AnyObject { func setup() - func update(receiveChainAsset: ChainAsset) - func update(payChainAsset: ChainAsset) + func update(receiveChainAsset: ChainAsset?) + func update(payChainAsset: ChainAsset?) func calculateQuote(for args: AssetConversion.QuoteArgs) func calculateFee(args: AssetConversion.CallArgs) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 41d9fd674f..eb36ce16ec 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -117,6 +117,7 @@ final class SwapSetupViewController: UIViewController, ViewHolder { } @objc private func swapAction() { + view.endEditing(true) presenter.swap() } From b37f025df534bc3a10bdce49892b569882fc4cd9 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 12 Oct 2023 13:50:41 +0300 Subject: [PATCH 033/204] add separator --- .../Extension/UIKit/UIView+Separator.swift | 16 ++++++++++++++++ .../Common/View/CollapsableContainerView.swift | 2 +- .../Swaps/Setup/View/SwapDetailsView.swift | 1 + 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/novawallet/Common/Extension/UIKit/UIView+Separator.swift b/novawallet/Common/Extension/UIKit/UIView+Separator.swift index 6e7a0b4384..0c08e57077 100644 --- a/novawallet/Common/Extension/UIKit/UIView+Separator.swift +++ b/novawallet/Common/Extension/UIKit/UIView+Separator.swift @@ -1,4 +1,5 @@ import UIKit +import SnapKit extension UIView { static func createSeparator(color: UIColor? = R.color.colorDivider()) -> UIView { @@ -6,4 +7,19 @@ extension UIView { view.backgroundColor = color return view } + + func addBottomSeparator( + _ height: CGFloat = 1, + color: UIColor = R.color.colorDivider()!, + horizontalSpace: CGFloat = 0 + ) { + let separator = UIView.createSeparator(color: color) + addSubview(separator) + + separator.snp.makeConstraints { + $0.leading.trailing.equalToSuperview().inset(horizontalSpace) + $0.bottom.equalToSuperview() + $0.height.equalTo(height) + } + } } diff --git a/novawallet/Common/View/CollapsableContainerView.swift b/novawallet/Common/View/CollapsableContainerView.swift index 8421b618ec..7f7e6a4f29 100644 --- a/novawallet/Common/View/CollapsableContainerView.swift +++ b/novawallet/Common/View/CollapsableContainerView.swift @@ -149,7 +149,7 @@ class CollapsableContainerView: UIView { private func applyExpansionState() { if expanded { contentView.snp.updateConstraints { make in - make.top.bottom.equalToSuperview().offset(0) + make.top.bottom.equalToSuperview() } layoutIfNeeded() delegate?.didChangeExpansion(isExpanded: true, sender: self) diff --git a/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift b/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift index ce28e509ac..d557fe67e7 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift @@ -5,6 +5,7 @@ final class SwapDetailsView: CollapsableContainerView { $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote $0.titleView.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() + $0.addBottomSeparator() } let networkFeeCell = SwapNetworkFeeView(frame: .zero) From 4eb1e83d3efbf42e2e10fad6910d564c7d8c06fc Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 13 Oct 2023 12:22:58 +0300 Subject: [PATCH 034/204] include fee to input when max button was selected --- .../Swaps/Setup/SwapSetupPresenter.swift | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 16864d3c1a..7a69cdcf9f 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -42,7 +42,7 @@ final class SwapSetupPresenter { let buttonState = viewModelFactory.buttonState( assetIn: payChainAsset?.chainAssetId, assetOut: receiveChainAsset?.chainAssetId, - amountIn: absoluteValue(for: payAmountInput), + amountIn: getPayAmount(for: payAmountInput), amountOut: receiveAmountInput ) view?.didReceiveButtonState( @@ -75,7 +75,7 @@ final class SwapSetupPresenter { } let inputPriceViewModel = viewModelFactory.inputPriceViewModel( assetDisplayInfo: assetDisplayInfo, - amount: absoluteValue(for: payAmountInput), + amount: getPayAmount(for: payAmountInput), priceData: payAssetPriceData, locale: selectedLocale ) @@ -115,7 +115,7 @@ final class SwapSetupPresenter { } let amountInputViewModel = viewModelFactory.amountInputViewModel( chainAsset: payChainAsset, - amount: absoluteValue(for: payAmountInput), + amount: getPayAmount(for: payAmountInput), locale: selectedLocale ) view?.didReceiveAmount(payInputViewModel: amountInputViewModel) @@ -133,21 +133,29 @@ final class SwapSetupPresenter { view?.didReceiveAmount(receiveInputViewModel: amountInputViewModel) } - private func absoluteValue(for input: AmountInputResult?) -> Decimal? { + private func getPayAmount(for input: AmountInputResult?) -> Decimal? { guard let input = input, let payChainAsset = payChainAsset else { return nil } - guard let transferrableBalanceDecimal = - Decimal.fromSubstrateAmount( - assetBalance?.transferable ?? 0, - precision: payChainAsset.asset.displayInfo.assetPrecision - ) else { - return nil + let includedFee: BigUInt + switch input { + case .rate: + includedFee = fee ?? 0 + case .absolute: + includedFee = 0 } - return input.absoluteValue(from: transferrableBalanceDecimal) + let transferable = assetBalance?.transferable ?? 0 + let balance = max(transferable - includedFee, 0) + guard let balanceDecimal = Decimal.fromSubstrateAmount( + balance, + precision: payChainAsset.asset.displayInfo.assetPrecision + ) else { + return nil + } + return input.absoluteValue(from: balanceDecimal) } private func providePayAssetViews() { @@ -258,7 +266,7 @@ final class SwapSetupPresenter { } } case .sell: - if let payInPlank = absoluteValue(for: payAmountInput)?.toSubstrateAmount( + if let payInPlank = getPayAmount(for: payAmountInput)?.toSubstrateAmount( precision: Int16(payChainAsset.asset.precision)), payInPlank > 0 { let quoteArgs = AssetConversion.QuoteArgs( assetIn: payChainAsset.chainAssetId, From e23370066d9fa7986221a817a2c466bbfb20e234 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Sat, 14 Oct 2023 11:23:10 +0300 Subject: [PATCH 035/204] init --- novawallet.xcodeproj/project.pbxproj | 76 ++++++++ .../Amount/AmountInputFormatterPlugin.swift | 41 +++++ .../Amount/AmountInputViewModel.swift | 11 +- .../ViewModel/Amount/MoneyPresentable.swift | 38 ++-- .../Swaps/Setup/SwapSetupPresenter.swift | 25 ++- .../Swaps/Setup/SwapSetupProtocols.swift | 5 + .../Swaps/Setup/SwapSetupViewController.swift | 15 ++ .../Swaps/Setup/SwapSetupWireframe.swift | 16 ++ .../Swaps/Slippage/Model/Percent.swift | 6 + .../Slippage/SwapSlippageInputView.swift | 172 ++++++++++++++++++ .../Slippage/SwapSlippageInteractor.swift | 7 + .../Slippage/SwapSlippagePresenter.swift | 100 ++++++++++ .../Slippage/SwapSlippageProtocols.swift | 21 +++ .../Slippage/SwapSlippageViewController.swift | 100 ++++++++++ .../Slippage/SwapSlippageViewFactory.swift | 33 ++++ .../Slippage/SwapSlippageViewLayout.swift | 43 +++++ .../Slippage/SwapSlippageWireframe.swift | 7 + .../Common/Helpers/MoneyPresentableMock.swift | 10 + .../Helpers/MoneyPresentableTests.swift | 57 ++++++ .../SwapSlippage/SwapSlippageTests.swift | 16 ++ 20 files changed, 783 insertions(+), 16 deletions(-) create mode 100644 novawallet/Common/ViewModel/Amount/AmountInputFormatterPlugin.swift create mode 100644 novawallet/Modules/Swaps/Slippage/Model/Percent.swift create mode 100644 novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift create mode 100644 novawallet/Modules/Swaps/Slippage/SwapSlippageInteractor.swift create mode 100644 novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift create mode 100644 novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift create mode 100644 novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift create mode 100644 novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift create mode 100644 novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift create mode 100644 novawallet/Modules/Swaps/Slippage/SwapSlippageWireframe.swift create mode 100644 novawalletTests/Common/Helpers/MoneyPresentableMock.swift create mode 100644 novawalletTests/Common/Helpers/MoneyPresentableTests.swift create mode 100644 novawalletTests/Modules/SwapSlippage/SwapSlippageTests.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 5ebc3a5f67..fb38a56d5d 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 06FD6F5999D57B27B29C8738 /* ParaStkStakeConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037D3CE23FFD176F4F7DABC0 /* ParaStkStakeConfirmViewFactory.swift */; }; 0754911527A21957BD25A1DA /* CommonDelegationTracksViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD1EE26234E390E938D9311 /* CommonDelegationTracksViewFactory.swift */; }; 07AB0BC861AA5F134DB9AC26 /* StartStakingInfoProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C92B3D5B314FB3EAE65FA471 /* StartStakingInfoProtocols.swift */; }; + 07D1F0F4FAE24BED8A1CF257 /* SwapSlippagePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0816F2A4A5CC1F111E626188 /* SwapSlippagePresenter.swift */; }; 08999A79B34D287030887A7C /* StartStakingConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2876FA98B3E8F7EBCF5DEED0 /* StartStakingConfirmWireframe.swift */; }; 0909E06D5D06569554F70DD8 /* AssetsSearchInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44EAF50AAF6C23225E06C16C /* AssetsSearchInteractor.swift */; }; 09A6D92CE47636723DFC91F4 /* MessageSheetViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57535268534B154B42ED51CE /* MessageSheetViewFactory.swift */; }; @@ -296,6 +297,7 @@ 13CF38563E1849EAF1B4E4B6 /* ParitySignerAddConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2CF76ABE7BC9A99724D393 /* ParitySignerAddConfirmViewFactory.swift */; }; 13DE59F1804CD6761EBC26B9 /* NPoolsUnstakeConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793356FB65FB73CE7097C6F1 /* NPoolsUnstakeConfirmProtocols.swift */; }; 141BF00B1B59940711773726 /* StakingSelectPoolWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6654DEA68D7ED47AE8E52206 /* StakingSelectPoolWireframe.swift */; }; + 143F6C9044429A337265DF39 /* SwapSlippageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9F3BD600F80ED0426141843 /* SwapSlippageViewController.swift */; }; 148748ACAE23B7D15144015B /* DAppAuthSettingsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F23EDFB699CAEEADC9263A0D /* DAppAuthSettingsViewFactory.swift */; }; 14DD3CD30D9D658961078037 /* ReferendumsFiltersViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025FA616E9CFA2BC7C364B74 /* ReferendumsFiltersViewFactory.swift */; }; 151722B1A7CC5B181A51869D /* StakingMoreOptionsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74E4A8D1E4FEA5936AA38B24 /* StakingMoreOptionsInteractor.swift */; }; @@ -350,6 +352,7 @@ 233CB11F486DE1953D977295 /* WalletsListViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7073BBC153295FF46FD06FB3 /* WalletsListViewLayout.swift */; }; 2368E8BFA569B8D007F6244F /* AssetsSettingsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFC5052A062548D20D232DA /* AssetsSettingsWireframe.swift */; }; 2450083471CD071346371995 /* MessageSheetWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14383723F0B56C91A0B3016E /* MessageSheetWireframe.swift */; }; + 2451E27286A176CDA2DC040D /* SwapSlippageWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088C765E5A0F81B96ADE72D8 /* SwapSlippageWireframe.swift */; }; 247AE9F0FFC35C35B595EEE3 /* NftDetailsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793046EA14E4CAB096803BCD /* NftDetailsWireframe.swift */; }; 25381484F16FB930B8A90CE3 /* SelectValidatorsConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FE5F01FC9364788A91EFA5 /* SelectValidatorsConfirmProtocols.swift */; }; 255D7AEBA45EFA5324D92371 /* DAppListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED66939D92756F608FA11520 /* DAppListPresenter.swift */; }; @@ -701,9 +704,14 @@ 775F194D2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775F194C2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift */; }; 775F19512A5811FA009915B6 /* StartStakingParachainInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775F19502A5811FA009915B6 /* StartStakingParachainInteractor.swift */; }; 775F19532A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775F19522A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift */; }; + 7765BB712ADA66A400451274 /* MoneyPresentableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7765BB702ADA66A400451274 /* MoneyPresentableTests.swift */; }; + 7765BB732ADA744400451274 /* MoneyPresentableMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7765BB722ADA744400451274 /* MoneyPresentableMock.swift */; }; + 7765BB752ADA749400451274 /* AmountInputFormatterPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7765BB742ADA749400451274 /* AmountInputFormatterPlugin.swift */; }; 77740BBC2AD4A7B800E8C06F /* CollapsableContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBB2AD4A7B800E8C06F /* CollapsableContainerView.swift */; }; 77740BBE2AD4A7F500E8C06F /* SwapDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */; }; 77740BC02AD4A80D00E8C06F /* SwapRateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */; }; + 77740BC42AD8145500E8C06F /* SwapSlippageInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BC32AD8145500E8C06F /* SwapSlippageInputView.swift */; }; + 77740BC62AD849D100E8C06F /* Percent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BC52AD849D100E8C06F /* Percent.swift */; }; 77799ADD2A74219A00B7E564 /* ButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799ADC2A74219A00B7E564 /* ButtonState.swift */; }; 77799ADF2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799ADE2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift */; }; 77799AE52A792AE700B7E564 /* StakingTypeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799AE42A792AE700B7E564 /* StakingTypeViewModel.swift */; }; @@ -852,6 +860,7 @@ 800FCAF66DC8A24020D16A9C /* AccountExportPasswordInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194C9BFEE9BA8C9E448D79AA /* AccountExportPasswordInteractor.swift */; }; 80175BD9EE66BCE4016E7F28 /* GovernanceDelegateConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA3C305F49A4E609C7A5C14 /* GovernanceDelegateConfirmViewController.swift */; }; 8027EA456C0C13F6DA73D540 /* MoonbeamTermsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837E9D1F8096A0E9CA0E0CEB /* MoonbeamTermsPresenter.swift */; }; + 80603DA36CD481AE310CDFE1 /* SwapSlippageViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28294C13CF8F62D2FE4D0427 /* SwapSlippageViewFactory.swift */; }; 811096BAAA6BD237DF2769EA /* ReferendumVoteSetupViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF755EE09598254BB5E59CC2 /* ReferendumVoteSetupViewFactory.swift */; }; 81544BD01F6AD0197588D3C5 /* OperationDetailsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC6917929E4A752B79FE554 /* OperationDetailsWireframe.swift */; }; 81ADC94E1CC47A2C6F0F1BEA /* GovernanceEditDelegationTracksProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32F1F0F6F985195CD19EDDB /* GovernanceEditDelegationTracksProtocols.swift */; }; @@ -3362,6 +3371,7 @@ 8CF040889DBCA0E9D40BDC82 /* LedgerDiscoverViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67CAEB35921A61A8EC131AF8 /* LedgerDiscoverViewFactory.swift */; }; 8D9BC9C36DC891CDD900A895 /* AccountConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3104ABC4BECF08B0BA836AA /* AccountConfirmViewController.swift */; }; 8DA9BFE7774B292664FD843F /* DAppPhishingProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A6C6207095F63972E14618 /* DAppPhishingProtocols.swift */; }; + 8DCFB5C717B37AF2F0E22F85 /* SwapSlippageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD97740BCB718BAB9E3CAAC /* SwapSlippageTests.swift */; }; 8DF76D04C127E0048B253343 /* DAppListViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07D08AF2C744DF2073702499 /* DAppListViewFactory.swift */; }; 8E74A13BA73160F88B2B0948 /* AddDelegationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0492106F3E5C20019137E9AA /* AddDelegationViewController.swift */; }; 8EECC23DA32547DAAFC260BE /* ParaStkStakeConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9925D7FC8A58695700B4A308 /* ParaStkStakeConfirmPresenter.swift */; }; @@ -3387,6 +3397,7 @@ 93EB8C73108944E9C576936C /* ReferendumVotersPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9990DF2F0214CD51E5388CE /* ReferendumVotersPresenter.swift */; }; 940DA38E4586A27D7F3E0C67 /* ParitySignerAddressesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3280014154DCC105757E317C /* ParitySignerAddressesViewController.swift */; }; 948FE60822DFC49A0BD5740B /* NominationPoolBondMoreBaseWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4009AE7EF95E9CE1EB88B8 /* NominationPoolBondMoreBaseWireframe.swift */; }; + 94A6747402550FB39D2E2BE7 /* SwapSlippageProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE0E677E64DAC7E93562412 /* SwapSlippageProtocols.swift */; }; 94B0F0C84AF74B3CD7223C3A /* AccountConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7306D50F278F6CC90DC88F27 /* AccountConfirmPresenter.swift */; }; 94B234EE404088B077DB6411 /* DAppListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F3CCA5BA57C68D5AE2B42F /* DAppListProtocols.swift */; }; 9565BEB636E6D386B0C0FBE5 /* StakingPayoutConfirmationViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE0492B98AB9C1540846B39 /* StakingPayoutConfirmationViewFactory.swift */; }; @@ -3416,6 +3427,7 @@ 9B6CD060F0EB77C162D90D3E /* ChainAddressDetailsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781FA4C896AF31B4035AFB38 /* ChainAddressDetailsViewFactory.swift */; }; 9BADFCBF3AF5186094DB8D67 /* DAppTxDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB4A14C99D151B41F61F474 /* DAppTxDetailsInteractor.swift */; }; 9C223E4BF19F7314A9E6F1CA /* NominationPoolSearchViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686A91FF92C89FE8937EF5A /* NominationPoolSearchViewLayout.swift */; }; + 9C8C32AFF22AC1165FA7FDDA /* SwapSlippageInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF43098AE15348910BA4627 /* SwapSlippageInteractor.swift */; }; 9D509AD640B01CAB872E0E71 /* NominationPoolSearchViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15AC3897EC736F5096949BBC /* NominationPoolSearchViewFactory.swift */; }; 9D5926790B055C56FB74B282 /* AccountManagementProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B5072E250B7277F605855B3 /* AccountManagementProtocols.swift */; }; 9DE1757D047A4D1E97913774 /* GovernanceUnlockConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D3FE2CE7F9F2836755DBA63 /* GovernanceUnlockConfirmProtocols.swift */; }; @@ -3962,6 +3974,7 @@ F88D85C73094F6A1FC494D87 /* DAppSearchWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A191B92AD171FDDDD8C30E2 /* DAppSearchWireframe.swift */; }; F8C0CA3DDBCB5E509295F099 /* MarkdownDescriptionViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF9ED27CF12B7DA8B1378CF /* MarkdownDescriptionViewFactory.swift */; }; F92E73C24AB577F37B35649E /* WalletConnectSessionDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14339E7BE9FE045C6A2AB52 /* WalletConnectSessionDetailsInteractor.swift */; }; + F9CEF01779F811AEEED06C43 /* SwapSlippageViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E965356C7C646CB86BBEBB6 /* SwapSlippageViewLayout.swift */; }; F9E6E306BCE32992EA9ABF3E /* NominationPoolBondMoreConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894A6BB613EA991A09976B30 /* NominationPoolBondMoreConfirmProtocols.swift */; }; FA0E6F6A12CA290C7079AC6C /* StakingSetupAmountPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F80E6C0C3AC88D7CDB8204F /* StakingSetupAmountPresenter.swift */; }; FA62AACACA15CB04275DE957 /* ParaStkYourCollatorsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A2FD02269432066884F5AF /* ParaStkYourCollatorsInteractor.swift */; }; @@ -4030,6 +4043,8 @@ 07D08AF2C744DF2073702499 /* DAppListViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppListViewFactory.swift; sourceTree = ""; }; 07D165A41A0F3F0EC1926175 /* StakingMoreOptionsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionsProtocols.swift; sourceTree = ""; }; 080DF5C2C6DEC79D8324F084 /* NPoolsUnstakeConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeConfirmInteractor.swift; sourceTree = ""; }; + 0816F2A4A5CC1F111E626188 /* SwapSlippagePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSlippagePresenter.swift; sourceTree = ""; }; + 088C765E5A0F81B96ADE72D8 /* SwapSlippageWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSlippageWireframe.swift; sourceTree = ""; }; 0912F5E8BA170342D52F7D38 /* TransferConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferConfirmViewController.swift; sourceTree = ""; }; 0988D8EC0768B03BA7C55612 /* ParaStkYieldBoostStartInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStartInteractor.swift; sourceTree = ""; }; 09E4E9A052F9F04A92F158D6 /* GovernanceDelegateSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateSetupWireframe.swift; sourceTree = ""; }; @@ -4362,6 +4377,7 @@ 270B309EC85D8897A4ADD98A /* CustomValidatorListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CustomValidatorListViewController.swift; sourceTree = ""; }; 27A5489E97F846FE3D5931E5 /* ParaStkYieldBoostStopViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStopViewFactory.swift; sourceTree = ""; }; 27D5AF2F7609ADE855308089 /* AccountExportPasswordViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordViewController.swift; sourceTree = ""; }; + 28294C13CF8F62D2FE4D0427 /* SwapSlippageViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSlippageViewFactory.swift; sourceTree = ""; }; 285212895DBAB0098F302DF9 /* ParaStkYieldBoostStopWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStopWireframe.swift; sourceTree = ""; }; 2864B5E1A5AFC6A8C1B4B9E5 /* NPoolsUnstakeSetupViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeSetupViewController.swift; sourceTree = ""; }; 2876FA98B3E8F7EBCF5DEED0 /* StartStakingConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingConfirmWireframe.swift; sourceTree = ""; }; @@ -4393,6 +4409,7 @@ 2AD0A18F25D3D1E100312428 /* GitHubPhishingServiceFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubPhishingServiceFactory.swift; sourceTree = ""; }; 2AD0A19425D3D3EC00312428 /* GitHubOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubOperationFactory.swift; sourceTree = ""; }; 2ADA652B86975A2044ABB065 /* NominationPoolBondMoreConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreConfirmViewController.swift; sourceTree = ""; }; + 2AE0E677E64DAC7E93562412 /* SwapSlippageProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSlippageProtocols.swift; sourceTree = ""; }; 2AF8204E274FD2110092E3E7 /* BaseAccountCreatePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseAccountCreatePresenter.swift; sourceTree = ""; }; 2AFF4B9F274D1E4D00D790B4 /* UsernameSetupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameSetupViewController.swift; sourceTree = ""; }; 2AFF4BA1274D1E5C00D790B4 /* UsernameSetupViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameSetupViewLayout.swift; sourceTree = ""; }; @@ -4402,6 +4419,7 @@ 2E4B0600AFFB96A75CF98755 /* StakingRedeemProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRedeemProtocols.swift; sourceTree = ""; }; 2E5C5EE99A4B73789BE23039 /* Pods-novawalletAll-novawalletIntegrationTests.staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawalletIntegrationTests.staging.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests.staging.xcconfig"; sourceTree = ""; }; 2E800814C025B38C87CC282D /* TokensAddSelectNetworkViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensAddSelectNetworkViewFactory.swift; sourceTree = ""; }; + 2E965356C7C646CB86BBEBB6 /* SwapSlippageViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSlippageViewLayout.swift; sourceTree = ""; }; 2ECD8589BD30A8BE9492AD87 /* StakingRewardPayoutsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardPayoutsPresenter.swift; sourceTree = ""; }; 2F10F130391C4B3652FE8F59 /* ParitySignerTxScanProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxScanProtocols.swift; sourceTree = ""; }; 2F5A8E5D1C5A7F0B5B1570B9 /* NominationPoolBondMoreConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreConfirmViewLayout.swift; sourceTree = ""; }; @@ -4704,9 +4722,14 @@ 775F194C2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoRelaychainPresenter.swift; sourceTree = ""; }; 775F19502A5811FA009915B6 /* StartStakingParachainInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingParachainInteractor.swift; sourceTree = ""; }; 775F19522A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoParachainPresenter.swift; sourceTree = ""; }; + 7765BB702ADA66A400451274 /* MoneyPresentableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyPresentableTests.swift; sourceTree = ""; }; + 7765BB722ADA744400451274 /* MoneyPresentableMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyPresentableMock.swift; sourceTree = ""; }; + 7765BB742ADA749400451274 /* AmountInputFormatterPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountInputFormatterPlugin.swift; sourceTree = ""; }; 77740BBB2AD4A7B800E8C06F /* CollapsableContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsableContainerView.swift; sourceTree = ""; }; 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapDetailsView.swift; sourceTree = ""; }; 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRateView.swift; sourceTree = ""; }; + 77740BC32AD8145500E8C06F /* SwapSlippageInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapSlippageInputView.swift; sourceTree = ""; }; + 77740BC52AD849D100E8C06F /* Percent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Percent.swift; sourceTree = ""; }; 77799ADC2A74219A00B7E564 /* ButtonState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonState.swift; sourceTree = ""; }; 77799ADE2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoRelaychainWireframe.swift; sourceTree = ""; }; 77799AE42A792AE700B7E564 /* StakingTypeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingTypeViewModel.swift; sourceTree = ""; }; @@ -7629,6 +7652,7 @@ AEE5FB1726457AC1002B8FDC /* StakingRewardDestSetupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardDestSetupViewController.swift; sourceTree = ""; }; AEE5FB1926457AE9002B8FDC /* StakingRewardDestSetupProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardDestSetupProtocols.swift; sourceTree = ""; }; AEE5FB1B264A610C002B8FDC /* StakingRewardDestSetupLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardDestSetupLayout.swift; sourceTree = ""; }; + AEF43098AE15348910BA4627 /* SwapSlippageInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSlippageInteractor.swift; sourceTree = ""; }; AEF50585261EE6230098574D /* PurchaseProviderPickerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseProviderPickerTableViewCell.swift; sourceTree = ""; }; AEF5058A261EF27B0098574D /* PurchaseProviderPickerTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PurchaseProviderPickerTableViewCell.xib; sourceTree = ""; }; AEF505A82620249F0098574D /* UIColor+HEX.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+HEX.swift"; sourceTree = ""; }; @@ -7680,6 +7704,7 @@ BA518E1D79D86360F145B428 /* TokensAddSelectNetworkInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensAddSelectNetworkInteractor.swift; sourceTree = ""; }; BA7DAF20C447065DA5467696 /* TransactionHistoryViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransactionHistoryViewLayout.swift; sourceTree = ""; }; BAB2478DE3AF0885A3ED7ED8 /* StakingRedeemPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRedeemPresenter.swift; sourceTree = ""; }; + BAD97740BCB718BAB9E3CAAC /* SwapSlippageTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSlippageTests.swift; sourceTree = ""; }; BAF9ED27CF12B7DA8B1378CF /* MarkdownDescriptionViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MarkdownDescriptionViewFactory.swift; sourceTree = ""; }; BB1A1934B76A5DCC65855EE1 /* TransactionHistoryWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransactionHistoryWireframe.swift; sourceTree = ""; }; BB494F0B16C9588325CF0D84 /* SwapSetupPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupPresenter.swift; sourceTree = ""; }; @@ -8006,6 +8031,7 @@ F829E7F8B39EE7D977001510 /* ControllerAccountProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountProtocols.swift; sourceTree = ""; }; F8C16637ADA10892106DD304 /* GovernanceDelegateInfoWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateInfoWireframe.swift; sourceTree = ""; }; F8D55623AB063B11C1551012 /* Pods-novawalletAll-novawallet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawallet.debug.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawallet/Pods-novawalletAll-novawallet.debug.xcconfig"; sourceTree = ""; }; + F9F3BD600F80ED0426141843 /* SwapSlippageViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSlippageViewController.swift; sourceTree = ""; }; FA3F824117720D3CE65A195F /* LedgerDiscoverPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerDiscoverPresenter.swift; sourceTree = ""; }; FA59CE2C7AE548ACA9D66FD7 /* CrowdloanContributionConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmWireframe.swift; sourceTree = ""; }; FB2EA5D52F51F03FBAB490FE /* ChainAddressDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainAddressDetailsViewController.swift; sourceTree = ""; }; @@ -8210,6 +8236,7 @@ 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */, 0CE629D42AA9B5E200E250BD /* BalanceViewModel.swift */, 0CE629D52AA9B5E200E250BD /* BalanceViewModelFactory.swift */, + 7765BB742ADA749400451274 /* AmountInputFormatterPlugin.swift */, ); path = Amount; sourceTree = ""; @@ -8946,6 +8973,22 @@ path = AccountManagement; sourceTree = ""; }; + 288677D19FEB54E369E6B619 /* Slippage */ = { + isa = PBXGroup; + children = ( + 7752E16B2AD878A4006E2F92 /* Model */, + 2AE0E677E64DAC7E93562412 /* SwapSlippageProtocols.swift */, + 088C765E5A0F81B96ADE72D8 /* SwapSlippageWireframe.swift */, + 0816F2A4A5CC1F111E626188 /* SwapSlippagePresenter.swift */, + AEF43098AE15348910BA4627 /* SwapSlippageInteractor.swift */, + F9F3BD600F80ED0426141843 /* SwapSlippageViewController.swift */, + 2E965356C7C646CB86BBEBB6 /* SwapSlippageViewLayout.swift */, + 28294C13CF8F62D2FE4D0427 /* SwapSlippageViewFactory.swift */, + 77740BC32AD8145500E8C06F /* SwapSlippageInputView.swift */, + ); + path = Slippage; + sourceTree = ""; + }; 29BD7DA0076BA8BC3411221A /* Setup */ = { isa = PBXGroup; children = ( @@ -9198,6 +9241,14 @@ path = Instructions; sourceTree = ""; }; + 3CB2CEC27DA500C00B69D056 /* SwapSlippage */ = { + isa = PBXGroup; + children = ( + BAD97740BCB718BAB9E3CAAC /* SwapSlippageTests.swift */, + ); + path = SwapSlippage; + sourceTree = ""; + }; 3F64CF6065463B01222F1B8F /* ExportMnemonic */ = { isa = PBXGroup; children = ( @@ -9556,6 +9607,14 @@ path = View; sourceTree = ""; }; + 7752E16B2AD878A4006E2F92 /* Model */ = { + isa = PBXGroup; + children = ( + 77740BC52AD849D100E8C06F /* Percent.swift */, + ); + path = Model; + sourceTree = ""; + }; 775692822A24CA5100220756 /* AssetOperation */ = { isa = PBXGroup; children = ( @@ -9721,6 +9780,7 @@ 77C9BCBF2ACD2E0300022EA2 /* Swaps */ = { isa = PBXGroup; children = ( + 288677D19FEB54E369E6B619 /* Slippage */, 29BD7DA0076BA8BC3411221A /* Setup */, ); path = Swaps; @@ -14479,6 +14539,7 @@ 84B7C705289BFA79001A3566 /* AccountManagement */, 84B7C708289BFA79001A3566 /* WalletList */, 84B7C70A289BFA79001A3566 /* ControllerAccount */, + 3CB2CEC27DA500C00B69D056 /* SwapSlippage */, ); path = Modules; sourceTree = ""; @@ -16283,6 +16344,8 @@ 847F2D5627AB08D200AFD476 /* GradientColorFactoryTests.swift */, 842B2FCC2947239B002829B6 /* CoinGeckoUrlParserTests.swift */, 840D627029CB3FD900D5E894 /* URLBuilderTests.swift */, + 7765BB702ADA66A400451274 /* MoneyPresentableTests.swift */, + 7765BB722ADA744400451274 /* MoneyPresentableMock.swift */, ); path = Helpers; sourceTree = ""; @@ -19627,6 +19690,7 @@ 84B018B026E0450F00C75E28 /* ValidatorStateView.swift in Sources */, AEE0C43A272A8B1F009F9AD5 /* AddChainAccount+AccountCreateWireframe.swift in Sources */, 0C7C9B992ABFF355009A0362 /* String+Html.swift in Sources */, + 77740BC62AD849D100E8C06F /* Percent.swift in Sources */, 0C7E7FAB2A9F27FB00596628 /* NominationPoolsRedeemCall.swift in Sources */, 84038FF226FFBE1900C73F3F /* JsonLocalSubscriptionHandler.swift in Sources */, 842898D1265A955A002D5D65 /* ImageViewModel.swift in Sources */, @@ -20184,6 +20248,7 @@ 0C59E8E12AA60FF0001E11F3 /* CrowdloanExternalServiceFactory.swift in Sources */, 847297CF260B4035009B86D0 /* ChangeTargetConfirmInteractor.swift in Sources */, 8455F19C2A1DF088003F072D /* ChainsStore+Multistaking.swift in Sources */, + 7765BB752ADA749400451274 /* AmountInputFormatterPlugin.swift in Sources */, 8428768724AE046300D91AD8 /* LanguageSelectionViewFactory.swift in Sources */, 84FB9E26285C6C5000B42FC0 /* XcmVersionedMultiasset.swift in Sources */, 84953F6E2935E35D0033F47D /* EtherscanHistoryContext.swift in Sources */, @@ -22804,6 +22869,7 @@ 1232A714A96F937330FC0AFA /* GovernanceDelegateConfirmViewFactory.swift in Sources */, 4B1FA597B618713C75917816 /* GovernanceYourDelegationsProtocols.swift in Sources */, 15B079FA97C96327FD4A2E16 /* GovernanceYourDelegationsWireframe.swift in Sources */, + 77740BC42AD8145500E8C06F /* SwapSlippageInputView.swift in Sources */, 75249684C6F3EE4E553DABA1 /* GovernanceYourDelegationsPresenter.swift in Sources */, EB20C6B406155664B981BA94 /* GovernanceYourDelegationsInteractor.swift in Sources */, 59A0AF440ABAAA459EF7D993 /* GovernanceYourDelegationsViewController.swift in Sources */, @@ -23015,6 +23081,13 @@ 8786222ADF4643BE7A6FBBEB /* SwapSetupViewController.swift in Sources */, 350B8A18C9C91DF07D2E53C5 /* SwapSetupViewLayout.swift in Sources */, 66531C7E2E0E99C89A89A35A /* SwapSetupViewFactory.swift in Sources */, + 94A6747402550FB39D2E2BE7 /* SwapSlippageProtocols.swift in Sources */, + 2451E27286A176CDA2DC040D /* SwapSlippageWireframe.swift in Sources */, + 07D1F0F4FAE24BED8A1CF257 /* SwapSlippagePresenter.swift in Sources */, + 9C8C32AFF22AC1165FA7FDDA /* SwapSlippageInteractor.swift in Sources */, + 143F6C9044429A337265DF39 /* SwapSlippageViewController.swift in Sources */, + F9CEF01779F811AEEED06C43 /* SwapSlippageViewLayout.swift in Sources */, + 80603DA36CD481AE310CDFE1 /* SwapSlippageViewFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -23034,6 +23107,7 @@ 842D1E8E24D2091A00C30A7A /* CommonMocks.swift in Sources */, 8467FD0824E5E0BD005D486C /* KeystoreDefinition+Test.swift in Sources */, 84452F5D25D5CB3B00F47EC5 /* FileManagerTests.swift in Sources */, + 7765BB732ADA744400451274 /* MoneyPresentableMock.swift in Sources */, 84B7C72D289BFA79001A3566 /* ValidatorSearchTests.swift in Sources */, 841B45292603D91800C08693 /* EraValidatorsServiceStub.swift in Sources */, 84F47D652667804100F7647A /* CrowdloanBonusServiceStub.swift in Sources */, @@ -23066,6 +23140,7 @@ 840D627129CB3FD900D5E894 /* URLBuilderTests.swift in Sources */, 84D9C41126CD361C004AB2AB /* SpecVersionSubscriptionTests.swift in Sources */, 84B66A1626FDF7D70038B963 /* JsonSubscriptionFactoryStub.swift in Sources */, + 7765BB712ADA66A400451274 /* MoneyPresentableTests.swift in Sources */, 8857E02D28A4F6C000260BA2 /* CurrencyManagerStub.swift in Sources */, 8467FD3B24EAD236005D486C /* SigningWrapperTests.swift in Sources */, 84B7C71D289BFA79001A3566 /* MnemonicTextNormalizerTest.swift in Sources */, @@ -23175,6 +23250,7 @@ 84B7C748289BFA79001A3566 /* WalletListTests.swift in Sources */, 84B7C720289BFA79001A3566 /* ReferralCrowdloanTests.swift in Sources */, F4897BB126AED13D0075F291 /* EraCountdownOperationFactoryStub.swift in Sources */, + 8DCFB5C717B37AF2F0E22F85 /* SwapSlippageTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/novawallet/Common/ViewModel/Amount/AmountInputFormatterPlugin.swift b/novawallet/Common/ViewModel/Amount/AmountInputFormatterPlugin.swift new file mode 100644 index 0000000000..c83da24ae4 --- /dev/null +++ b/novawallet/Common/ViewModel/Amount/AmountInputFormatterPlugin.swift @@ -0,0 +1,41 @@ +import Foundation + +protocol AmountInputFormatterPluginProtocol { + func preProccessAmount(_ amount: String) -> String + func postProccesAmount(_ amount: String) -> String + func processAvailableCharacters(_ characterSet: CharacterSet) -> CharacterSet + func currentOffset(in amount: String) -> Int? +} + +struct AddSymbolAmountInputFormatterPlugin: AmountInputFormatterPluginProtocol { + var symbol = "%" + var separator = " " + + func preProccessAmount(_ amount: String) -> String { + guard amount.hasSuffix(symbol) else { + return amount + } + + let count = amount.hasSuffix(separator + symbol) ? symbol.count + separator.count : symbol.count + + let offset = amount.count - count + let index = amount.index(amount.startIndex, offsetBy: offset) + return String(amount.prefix(upTo: index)) + } + + func postProccesAmount(_ amount: String) -> String { + [amount, symbol].joined(separator: separator) + } + + func processAvailableCharacters(_ characterSet: CharacterSet) -> CharacterSet { + characterSet.union(CharacterSet(charactersIn: symbol).inverted) + } + + func currentOffset(in amount: String) -> Int? { + guard amount.hasSuffix(symbol) else { + return nil + } + let count = amount.hasSuffix(separator + symbol) ? symbol.count + separator.count : symbol.count + return amount.count - count + } +} diff --git a/novawallet/Common/ViewModel/Amount/AmountInputViewModel.swift b/novawallet/Common/ViewModel/Amount/AmountInputViewModel.swift index 9ec5d7dc16..f6fa2cb0e2 100644 --- a/novawallet/Common/ViewModel/Amount/AmountInputViewModel.swift +++ b/novawallet/Common/ViewModel/Amount/AmountInputViewModel.swift @@ -13,6 +13,7 @@ protocol AmountInputViewModelProtocol: AnyObject { func didReceiveReplacement(_ string: String, for range: NSRange) -> Bool func didUpdateAmount(to newAmount: Decimal) + var currentOffset: Int? { get } } extension AmountInputViewModelProtocol { @@ -62,19 +63,23 @@ final class AmountInputViewModel: AmountInputViewModelProtocol, MoneyPresentable public var observable: WalletViewModelObserverContainer + let plugin: AmountInputFormatterPluginProtocol? + public init( symbol: String, amount: Decimal?, limit: Decimal, formatter: NumberFormatter, inputLocale: Locale = Locale.current, - precision: Int16 = 2 + precision: Int16 = 2, + plugin: AmountInputFormatterPluginProtocol? = nil ) { self.symbol = symbol self.limit = limit self.formatter = formatter self.inputLocale = inputLocale self.precision = precision + self.plugin = plugin observable = WalletViewModelObserverContainer() @@ -118,4 +123,8 @@ final class AmountInputViewModel: AmountInputViewModelProtocol, MoneyPresentable amount = inputAmount } + + var currentOffset: Int? { + plugin?.currentOffset(in: amount) + } } diff --git a/novawallet/Common/ViewModel/Amount/MoneyPresentable.swift b/novawallet/Common/ViewModel/Amount/MoneyPresentable.swift index f19c281a09..72ae87d531 100644 --- a/novawallet/Common/ViewModel/Amount/MoneyPresentable.swift +++ b/novawallet/Common/ViewModel/Amount/MoneyPresentable.swift @@ -4,6 +4,7 @@ protocol MoneyPresentable { var formatter: NumberFormatter { get } var amount: String { get } var precision: Int16 { get } + var plugin: AmountInputFormatterPluginProtocol? { get } func transform(input: String, from locale: Locale) -> String } @@ -18,17 +19,19 @@ extension MoneyPresentable { return "" } - guard let decimalAmount = Decimal(string: amount, locale: formatter.locale) else { + let preprocessedAmount = plugin?.preProccessAmount(amount) ?? amount + + guard let decimalAmount = Decimal(string: preprocessedAmount, locale: formatter.locale) else { return nil } var amountFormatted = formatter.string(from: decimalAmount as NSDecimalNumber) let separator = decimalSeparator() - if amount.hasSuffix(separator) { + if preprocessedAmount.hasSuffix(separator) { amountFormatted?.append(separator) } else { - let amountParts = amount.components(separatedBy: separator) + let amountParts = preprocessedAmount.components(separatedBy: separator) let formattedParts = amountFormatted?.components(separatedBy: separator) if amountParts.count == 2, formattedParts?.count == 1 { @@ -51,7 +54,11 @@ extension MoneyPresentable { } } - return amountFormatted + guard let plugin = plugin, let amountFormatted = amountFormatted else { + return amountFormatted + } + + return plugin.postProccesAmount(amountFormatted) } private func decimalSeparator() -> String { @@ -63,12 +70,14 @@ extension MoneyPresentable { } private func notEligibleSet() -> CharacterSet { - CharacterSet.decimalDigits + let availableSet = CharacterSet.decimalDigits .union(CharacterSet(charactersIn: "\(decimalSeparator())\(groupingSeparator())")).inverted + return plugin?.processAvailableCharacters(availableSet) ?? availableSet } private func isValid(amount: String) -> Bool { - let components = amount.components(separatedBy: decimalSeparator()) + let preprocessedAmount = plugin?.preProccessAmount(amount) ?? amount + let components = preprocessedAmount.components(separatedBy: decimalSeparator()) return !((precision == 0 && components.count > 1) || components.count > 2 || @@ -80,7 +89,8 @@ extension MoneyPresentable { return self.amount } - var newAmount = (self.amount + amount).replacingOccurrences( + let preprocessedAmount = plugin?.preProccessAmount(self.amount) ?? self.amount + var newAmount = (preprocessedAmount + amount).replacingOccurrences( of: groupingSeparator(), with: "" ) @@ -89,15 +99,19 @@ extension MoneyPresentable { newAmount = "\(MoneyPresentableConstants.singleZero)\(newAmount)" } - return isValid(amount: newAmount) ? newAmount : self.amount + let postprocessedAmount = plugin?.postProccesAmount(newAmount) ?? newAmount + + return isValid(amount: postprocessedAmount) ? postprocessedAmount : self.amount } func set(_ amount: String) -> String { - guard amount.rangeOfCharacter(from: notEligibleSet()) == nil else { + let preprocessedAmount = plugin?.preProccessAmount(amount) ?? amount + + guard preprocessedAmount.rangeOfCharacter(from: notEligibleSet()) == nil else { return self.amount } - var settingAmount = amount.replacingOccurrences( + var settingAmount = preprocessedAmount.replacingOccurrences( of: groupingSeparator(), with: "" ) @@ -106,7 +120,9 @@ extension MoneyPresentable { settingAmount = "\(MoneyPresentableConstants.singleZero)\(settingAmount)" } - return isValid(amount: settingAmount) ? settingAmount : self.amount + let postprocessedAmount = plugin?.postProccesAmount(settingAmount) ?? settingAmount + + return isValid(amount: postprocessedAmount) ? postprocessedAmount : self.amount } func transform(input: String, from locale: Locale) -> String { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index c9ae6a4e23..4c3569d62d 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -23,6 +23,8 @@ final class SwapSetupPresenter { } } + private var slippage: (value: BigRational, direction: AssetConversion.Direction)? + private var feeIdentifier: String? private var accountId: AccountId? @@ -205,19 +207,20 @@ final class SwapSetupPresenter { } private func estimateFee() { - guard let quote = quote, let accountId = accountId else { + guard let quote = quote, + let accountId = accountId, + let slippage = slippage else { return } - // TODO: Provide slippage and direction let args = AssetConversion.CallArgs( assetIn: quote.assetIn, amountIn: quote.amountIn, assetOut: quote.assetOut, amountOut: quote.amountOut, receiver: accountId, - direction: .sell, - slippage: .percent(of: 1) + direction: slippage.direction, + slippage: slippage.value ) guard args.identifier != feeIdentifier else { @@ -284,6 +287,8 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideReceiveAssetViews() provideDetailsViewModel(isAvailable: false) provideButtonState() + // TODO: get from settings + slippage = (value: .percent(of: 1), direction: .sell) interactor.setup() } @@ -338,6 +343,18 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { // TODO: navigate to confirm screen func proceed() {} + + func showSettings() { + wireframe.showSettings( + from: view + ) { [weak self, payChainAsset] slippageValue in + guard payChainAsset == self?.payChainAsset else { + return + } + self?.slippage = (value: slippageValue, direction: .sell) + self?.estimateFee() + } + } } extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 1ec8bf174c..8627596dd6 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -26,6 +26,7 @@ protocol SwapSetupPresenterProtocol: AnyObject { func showFeeActions() func showFeeInfo() func showRateInfo() + func showSettings() } protocol SwapSetupInteractorInputProtocol: AnyObject { @@ -55,6 +56,10 @@ protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryabl chainAsset: ChainAsset?, completionHandler: @escaping (ChainAsset) -> Void ) + func showSettings( + from view: ControllerBackedProtocol?, + completionHandler: @escaping (BigRational) -> Void + ) } enum SwapSetupError: Error { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 186b96314b..bf44eb0c53 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -29,6 +29,7 @@ final class SwapSetupViewController: UIViewController, ViewHolder { setupHandlers() setupLocalization() + setupNavigationItem() presenter.setup() } @@ -97,6 +98,16 @@ final class SwapSetupViewController: UIViewController, ViewHolder { rootView.receiveAmountInputView.textInputView.textField.inputAccessoryView = accessoryView } + private func setupNavigationItem() { + navigationItem.rightBarButtonItem = UIBarButtonItem( + image: R.image.iconAssetsSettings()?.withRenderingMode(.alwaysTemplate), + style: .plain, + target: self, + action: #selector(settingsAction) + ) + navigationItem.rightBarButtonItem?.tintColor = R.color.colorIconPrimary() + } + @objc private func selectPayTokenAction() { rootView.receiveAmountInputView.endEditing(true) presenter.selectPayToken() @@ -140,6 +151,10 @@ final class SwapSetupViewController: UIViewController, ViewHolder { @objc private func doneAction() { view.endEditing(true) } + + @objc private func settingsAction() { + presenter.showSettings() + } } extension SwapSetupViewController: SwapSetupViewProtocol { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index b80c56af15..b4bc6584cf 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -46,4 +46,20 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { view?.controller.present(navigationController, animated: true, completion: nil) } + + func showSettings( + from view: ControllerBackedProtocol?, + completionHandler: @escaping (BigRational) -> Void + ) { + guard let settingsView = SwapSlippageViewFactory.createView( + completionHandler: completionHandler + ) else { + return + } + + view?.controller.navigationController?.pushViewController( + settingsView.controller, + animated: true + ) + } } diff --git a/novawallet/Modules/Swaps/Slippage/Model/Percent.swift b/novawallet/Modules/Swaps/Slippage/Model/Percent.swift new file mode 100644 index 0000000000..b22ac5c347 --- /dev/null +++ b/novawallet/Modules/Swaps/Slippage/Model/Percent.swift @@ -0,0 +1,6 @@ +import Foundation + +struct Percent { + let value: Decimal + let title: String +} diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift new file mode 100644 index 0000000000..4945e47671 --- /dev/null +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift @@ -0,0 +1,172 @@ +import SnapKit +import SoraUI + +protocol SwapSlippageInputViewDelegateProtocol: AnyObject { + func didSelect(percent: Percent, sender: Any?) +} + +final class SwapSlippageInputView: BackgroundedContentControl { + let textField: UITextField = .create { + $0.font = .title2 + $0.textColor = R.color.colorTextPrimary() + $0.tintColor = R.color.colorTextPrimary() + $0.textAlignment = .left + + $0.attributedPlaceholder = NSAttributedString( + string: "0.5 %", + attributes: [ + .foregroundColor: R.color.colorHintText()!, + .font: UIFont.title2 + ] + ) + + $0.keyboardType = .decimalPad + $0.clearButtonMode = .whileEditing + } + + var roundedBackgroundView: RoundedView? { + backgroundView as? RoundedView + } + + var buttonsStack = UIView.hStack( + alignment: .center, + distribution: .equalSpacing, + spacing: 8, + [] + ) + + weak var delegate: SwapSlippageInputViewDelegateProtocol? + private(set) var inputViewModel: AmountInputViewModelProtocol? + + override init(frame: CGRect) { + super.init(frame: frame) + setupLayout() + configureBackgroundViewIfNeeded() + setupTextFieldHandlers() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + addSubview(textField) + textField.snp.makeConstraints { + $0.leading.trailing.equalToSuperview().inset(12) + $0.top.bottom.equalToSuperview().inset(14) + } + addSubview(buttonsStack) + buttonsStack.snp.makeConstraints { + $0.trailing.equalToSuperview().inset(12) + $0.top.bottom.equalToSuperview().inset(8) + $0.width.lessThanOrEqualToSuperview().multipliedBy(0.7) + } + } + + private func configureBackgroundViewIfNeeded() { + if backgroundView == nil { + let roundedView = RoundedView() + roundedView.apply(style: .strokeOnEditing) + roundedView.isUserInteractionEnabled = false + backgroundView = roundedView + } + } + + private func setupTextFieldHandlers() { + textField.addTarget(self, action: #selector(editingDidChangeAction), for: .editingChanged) + textField.addTarget(self, action: #selector(editingDidBeginAction), for: .editingDidBegin) + textField.addTarget(self, action: #selector(editingDidEndAction), for: .editingDidEnd) + textField.addTarget(self, action: #selector(editingDidEndAction), for: .editingDidEndOnExit) + textField.delegate = self + } + + @objc private func editingDidBeginAction() { + buttonsStack.isHidden = true + roundedBackgroundView?.strokeWidth = textField.isFirstResponder ? 0.5 : 0.0 + } + + @objc private func editingDidChangeAction() { + buttonsStack.isHidden = true + } + + @objc private func editingDidEndAction() { + if textField.text.isNilOrEmpty { + buttonsStack.isHidden = false + } + roundedBackgroundView?.strokeWidth = textField.isFirstResponder ? 0.5 : 0.0 + } + + @objc private func buttonAction(_ sender: RoundedButton) { + guard let delegate = delegate else { + return + } + if let index = buttonsStack.arrangedSubviews.index(where: { $0 === sender }), + let buttonModel = viewModel[safe: index] { + delegate.didSelect(percent: buttonModel, sender: self) + } + } + + private var viewModel: [Percent] = [] + + private func createButton(title: String) -> RoundedButton { + let button = RoundedButton() + button.applyAccessoryStyle() + button.contentInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) + button.imageWithTitleView?.title = title + button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside) + return button + } +} + +extension SwapSlippageInputView: UITextFieldDelegate { + func textField( + _: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + inputViewModel?.didReceiveReplacement(string, for: range) ?? false + } + + func textFieldDidBeginEditing(_ textField: UITextField) { + if let offset = inputViewModel?.currentOffset, + let position = textField.position(from: textField.beginningOfDocument, offset: offset) { + textField.selectedTextRange = textField.textRange( + from: textField.beginningOfDocument, + to: position + ) + } + } +} + +extension SwapSlippageInputView: AmountInputViewModelObserver { + func amountInputDidChange() { + textField.text = inputViewModel?.displayAmount + + if textField.isEditing { + sendActions(for: .editingChanged) + } + } +} + +extension SwapSlippageInputView { + func bind(viewModel: [Percent]) { + buttonsStack.arrangedSubviews.forEach { + $0.removeFromSuperview() + } + viewModel.forEach { + buttonsStack.addArrangedSubview( + createButton(title: $0.title) + ) + } + self.viewModel = viewModel + } + + func bind(inputViewModel: AmountInputViewModelProtocol) { + self.inputViewModel?.observable.remove(observer: self) + inputViewModel.observable.add(observer: self) + + self.inputViewModel = inputViewModel + textField.text = inputViewModel.displayAmount + } +} diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageInteractor.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageInteractor.swift new file mode 100644 index 0000000000..8d62554d33 --- /dev/null +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageInteractor.swift @@ -0,0 +1,7 @@ +import UIKit + +final class SwapSlippageInteractor { + weak var presenter: SwapSlippageInteractorOutputProtocol? +} + +extension SwapSlippageInteractor: SwapSlippageInteractorInputProtocol {} diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift new file mode 100644 index 0000000000..fea3f3f01b --- /dev/null +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -0,0 +1,100 @@ +import Foundation +import SoraFoundation +import BigInt + +final class SwapSlippagePresenter { + weak var view: SwapSlippageViewProtocol? + let wireframe: SwapSlippageWireframeProtocol + let interactor: SwapSlippageInteractorInputProtocol + let numberFormatterLocalizable: LocalizableResource + let percentFormatterLocalizable: LocalizableResource + let completionHandler: (BigRational) -> Void + let prefilledPercents: [Decimal] = [0.1, 1, 3] + + private var percentFormatter: NumberFormatter + private var numberFormatter: NumberFormatter + private var amountInput: Decimal? + + init( + interactor: SwapSlippageInteractorInputProtocol, + wireframe: SwapSlippageWireframeProtocol, + numberFormatterLocalizable: LocalizableResource, + percentFormatterLocalizable: LocalizableResource, + localizationManager: LocalizationManagerProtocol, + completionHandler: @escaping (BigRational) -> Void + ) { + self.interactor = interactor + self.wireframe = wireframe + self.numberFormatterLocalizable = numberFormatterLocalizable + self.percentFormatterLocalizable = percentFormatterLocalizable + self.completionHandler = completionHandler + percentFormatter = percentFormatterLocalizable.value(for: localizationManager.selectedLocale) + numberFormatter = numberFormatterLocalizable.value(for: localizationManager.selectedLocale) + self.localizationManager = localizationManager + } + + private func title(for percent: Decimal) -> String { + percentFormatter.stringFromDecimal(percent) ?? "" + } + + func provideAmountViewModel() { + let inputViewModel = AmountInputViewModel( + symbol: "", + amount: amountInput, + limit: 50, + formatter: numberFormatter, + inputLocale: selectedLocale, + precision: 1, + plugin: AddSymbolAmountInputFormatterPlugin() + ) + + view?.didReceiveInput(viewModel: inputViewModel) + } + + func fraction(from number: Decimal) -> BigRational { + let decimalNumber = NSDecimalNumber(decimal: number) + let scale = -number.exponent + let numerator = decimalNumber.multiplying(byPowerOf10: Int16(scale)).intValue + let denominator = Int(truncating: pow(10, scale) as NSNumber) + return .init(numerator: BigUInt(numerator), denominator: BigUInt(denominator)) + } +} + +extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { + func setup() { + let viewModel = prefilledPercents.map { + Percent( + value: $0, + title: title(for: $0 / (percentFormatter.multiplier?.decimalValue ?? 1)) + ) + } + view?.didReceivePreFilledPercents(viewModel: viewModel) + provideAmountViewModel() + } + + func select(percent: Percent) { + amountInput = percent.value + provideAmountViewModel() + } + + func updateAmount(_ amount: Decimal?) { + amountInput = amount + } + + func apply() { + if let amountInput = amountInput { + let rational = fraction(from: amountInput) + completionHandler(rational) + } + wireframe.close(from: view) + } +} + +extension SwapSlippagePresenter: SwapSlippageInteractorOutputProtocol {} + +extension SwapSlippagePresenter: Localizable { + func applyLocalization() { + percentFormatter = percentFormatterLocalizable.value(for: selectedLocale) + numberFormatter = numberFormatterLocalizable.value(for: selectedLocale) + } +} diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift new file mode 100644 index 0000000000..5469da77c0 --- /dev/null +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift @@ -0,0 +1,21 @@ +import Foundation + +protocol SwapSlippageViewProtocol: ControllerBackedProtocol { + func didReceivePreFilledPercents(viewModel: [Percent]) + func didReceiveInput(viewModel: AmountInputViewModelProtocol) +} + +protocol SwapSlippagePresenterProtocol: AnyObject { + func setup() + func select(percent: Percent) + func updateAmount(_ amount: Decimal?) + func apply() +} + +protocol SwapSlippageInteractorInputProtocol: AnyObject {} + +protocol SwapSlippageInteractorOutputProtocol: AnyObject {} + +protocol SwapSlippageWireframeProtocol: AnyObject { + func close(from view: ControllerBackedProtocol?) +} diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift new file mode 100644 index 0000000000..e20d7ce157 --- /dev/null +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift @@ -0,0 +1,100 @@ +import UIKit +import SoraFoundation + +final class SwapSlippageViewController: UIViewController, ViewHolder { + typealias RootViewType = SwapSlippageViewLayout + + let presenter: SwapSlippagePresenterProtocol + + init( + presenter: SwapSlippagePresenterProtocol, + localizationManager: LocalizationManagerProtocol + ) { + self.presenter = presenter + super.init(nibName: nil, bundle: nil) + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = SwapSlippageViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupLocalization() + setupHandlers() + setupAccessoryView() + presenter.setup() + } + + private func setupLocalization() { + title = "Swap settings" + rootView.slippageButton.imageWithTitleView?.title = "Slippage" + rootView.actionButton.imageWithTitleView?.title = "Apply" + } + + private func setupHandlers() { + rootView.amountInput.delegate = self + rootView.actionButton.addTarget(self, action: #selector(applyButtonAction), for: .touchUpInside) + rootView.amountInput.textField.addTarget(self, action: #selector(inputEditingAction), for: .editingChanged) + } + + private func setupAccessoryView() { + let accessoryView = + UIFactory.default.createDoneAccessoryView( + target: self, + selector: #selector(doneButtonAction), + locale: selectedLocale + ) + rootView.amountInput.textField.inputAccessoryView = accessoryView + } + + private func updateActionButton() { + rootView.actionButton.isEnabled = rootView.amountInput.inputViewModel?.isValid == true + } + + @objc private func applyButtonAction() { + presenter.apply() + } + + @objc private func doneButtonAction() { + rootView.amountInput.endEditing(true) + } + + @objc private func inputEditingAction() { + let amount = rootView.amountInput.inputViewModel?.decimalAmount + presenter.updateAmount(amount) + updateActionButton() + } +} + +extension SwapSlippageViewController: SwapSlippageViewProtocol { + func didReceivePreFilledPercents(viewModel: [Percent]) { + rootView.amountInput.bind(viewModel: viewModel) + } + + func didReceiveInput(viewModel: AmountInputViewModelProtocol) { + rootView.amountInput.bind(inputViewModel: viewModel) + updateActionButton() + } +} + +extension SwapSlippageViewController: SwapSlippageInputViewDelegateProtocol { + func didSelect(percent: Percent, sender _: Any?) { + presenter.select(percent: percent) + } +} + +extension SwapSlippageViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift new file mode 100644 index 0000000000..9cbc16b50b --- /dev/null +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift @@ -0,0 +1,33 @@ +import Foundation +import SoraFoundation + +struct SwapSlippageViewFactory { + static func createView( + completionHandler: @escaping (BigRational) -> Void + ) -> SwapSlippageViewProtocol? { + let interactor = SwapSlippageInteractor() + let wireframe = SwapSlippageWireframe() + + let amountFormatter = NumberFormatter.amount + let percentFormatter = NumberFormatter.percent + + let presenter = SwapSlippagePresenter( + interactor: interactor, + wireframe: wireframe, + numberFormatterLocalizable: amountFormatter.localizableResource(), + percentFormatterLocalizable: percentFormatter.localizableResource(), + localizationManager: LocalizationManager.shared, + completionHandler: completionHandler + ) + + let view = SwapSlippageViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + interactor.presenter = presenter + + return view + } +} diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift new file mode 100644 index 0000000000..bbc3310838 --- /dev/null +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift @@ -0,0 +1,43 @@ +import UIKit +import SoraUI + +final class SwapSlippageViewLayout: ScrollableContainerLayoutView { + let slippageButton: RoundedButton = .create { + $0.applyIconStyle() + $0.imageWithTitleView?.iconImage = R.image.iconInfoFilled()?.tinted( + with: R.color.colorIconSecondary()! + ) + $0.imageWithTitleView?.titleColor = R.color.colorTextPrimary() + $0.imageWithTitleView?.titleFont = .regularFootnote + $0.imageWithTitleView?.spacingBetweenLabelAndIcon = 4 + $0.imageWithTitleView?.layoutType = .horizontalLabelFirst + } + + let amountInput = SwapSlippageInputView() + + let actionButton: TriangularedButton = .create { + $0.applyDefaultStyle() + } + + override func setupLayout() { + super.setupLayout() + let title = UIView.hStack([ + slippageButton, + FlexibleSpaceView() + ]) + addArrangedSubview(title, spacingAfter: 12) + slippageButton.setContentHuggingPriority(.low, for: .horizontal) + addArrangedSubview(amountInput) + + amountInput.snp.makeConstraints { + $0.height.equalTo(48) + } + + addSubview(actionButton) + actionButton.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) + make.bottom.equalTo(safeAreaLayoutGuide).inset(UIConstants.actionBottomInset) + make.height.equalTo(UIConstants.actionHeight) + } + } +} diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageWireframe.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageWireframe.swift new file mode 100644 index 0000000000..b4d1598267 --- /dev/null +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageWireframe.swift @@ -0,0 +1,7 @@ +import Foundation + +final class SwapSlippageWireframe: SwapSlippageWireframeProtocol { + func close(from view: ControllerBackedProtocol?) { + view?.controller.navigationController?.popViewController(animated: true) + } +} diff --git a/novawalletTests/Common/Helpers/MoneyPresentableMock.swift b/novawalletTests/Common/Helpers/MoneyPresentableMock.swift new file mode 100644 index 0000000000..3850d12166 --- /dev/null +++ b/novawalletTests/Common/Helpers/MoneyPresentableMock.swift @@ -0,0 +1,10 @@ +import Foundation +@testable import novawallet + +final class MoneyPresentableMock: MoneyPresentable { + var formatter: NumberFormatter { NumberFormatter.amount } + var amount: String = "" + var precision: Int16 = 2 + let plugin: AmountInputFormatterPluginProtocol? = AddSymbolAmountInputFormatterPlugin() +} + diff --git a/novawalletTests/Common/Helpers/MoneyPresentableTests.swift b/novawalletTests/Common/Helpers/MoneyPresentableTests.swift new file mode 100644 index 0000000000..aa4c5a2ca8 --- /dev/null +++ b/novawalletTests/Common/Helpers/MoneyPresentableTests.swift @@ -0,0 +1,57 @@ +import XCTest +@testable import novawallet + +final class MoneyPresentableTests: XCTestCase { + let moneyPresentable = MoneyPresentableMock() + + override func setUpWithError() throws { + moneyPresentable.amount = "" + } + + func testWhenAddNumberToEmptyInput_ThanResultCorrectPercent() { + let addingSymbol = "3" + let expectation = "3 %" + + let result = moneyPresentable.add(addingSymbol) + XCTAssertEqual(result, expectation) + } + + func testWhenAddNumberToNonEmptyInput_ThanResultCorrectPercent() { + let text = "1 %" + let addingSymbol = "2" + let expectation = "12 %" + moneyPresentable.amount = text + let result = moneyPresentable.add(addingSymbol) + + XCTAssertEqual(result, expectation) + } + + func testWhenSetNumberWithPercent_ThanResultCorrectPercent() { + let setAmount = "5%" + let expectation = "5 %" + + let result = moneyPresentable.set(setAmount) + + XCTAssertEqual(result, expectation) + } + + func testWhenSetNumber_ThanResultCorrectPercent() { + let setAmount = "2.5" + let expectation = "2.5 %" + + let result = moneyPresentable.set(setAmount) + + XCTAssertEqual(result, expectation) + } + + func testWhenSetInvalidNumber_ThanResultNotChanged() { + let setAmount = "0.1 %%" + let expectation = "1 %" + moneyPresentable.amount = "1 %" + + let result = moneyPresentable.set(setAmount) + + XCTAssertEqual(result, expectation) + } + +} diff --git a/novawalletTests/Modules/SwapSlippage/SwapSlippageTests.swift b/novawalletTests/Modules/SwapSlippage/SwapSlippageTests.swift new file mode 100644 index 0000000000..ab96eccc16 --- /dev/null +++ b/novawalletTests/Modules/SwapSlippage/SwapSlippageTests.swift @@ -0,0 +1,16 @@ +import XCTest + +class SwapSlippageTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + XCTFail("Did you forget to add tests?") + } +} From 7161aa89b793a254e0e311deef314fe302ea625c Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 16 Oct 2023 10:09:00 +0200 Subject: [PATCH 036/204] fix direction --- .../Swaps/Setup/SwapSetupPresenter.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index c9ae6a4e23..2e1871c4c2 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -205,18 +205,18 @@ final class SwapSetupPresenter { } private func estimateFee() { - guard let quote = quote, let accountId = accountId else { + guard let quote = quote, let quoteArgs = quoteArgs, let accountId = accountId else { return } - // TODO: Provide slippage and direction + // TODO: Provide slippage let args = AssetConversion.CallArgs( assetIn: quote.assetIn, amountIn: quote.amountIn, assetOut: quote.assetOut, amountOut: quote.amountOut, receiver: accountId, - direction: .sell, + direction: quoteArgs.direction, slippage: .percent(of: 1) ) @@ -238,7 +238,11 @@ final class SwapSetupPresenter { quote = nil switch direction { case .buy: - if let receiveInPlank = receiveAmountInput?.toSubstrateAmount(precision: Int16(receiveChainAsset.asset.precision)), receiveInPlank > 0 { + if + let receiveInPlank = receiveAmountInput?.toSubstrateAmount( + precision: receiveChainAsset.assetDisplayInfo.assetPrecision + ), + receiveInPlank > 0 { let quoteArgs = AssetConversion.QuoteArgs( assetIn: payChainAsset.chainAssetId, assetOut: receiveChainAsset.chainAssetId, @@ -269,8 +273,6 @@ final class SwapSetupPresenter { receiveAmountInput = nil provideReceiveAmountInputViewModel() - default: - break } provideRateViewModel() @@ -357,7 +359,7 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in self?.estimateFee() } - case let .price(_, priceId): + case .price: wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in self?.estimateFee() } @@ -385,12 +387,10 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { receiveAmountInput = receiveChainAsset.map { Decimal.fromSubstrateAmount( quote.amountOut, - precision: Int16($0.asset.precision) + precision: $0.asset.displayInfo.assetPrecision ) ?? 0 } provideReceiveAmountInputViewModel() - default: - break } provideRateViewModel() From 6acd426a3a1ce7bcd54eb194a52f2b5ffc1f4bb5 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 16 Oct 2023 10:22:04 +0200 Subject: [PATCH 037/204] refactor swap logic --- Podfile.lock | 2 +- .../iconPencilEdit.imageset/Contents.json | 12 ++++++++++++ .../iconPencilEdit.imageset/iconPencilEdit.pdf | Bin 0 -> 1693 bytes .../Modules/Swaps/Setup/SwapSetupPresenter.swift | 4 ++-- .../Swaps/Setup/View/SwapNetworkFeeView.swift | 5 +++-- 5 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 novawallet/Assets.xcassets/iconPencilEdit.imageset/Contents.json create mode 100644 novawallet/Assets.xcassets/iconPencilEdit.imageset/iconPencilEdit.pdf diff --git a/Podfile.lock b/Podfile.lock index 7581bee044..9500b91c99 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -305,4 +305,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f37e3724d47617fb7ce7ed5e0a583491617b5899 -COCOAPODS: 1.13.0 +COCOAPODS: 1.12.1 diff --git a/novawallet/Assets.xcassets/iconPencilEdit.imageset/Contents.json b/novawallet/Assets.xcassets/iconPencilEdit.imageset/Contents.json new file mode 100644 index 0000000000..3d542df31b --- /dev/null +++ b/novawallet/Assets.xcassets/iconPencilEdit.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "iconPencilEdit.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconPencilEdit.imageset/iconPencilEdit.pdf b/novawallet/Assets.xcassets/iconPencilEdit.imageset/iconPencilEdit.pdf new file mode 100644 index 0000000000000000000000000000000000000000..54973c3f861b59fd917305d3698e45c033f3ab94 GIT binary patch literal 1693 zcmZWqO>fgc5WVlO*h{5ysI#+Qvny4Vpe+>wL`k_-9E@w)pwx!gpu(@`jT75RYemZV z*)#Lz&FuK-`r_>g6|59#quXykl-8%GdU5&GU)}U&UvKYd_^V273TF*2eRj6}sk*rn zH)Rj}Kh({A|DYYfC0^+uoH?B7+L`lS|^r0+XLg=9buGa4t8 zH9fQAGb(a4)&yNW&Ysh2lhIX;A}T05j#0z1&Ku`6oJ>Wt(aburP2t&)LxWID2ti^vlYAD1 z$p;YxMv9_`1al^*kn~n{8X6IzB}x*(Q*gp7WS56f#;24l96`u?H;_)m*)Ujuwzn>1 zI3&jbu~3?`nI&aPLkl2)p%8>$4#6OEwbv?$=h;HywL&v&j2_8S;fVu8m?5;0;@I35 zZQhSKcVVp_z|f+@5zP_JBe9G`0l7FjV8kFaBnNNECk0p}Poi-IbL*1~ha;DV9%4=y zV?r7aQJ(RG3eR(hGB2?Q8Aiy7c|eIKpy38|rfFm-oFw*I!6akB0v^T^Qv?Vf@}MN3 zI6DC|iXj{knLbfBVf>*LW?4oaT|l$V5NDTpcHXKMvt^GGlMgNpb4^<)L6VR$lkj-X zx`MRdRii#D*$}(qKkf=y3)@|TV(hd+Fjjjj=ylVyeW!1~VkO~PeEIvc){E=%zTN>J z>$_F?rhU@4n4IS;gW?Fy));A7ckS;@Rd-sJ-f|c2ylwirK}OkRBY0hIfN7kp0m3F5 zYY=GOm+x11psJ;Ur5^K~x4+>_FZg#r_9EdZ>_w6SyD8yZ_<7lv>-PT8^=VUoS4Oki zmOpIG7=jBL@b1FO8Z6yR5ZZYN5qVTC*Yp+!1SK9q&S3={LY`*Fep9a2^+w4Dhd*KfV1AY$|9w literal 0 HcmV?d00001 diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 2e1871c4c2..80ef295465 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -25,6 +25,7 @@ final class SwapSetupPresenter { private var feeIdentifier: String? private var accountId: AccountId? + private var splippage: BigRational = .percent(of: 1) init( interactor: SwapSetupInteractorInputProtocol, @@ -209,7 +210,6 @@ final class SwapSetupPresenter { return } - // TODO: Provide slippage let args = AssetConversion.CallArgs( assetIn: quote.assetIn, amountIn: quote.amountIn, @@ -217,7 +217,7 @@ final class SwapSetupPresenter { amountOut: quote.amountOut, receiver: accountId, direction: quoteArgs.direction, - slippage: .percent(of: 1) + slippage: splippage ) guard args.identifier != feeIdentifier else { diff --git a/novawallet/Modules/Swaps/Setup/View/SwapNetworkFeeView.swift b/novawallet/Modules/Swaps/Setup/View/SwapNetworkFeeView.swift index 1aa64435ae..ca9bafde49 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapNetworkFeeView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapNetworkFeeView.swift @@ -3,12 +3,13 @@ import UIKit import SoraUI import Kingfisher -final class SwapNetworkFeeView: GenericTitleValueView>, SkeletonableView { +final class SwapNetworkFeeView: GenericTitleValueView>, + SkeletonableView { var titleButton: RoundedButton { titleView } var valueTopButton: RoundedButton { valueView.fView } var valueBottomLabel: UILabel { valueView.sView } var skeletonView: SkrullableView? - private lazy var iconPencil = R.image.iconPencil()?.tinted(with: R.color.colorIconSecondary()!)?.kf.resize(to: .init(width: 16, height: 16)) + private lazy var iconPencil = R.image.iconPencilEdit()! private var isLoading: Bool = false From 12a0fdd211093ae7ebf304639d4624322a69e11c Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 16 Oct 2023 10:57:24 +0200 Subject: [PATCH 038/204] fix swap asset spacing --- novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift b/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift index 4a1fae4079..b643d027c5 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift @@ -14,7 +14,7 @@ final class SwapAssetControl: BackgroundedContentControl { } } - var horizontalSpacing: CGFloat = 8 { + var horizontalSpacing: CGFloat = 12 { didSet { setNeedsLayout() } From 02346d7b9b37339f0363687f0066869e313f4c4d Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 17 Oct 2023 13:51:05 +0300 Subject: [PATCH 039/204] fix layout --- .../Amount/AmountInputFormatterPlugin.swift | 5 +- .../Swaps/Setup/SwapSetupPresenter.swift | 7 +- .../Swaps/Setup/SwapSetupProtocols.swift | 2 + .../Swaps/Setup/SwapSetupWireframe.swift | 4 + .../Slippage/SwapSlippageInputView.swift | 109 +++++++++++++----- .../Slippage/SwapSlippagePresenter.swift | 39 ++++++- .../Slippage/SwapSlippageProtocols.swift | 3 + .../Slippage/SwapSlippageViewController.swift | 35 +++++- .../Slippage/SwapSlippageViewFactory.swift | 4 + novawallet/en.lproj/Localizable.strings | 2 + novawallet/ru.lproj/Localizable.strings | 2 + 11 files changed, 174 insertions(+), 38 deletions(-) diff --git a/novawallet/Common/ViewModel/Amount/AmountInputFormatterPlugin.swift b/novawallet/Common/ViewModel/Amount/AmountInputFormatterPlugin.swift index c83da24ae4..db3b5b10bc 100644 --- a/novawallet/Common/ViewModel/Amount/AmountInputFormatterPlugin.swift +++ b/novawallet/Common/ViewModel/Amount/AmountInputFormatterPlugin.swift @@ -24,7 +24,10 @@ struct AddSymbolAmountInputFormatterPlugin: AmountInputFormatterPluginProtocol { } func postProccesAmount(_ amount: String) -> String { - [amount, symbol].joined(separator: separator) + guard !amount.isEmpty else { + return "" + } + return [amount, symbol].joined(separator: separator) } func processAvailableCharacters(_ characterSet: CharacterSet) -> CharacterSet { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 4c3569d62d..7fdc79688c 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -345,8 +345,13 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { func proceed() {} func showSettings() { + guard let payChainAsset = payChainAsset else { + return + } wireframe.showSettings( - from: view + from: view, + percent: slippage?.value, + chainAsset: payChainAsset ) { [weak self, payChainAsset] slippageValue in guard payChainAsset == self?.payChainAsset else { return diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 8627596dd6..7b24002145 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -58,6 +58,8 @@ protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryabl ) func showSettings( from view: ControllerBackedProtocol?, + percent: BigRational?, + chainAsset: ChainAsset, completionHandler: @escaping (BigRational) -> Void ) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index b4bc6584cf..bbbe531126 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -49,9 +49,13 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { func showSettings( from view: ControllerBackedProtocol?, + percent: BigRational?, + chainAsset: ChainAsset, completionHandler: @escaping (BigRational) -> Void ) { guard let settingsView = SwapSlippageViewFactory.createView( + percent: percent, + chainAsset: chainAsset, completionHandler: completionHandler ) else { return diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift index 4945e47671..18d2d59a78 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift @@ -7,7 +7,7 @@ protocol SwapSlippageInputViewDelegateProtocol: AnyObject { final class SwapSlippageInputView: BackgroundedContentControl { let textField: UITextField = .create { - $0.font = .title2 + $0.font = UIFont.regularSubheadline $0.textColor = R.color.colorTextPrimary() $0.tintColor = R.color.colorTextPrimary() $0.textAlignment = .left @@ -16,7 +16,7 @@ final class SwapSlippageInputView: BackgroundedContentControl { string: "0.5 %", attributes: [ .foregroundColor: R.color.colorHintText()!, - .font: UIFont.title2 + .font: UIFont.regularSubheadline ] ) @@ -24,6 +24,14 @@ final class SwapSlippageInputView: BackgroundedContentControl { $0.clearButtonMode = .whileEditing } + let symbolLabel: UILabel = .create { + $0.apply(style: .regularSubhedlinePrimary) + $0.numberOfLines = 1 + $0.textAlignment = .left + $0.text = "%" + $0.isHidden = true + } + var roundedBackgroundView: RoundedView? { backgroundView as? RoundedView } @@ -40,8 +48,10 @@ final class SwapSlippageInputView: BackgroundedContentControl { override init(frame: CGRect) { super.init(frame: frame) - setupLayout() + + contentInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12) configureBackgroundViewIfNeeded() + configureContentView() setupTextFieldHandlers() } @@ -50,18 +60,49 @@ final class SwapSlippageInputView: BackgroundedContentControl { fatalError("init(coder:) has not been implemented") } - private func setupLayout() { - addSubview(textField) - textField.snp.makeConstraints { - $0.leading.trailing.equalToSuperview().inset(12) - $0.top.bottom.equalToSuperview().inset(14) + override func layoutSubviews() { + let availableWidth = bounds.width - contentInsets.left - contentInsets.right + let textFieldWidth = max(availableWidth, 0) + + let textFieldHeight: CGFloat = textField.intrinsicContentSize.height + + textField.frame = CGRect( + x: bounds.maxX - contentInsets.right - textFieldWidth, + y: bounds.midY - textFieldHeight / 2, + width: textFieldWidth, + height: textFieldHeight + ) + + if !symbolLabel.isHidden { + let text = textField.text ?? "" + let texFieldContentSize = text.size(withAttributes: textField.typingAttributes) + symbolLabel.frame = .init( + x: textField.frame.minX + texFieldContentSize.width + 4, + y: textField.frame.midY - symbolLabel.intrinsicContentSize.height / 2, + width: symbolLabel.intrinsicContentSize.width, + height: symbolLabel.intrinsicContentSize.height + ) } - addSubview(buttonsStack) - buttonsStack.snp.makeConstraints { - $0.trailing.equalToSuperview().inset(12) - $0.top.bottom.equalToSuperview().inset(8) - $0.width.lessThanOrEqualToSuperview().multipliedBy(0.7) + + if !buttonsStack.isHidden, !buttonsStack.arrangedSubviews.isEmpty { + var buttonsWidth: CGFloat = buttonsStack.arrangedSubviews.reduce(into: 0) { + $0 = $0 + $1.intrinsicContentSize.width + } + buttonsWidth += CGFloat(buttonsStack.arrangedSubviews.count - 1) * 8 + let height: CGFloat = buttonsStack.arrangedSubviews.max(by: { + $0.intrinsicContentSize.height > $1.intrinsicContentSize.height + })?.intrinsicContentSize.height ?? 0 + let buttonStackX = bounds.maxX - contentInsets.right - buttonsWidth + + buttonsStack.frame = .init( + x: buttonStackX, + y: textField.frame.midY - height / 2, + width: buttonsWidth, + height: height + ) } + + backgroundView?.frame = bounds } private func configureBackgroundViewIfNeeded() { @@ -73,8 +114,13 @@ final class SwapSlippageInputView: BackgroundedContentControl { } } + private func configureContentView() { + addSubview(textField) + addSubview(symbolLabel) + addSubview(buttonsStack) + } + private func setupTextFieldHandlers() { - textField.addTarget(self, action: #selector(editingDidChangeAction), for: .editingChanged) textField.addTarget(self, action: #selector(editingDidBeginAction), for: .editingDidBegin) textField.addTarget(self, action: #selector(editingDidEndAction), for: .editingDidEnd) textField.addTarget(self, action: #selector(editingDidEndAction), for: .editingDidEndOnExit) @@ -86,10 +132,6 @@ final class SwapSlippageInputView: BackgroundedContentControl { roundedBackgroundView?.strokeWidth = textField.isFirstResponder ? 0.5 : 0.0 } - @objc private func editingDidChangeAction() { - buttonsStack.isHidden = true - } - @objc private func editingDidEndAction() { if textField.text.isNilOrEmpty { buttonsStack.isHidden = false @@ -101,7 +143,7 @@ final class SwapSlippageInputView: BackgroundedContentControl { guard let delegate = delegate else { return } - if let index = buttonsStack.arrangedSubviews.index(where: { $0 === sender }), + if let index = buttonsStack.arrangedSubviews.firstIndex(where: { $0 === sender }), let buttonModel = viewModel[safe: index] { delegate.didSelect(percent: buttonModel, sender: self) } @@ -117,6 +159,13 @@ final class SwapSlippageInputView: BackgroundedContentControl { button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside) return button } + + private func updateViewsVisablilty(for text: String?) { + symbolLabel.isHidden = text.isNilOrEmpty + buttonsStack.isHidden = !text.isNilOrEmpty + setNeedsLayout() + layoutIfNeeded() + } } extension SwapSlippageInputView: UITextFieldDelegate { @@ -125,27 +174,24 @@ extension SwapSlippageInputView: UITextFieldDelegate { shouldChangeCharactersIn range: NSRange, replacementString string: String ) -> Bool { - inputViewModel?.didReceiveReplacement(string, for: range) ?? false + let shouldChangeCharacters = inputViewModel?.didReceiveReplacement(string, for: range) ?? false + updateViewsVisablilty(for: string) + return shouldChangeCharacters } - func textFieldDidBeginEditing(_ textField: UITextField) { - if let offset = inputViewModel?.currentOffset, - let position = textField.position(from: textField.beginningOfDocument, offset: offset) { - textField.selectedTextRange = textField.textRange( - from: textField.beginningOfDocument, - to: position - ) - } + func textFieldShouldClear(_: UITextField) -> Bool { + updateViewsVisablilty(for: "") + + return true } } extension SwapSlippageInputView: AmountInputViewModelObserver { func amountInputDidChange() { textField.text = inputViewModel?.displayAmount + updateViewsVisablilty(for: textField.text) - if textField.isEditing { - sendActions(for: .editingChanged) - } + sendActions(for: .editingChanged) } } @@ -168,5 +214,6 @@ extension SwapSlippageInputView { self.inputViewModel = inputViewModel textField.text = inputViewModel.displayAmount + updateViewsVisablilty(for: textField.text) } } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index fea3f3f01b..870a6321c4 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -10,6 +10,8 @@ final class SwapSlippagePresenter { let percentFormatterLocalizable: LocalizableResource let completionHandler: (BigRational) -> Void let prefilledPercents: [Decimal] = [0.1, 1, 3] + let initPercent: BigRational? + let chainAsset: ChainAsset private var percentFormatter: NumberFormatter private var numberFormatter: NumberFormatter @@ -21,12 +23,16 @@ final class SwapSlippagePresenter { numberFormatterLocalizable: LocalizableResource, percentFormatterLocalizable: LocalizableResource, localizationManager: LocalizationManagerProtocol, + initPercent: BigRational?, + chainAsset: ChainAsset, completionHandler: @escaping (BigRational) -> Void ) { self.interactor = interactor self.wireframe = wireframe self.numberFormatterLocalizable = numberFormatterLocalizable self.percentFormatterLocalizable = percentFormatterLocalizable + self.initPercent = initPercent + self.chainAsset = chainAsset self.completionHandler = completionHandler percentFormatter = percentFormatterLocalizable.value(for: localizationManager.selectedLocale) numberFormatter = numberFormatterLocalizable.value(for: localizationManager.selectedLocale) @@ -45,12 +51,17 @@ final class SwapSlippagePresenter { formatter: numberFormatter, inputLocale: selectedLocale, precision: 1, - plugin: AddSymbolAmountInputFormatterPlugin() + plugin: nil ) view?.didReceiveInput(viewModel: inputViewModel) } + func provideResetButtonState() { + let amountChanged = amountInput.map { fraction(from: $0) } != initPercent + view?.didReceiveResetState(available: amountChanged) + } + func fraction(from number: Decimal) -> BigRational { let decimalNumber = NSDecimalNumber(decimal: number) let scale = -number.exponent @@ -68,17 +79,41 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { title: title(for: $0 / (percentFormatter.multiplier?.decimalValue ?? 1)) ) } - view?.didReceivePreFilledPercents(viewModel: viewModel) + + if let percent = initPercent, percent.denominator != 0 { + let numerator = percent.numerator.decimal(precision: chainAsset.asset.precision) + let denominator = percent.denominator.decimal(precision: chainAsset.asset.precision) + amountInput = numerator / denominator + } + provideResetButtonState() provideAmountViewModel() + view?.didReceivePreFilledPercents(viewModel: viewModel) } func select(percent: Percent) { amountInput = percent.value provideAmountViewModel() + provideResetButtonState() } func updateAmount(_ amount: Decimal?) { amountInput = amount + provideResetButtonState() + } + + func showSlippageInfo() { + // TODO: show bottomsheet + } + + func reset() { + if let initPercent = initPercent, initPercent.denominator != 0 { + amountInput = initPercent.numerator.decimal(precision: chainAsset.asset.precision) / initPercent.denominator.decimal(precision: chainAsset.asset.precision) + } else { + amountInput = nil + } + + provideAmountViewModel() + provideResetButtonState() } func apply() { diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift index 5469da77c0..f9ebc069bd 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift @@ -3,6 +3,7 @@ import Foundation protocol SwapSlippageViewProtocol: ControllerBackedProtocol { func didReceivePreFilledPercents(viewModel: [Percent]) func didReceiveInput(viewModel: AmountInputViewModelProtocol) + func didReceiveResetState(available: Bool) } protocol SwapSlippagePresenterProtocol: AnyObject { @@ -10,6 +11,8 @@ protocol SwapSlippagePresenterProtocol: AnyObject { func select(percent: Percent) func updateAmount(_ amount: Decimal?) func apply() + func showSlippageInfo() + func reset() } protocol SwapSlippageInteractorInputProtocol: AnyObject {} diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift index e20d7ce157..90e871e28d 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift @@ -30,19 +30,27 @@ final class SwapSlippageViewController: UIViewController, ViewHolder { setupLocalization() setupHandlers() setupAccessoryView() + setupNavigationItem() presenter.setup() } private func setupLocalization() { - title = "Swap settings" - rootView.slippageButton.imageWithTitleView?.title = "Slippage" - rootView.actionButton.imageWithTitleView?.title = "Apply" + let languages = selectedLocale.rLanguages + title = R.string.localizable.swapsSetupSettingsTitle( + preferredLanguages: languages) + rootView.slippageButton.imageWithTitleView?.title = R.string.localizable.swapsSetupSlippage( + preferredLanguages: languages) + rootView.actionButton.imageWithTitleView?.title = R.string.localizable.commonApply( + preferredLanguages: languages) + navigationItem.rightBarButtonItem?.title = R.string.localizable.commonReset( + preferredLanguages: selectedLocale.rLanguages) } private func setupHandlers() { rootView.amountInput.delegate = self rootView.actionButton.addTarget(self, action: #selector(applyButtonAction), for: .touchUpInside) rootView.amountInput.textField.addTarget(self, action: #selector(inputEditingAction), for: .editingChanged) + rootView.slippageButton.addTarget(self, action: #selector(slippageInfoAction), for: .touchUpInside) } private func setupAccessoryView() { @@ -55,6 +63,15 @@ final class SwapSlippageViewController: UIViewController, ViewHolder { rootView.amountInput.textField.inputAccessoryView = accessoryView } + private func setupNavigationItem() { + navigationItem.rightBarButtonItem = .init( + title: R.string.localizable.commonReset(preferredLanguages: selectedLocale.rLanguages), + style: .plain, + target: self, + action: #selector(resetAction) + ) + } + private func updateActionButton() { rootView.actionButton.isEnabled = rootView.amountInput.inputViewModel?.isValid == true } @@ -72,6 +89,14 @@ final class SwapSlippageViewController: UIViewController, ViewHolder { presenter.updateAmount(amount) updateActionButton() } + + @objc private func slippageInfoAction() { + presenter.showSlippageInfo() + } + + @objc private func resetAction() { + presenter.reset() + } } extension SwapSlippageViewController: SwapSlippageViewProtocol { @@ -83,6 +108,10 @@ extension SwapSlippageViewController: SwapSlippageViewProtocol { rootView.amountInput.bind(inputViewModel: viewModel) updateActionButton() } + + func didReceiveResetState(available: Bool) { + navigationItem.rightBarButtonItem?.isEnabled = available + } } extension SwapSlippageViewController: SwapSlippageInputViewDelegateProtocol { diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift index 9cbc16b50b..07f64e69f4 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift @@ -3,6 +3,8 @@ import SoraFoundation struct SwapSlippageViewFactory { static func createView( + percent: BigRational?, + chainAsset: ChainAsset, completionHandler: @escaping (BigRational) -> Void ) -> SwapSlippageViewProtocol? { let interactor = SwapSlippageInteractor() @@ -17,6 +19,8 @@ struct SwapSlippageViewFactory { numberFormatterLocalizable: amountFormatter.localizableResource(), percentFormatterLocalizable: percentFormatter.localizableResource(), localizationManager: LocalizationManager.shared, + initPercent: percent, + chainAsset: chainAsset, completionHandler: completionHandler ) diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index b0d695df7a..25ba84b586 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1391,3 +1391,5 @@ "swaps.setup.details.title" = "Swap details"; "swaps.pay.token.selection.title" = "Token to pay"; "swaps.receive.token.selection.title" = "Token to receive"; +"swaps.setup.settings.title" = "Swap settings"; +"swaps.setup.slippage" = "Slippage"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 5048b5b358..40c23d458f 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1392,3 +1392,5 @@ "swaps.setup.details.title" = "Детали обмена"; "swaps.pay.token.selection.title" = "Токен для оплаты"; "swaps.receive.token.selection.title" = "Токен для получения"; +"swaps.setup.settings.title" = "Настройки обмена"; +"swaps.setup.slippage" = "Slippage"; From 119fdc5554a5ddc3b6a9dd2605c1fa32568a0db6 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 17 Oct 2023 13:56:05 +0300 Subject: [PATCH 040/204] build fix --- Podfile.lock | 2 +- novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 9500b91c99..7581bee044 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -305,4 +305,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f37e3724d47617fb7ce7ed5e0a583491617b5899 -COCOAPODS: 1.12.1 +COCOAPODS: 1.13.0 diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 1a65f68453..3274fc2253 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -210,7 +210,7 @@ final class SwapSetupPresenter { private func estimateFee() { guard let quote = quote, let accountId = accountId, - let quoteArgs = quoteArg, + let quoteArgs = quoteArgs, let slippage = slippage else { return } From 889c92e6f0c1f2025fed58b4b0fa613f170d85cc Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 17 Oct 2023 14:10:55 +0300 Subject: [PATCH 041/204] remove unused code --- novawallet.xcodeproj/project.pbxproj | 12 ---- .../Amount/AmountInputFormatterPlugin.swift | 44 -------------- .../Amount/AmountInputViewModel.swift | 11 +--- .../ViewModel/Amount/MoneyPresentable.swift | 38 ++++--------- .../Swaps/Setup/SwapSetupPresenter.swift | 10 ++-- .../Slippage/SwapSlippagePresenter.swift | 3 +- .../Common/Helpers/MoneyPresentableMock.swift | 10 ---- .../Helpers/MoneyPresentableTests.swift | 57 ------------------- 8 files changed, 18 insertions(+), 167 deletions(-) delete mode 100644 novawallet/Common/ViewModel/Amount/AmountInputFormatterPlugin.swift delete mode 100644 novawalletTests/Common/Helpers/MoneyPresentableMock.swift delete mode 100644 novawalletTests/Common/Helpers/MoneyPresentableTests.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index fb38a56d5d..b2250cd5c9 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -704,9 +704,6 @@ 775F194D2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775F194C2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift */; }; 775F19512A5811FA009915B6 /* StartStakingParachainInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775F19502A5811FA009915B6 /* StartStakingParachainInteractor.swift */; }; 775F19532A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775F19522A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift */; }; - 7765BB712ADA66A400451274 /* MoneyPresentableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7765BB702ADA66A400451274 /* MoneyPresentableTests.swift */; }; - 7765BB732ADA744400451274 /* MoneyPresentableMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7765BB722ADA744400451274 /* MoneyPresentableMock.swift */; }; - 7765BB752ADA749400451274 /* AmountInputFormatterPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7765BB742ADA749400451274 /* AmountInputFormatterPlugin.swift */; }; 77740BBC2AD4A7B800E8C06F /* CollapsableContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBB2AD4A7B800E8C06F /* CollapsableContainerView.swift */; }; 77740BBE2AD4A7F500E8C06F /* SwapDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */; }; 77740BC02AD4A80D00E8C06F /* SwapRateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */; }; @@ -4722,9 +4719,6 @@ 775F194C2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoRelaychainPresenter.swift; sourceTree = ""; }; 775F19502A5811FA009915B6 /* StartStakingParachainInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingParachainInteractor.swift; sourceTree = ""; }; 775F19522A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoParachainPresenter.swift; sourceTree = ""; }; - 7765BB702ADA66A400451274 /* MoneyPresentableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyPresentableTests.swift; sourceTree = ""; }; - 7765BB722ADA744400451274 /* MoneyPresentableMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyPresentableMock.swift; sourceTree = ""; }; - 7765BB742ADA749400451274 /* AmountInputFormatterPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountInputFormatterPlugin.swift; sourceTree = ""; }; 77740BBB2AD4A7B800E8C06F /* CollapsableContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsableContainerView.swift; sourceTree = ""; }; 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapDetailsView.swift; sourceTree = ""; }; 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRateView.swift; sourceTree = ""; }; @@ -8236,7 +8230,6 @@ 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */, 0CE629D42AA9B5E200E250BD /* BalanceViewModel.swift */, 0CE629D52AA9B5E200E250BD /* BalanceViewModelFactory.swift */, - 7765BB742ADA749400451274 /* AmountInputFormatterPlugin.swift */, ); path = Amount; sourceTree = ""; @@ -16344,8 +16337,6 @@ 847F2D5627AB08D200AFD476 /* GradientColorFactoryTests.swift */, 842B2FCC2947239B002829B6 /* CoinGeckoUrlParserTests.swift */, 840D627029CB3FD900D5E894 /* URLBuilderTests.swift */, - 7765BB702ADA66A400451274 /* MoneyPresentableTests.swift */, - 7765BB722ADA744400451274 /* MoneyPresentableMock.swift */, ); path = Helpers; sourceTree = ""; @@ -20248,7 +20239,6 @@ 0C59E8E12AA60FF0001E11F3 /* CrowdloanExternalServiceFactory.swift in Sources */, 847297CF260B4035009B86D0 /* ChangeTargetConfirmInteractor.swift in Sources */, 8455F19C2A1DF088003F072D /* ChainsStore+Multistaking.swift in Sources */, - 7765BB752ADA749400451274 /* AmountInputFormatterPlugin.swift in Sources */, 8428768724AE046300D91AD8 /* LanguageSelectionViewFactory.swift in Sources */, 84FB9E26285C6C5000B42FC0 /* XcmVersionedMultiasset.swift in Sources */, 84953F6E2935E35D0033F47D /* EtherscanHistoryContext.swift in Sources */, @@ -23107,7 +23097,6 @@ 842D1E8E24D2091A00C30A7A /* CommonMocks.swift in Sources */, 8467FD0824E5E0BD005D486C /* KeystoreDefinition+Test.swift in Sources */, 84452F5D25D5CB3B00F47EC5 /* FileManagerTests.swift in Sources */, - 7765BB732ADA744400451274 /* MoneyPresentableMock.swift in Sources */, 84B7C72D289BFA79001A3566 /* ValidatorSearchTests.swift in Sources */, 841B45292603D91800C08693 /* EraValidatorsServiceStub.swift in Sources */, 84F47D652667804100F7647A /* CrowdloanBonusServiceStub.swift in Sources */, @@ -23140,7 +23129,6 @@ 840D627129CB3FD900D5E894 /* URLBuilderTests.swift in Sources */, 84D9C41126CD361C004AB2AB /* SpecVersionSubscriptionTests.swift in Sources */, 84B66A1626FDF7D70038B963 /* JsonSubscriptionFactoryStub.swift in Sources */, - 7765BB712ADA66A400451274 /* MoneyPresentableTests.swift in Sources */, 8857E02D28A4F6C000260BA2 /* CurrencyManagerStub.swift in Sources */, 8467FD3B24EAD236005D486C /* SigningWrapperTests.swift in Sources */, 84B7C71D289BFA79001A3566 /* MnemonicTextNormalizerTest.swift in Sources */, diff --git a/novawallet/Common/ViewModel/Amount/AmountInputFormatterPlugin.swift b/novawallet/Common/ViewModel/Amount/AmountInputFormatterPlugin.swift deleted file mode 100644 index db3b5b10bc..0000000000 --- a/novawallet/Common/ViewModel/Amount/AmountInputFormatterPlugin.swift +++ /dev/null @@ -1,44 +0,0 @@ -import Foundation - -protocol AmountInputFormatterPluginProtocol { - func preProccessAmount(_ amount: String) -> String - func postProccesAmount(_ amount: String) -> String - func processAvailableCharacters(_ characterSet: CharacterSet) -> CharacterSet - func currentOffset(in amount: String) -> Int? -} - -struct AddSymbolAmountInputFormatterPlugin: AmountInputFormatterPluginProtocol { - var symbol = "%" - var separator = " " - - func preProccessAmount(_ amount: String) -> String { - guard amount.hasSuffix(symbol) else { - return amount - } - - let count = amount.hasSuffix(separator + symbol) ? symbol.count + separator.count : symbol.count - - let offset = amount.count - count - let index = amount.index(amount.startIndex, offsetBy: offset) - return String(amount.prefix(upTo: index)) - } - - func postProccesAmount(_ amount: String) -> String { - guard !amount.isEmpty else { - return "" - } - return [amount, symbol].joined(separator: separator) - } - - func processAvailableCharacters(_ characterSet: CharacterSet) -> CharacterSet { - characterSet.union(CharacterSet(charactersIn: symbol).inverted) - } - - func currentOffset(in amount: String) -> Int? { - guard amount.hasSuffix(symbol) else { - return nil - } - let count = amount.hasSuffix(separator + symbol) ? symbol.count + separator.count : symbol.count - return amount.count - count - } -} diff --git a/novawallet/Common/ViewModel/Amount/AmountInputViewModel.swift b/novawallet/Common/ViewModel/Amount/AmountInputViewModel.swift index f6fa2cb0e2..9ec5d7dc16 100644 --- a/novawallet/Common/ViewModel/Amount/AmountInputViewModel.swift +++ b/novawallet/Common/ViewModel/Amount/AmountInputViewModel.swift @@ -13,7 +13,6 @@ protocol AmountInputViewModelProtocol: AnyObject { func didReceiveReplacement(_ string: String, for range: NSRange) -> Bool func didUpdateAmount(to newAmount: Decimal) - var currentOffset: Int? { get } } extension AmountInputViewModelProtocol { @@ -63,23 +62,19 @@ final class AmountInputViewModel: AmountInputViewModelProtocol, MoneyPresentable public var observable: WalletViewModelObserverContainer - let plugin: AmountInputFormatterPluginProtocol? - public init( symbol: String, amount: Decimal?, limit: Decimal, formatter: NumberFormatter, inputLocale: Locale = Locale.current, - precision: Int16 = 2, - plugin: AmountInputFormatterPluginProtocol? = nil + precision: Int16 = 2 ) { self.symbol = symbol self.limit = limit self.formatter = formatter self.inputLocale = inputLocale self.precision = precision - self.plugin = plugin observable = WalletViewModelObserverContainer() @@ -123,8 +118,4 @@ final class AmountInputViewModel: AmountInputViewModelProtocol, MoneyPresentable amount = inputAmount } - - var currentOffset: Int? { - plugin?.currentOffset(in: amount) - } } diff --git a/novawallet/Common/ViewModel/Amount/MoneyPresentable.swift b/novawallet/Common/ViewModel/Amount/MoneyPresentable.swift index 72ae87d531..f19c281a09 100644 --- a/novawallet/Common/ViewModel/Amount/MoneyPresentable.swift +++ b/novawallet/Common/ViewModel/Amount/MoneyPresentable.swift @@ -4,7 +4,6 @@ protocol MoneyPresentable { var formatter: NumberFormatter { get } var amount: String { get } var precision: Int16 { get } - var plugin: AmountInputFormatterPluginProtocol? { get } func transform(input: String, from locale: Locale) -> String } @@ -19,19 +18,17 @@ extension MoneyPresentable { return "" } - let preprocessedAmount = plugin?.preProccessAmount(amount) ?? amount - - guard let decimalAmount = Decimal(string: preprocessedAmount, locale: formatter.locale) else { + guard let decimalAmount = Decimal(string: amount, locale: formatter.locale) else { return nil } var amountFormatted = formatter.string(from: decimalAmount as NSDecimalNumber) let separator = decimalSeparator() - if preprocessedAmount.hasSuffix(separator) { + if amount.hasSuffix(separator) { amountFormatted?.append(separator) } else { - let amountParts = preprocessedAmount.components(separatedBy: separator) + let amountParts = amount.components(separatedBy: separator) let formattedParts = amountFormatted?.components(separatedBy: separator) if amountParts.count == 2, formattedParts?.count == 1 { @@ -54,11 +51,7 @@ extension MoneyPresentable { } } - guard let plugin = plugin, let amountFormatted = amountFormatted else { - return amountFormatted - } - - return plugin.postProccesAmount(amountFormatted) + return amountFormatted } private func decimalSeparator() -> String { @@ -70,14 +63,12 @@ extension MoneyPresentable { } private func notEligibleSet() -> CharacterSet { - let availableSet = CharacterSet.decimalDigits + CharacterSet.decimalDigits .union(CharacterSet(charactersIn: "\(decimalSeparator())\(groupingSeparator())")).inverted - return plugin?.processAvailableCharacters(availableSet) ?? availableSet } private func isValid(amount: String) -> Bool { - let preprocessedAmount = plugin?.preProccessAmount(amount) ?? amount - let components = preprocessedAmount.components(separatedBy: decimalSeparator()) + let components = amount.components(separatedBy: decimalSeparator()) return !((precision == 0 && components.count > 1) || components.count > 2 || @@ -89,8 +80,7 @@ extension MoneyPresentable { return self.amount } - let preprocessedAmount = plugin?.preProccessAmount(self.amount) ?? self.amount - var newAmount = (preprocessedAmount + amount).replacingOccurrences( + var newAmount = (self.amount + amount).replacingOccurrences( of: groupingSeparator(), with: "" ) @@ -99,19 +89,15 @@ extension MoneyPresentable { newAmount = "\(MoneyPresentableConstants.singleZero)\(newAmount)" } - let postprocessedAmount = plugin?.postProccesAmount(newAmount) ?? newAmount - - return isValid(amount: postprocessedAmount) ? postprocessedAmount : self.amount + return isValid(amount: newAmount) ? newAmount : self.amount } func set(_ amount: String) -> String { - let preprocessedAmount = plugin?.preProccessAmount(amount) ?? amount - - guard preprocessedAmount.rangeOfCharacter(from: notEligibleSet()) == nil else { + guard amount.rangeOfCharacter(from: notEligibleSet()) == nil else { return self.amount } - var settingAmount = preprocessedAmount.replacingOccurrences( + var settingAmount = amount.replacingOccurrences( of: groupingSeparator(), with: "" ) @@ -120,9 +106,7 @@ extension MoneyPresentable { settingAmount = "\(MoneyPresentableConstants.singleZero)\(settingAmount)" } - let postprocessedAmount = plugin?.postProccesAmount(settingAmount) ?? settingAmount - - return isValid(amount: postprocessedAmount) ? postprocessedAmount : self.amount + return isValid(amount: settingAmount) ? settingAmount : self.amount } func transform(input: String, from locale: Locale) -> String { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 3274fc2253..3c9565a216 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -23,7 +23,7 @@ final class SwapSetupPresenter { } } - private var slippage: (value: BigRational, direction: AssetConversion.Direction)? + private var slippage: BigRational? private var feeIdentifier: String? private var accountId: AccountId? @@ -222,7 +222,7 @@ final class SwapSetupPresenter { amountOut: quote.amountOut, receiver: accountId, direction: quoteArgs.direction, - slippage: slippage.value + slippage: slippage ) guard args.identifier != feeIdentifier else { @@ -292,7 +292,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideDetailsViewModel(isAvailable: false) provideButtonState() // TODO: get from settings - slippage = (value: .percent(of: 1), direction: .sell) + slippage = .percent(of: 1) interactor.setup() } @@ -354,13 +354,13 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } wireframe.showSettings( from: view, - percent: slippage?.value, + percent: slippage, chainAsset: payChainAsset ) { [weak self, payChainAsset] slippageValue in guard payChainAsset == self?.payChainAsset else { return } - self?.slippage = (value: slippageValue, direction: .sell) + self?.slippage = slippageValue self?.estimateFee() } } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index 870a6321c4..230faa4885 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -50,8 +50,7 @@ final class SwapSlippagePresenter { limit: 50, formatter: numberFormatter, inputLocale: selectedLocale, - precision: 1, - plugin: nil + precision: 1 ) view?.didReceiveInput(viewModel: inputViewModel) diff --git a/novawalletTests/Common/Helpers/MoneyPresentableMock.swift b/novawalletTests/Common/Helpers/MoneyPresentableMock.swift deleted file mode 100644 index 3850d12166..0000000000 --- a/novawalletTests/Common/Helpers/MoneyPresentableMock.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation -@testable import novawallet - -final class MoneyPresentableMock: MoneyPresentable { - var formatter: NumberFormatter { NumberFormatter.amount } - var amount: String = "" - var precision: Int16 = 2 - let plugin: AmountInputFormatterPluginProtocol? = AddSymbolAmountInputFormatterPlugin() -} - diff --git a/novawalletTests/Common/Helpers/MoneyPresentableTests.swift b/novawalletTests/Common/Helpers/MoneyPresentableTests.swift deleted file mode 100644 index aa4c5a2ca8..0000000000 --- a/novawalletTests/Common/Helpers/MoneyPresentableTests.swift +++ /dev/null @@ -1,57 +0,0 @@ -import XCTest -@testable import novawallet - -final class MoneyPresentableTests: XCTestCase { - let moneyPresentable = MoneyPresentableMock() - - override func setUpWithError() throws { - moneyPresentable.amount = "" - } - - func testWhenAddNumberToEmptyInput_ThanResultCorrectPercent() { - let addingSymbol = "3" - let expectation = "3 %" - - let result = moneyPresentable.add(addingSymbol) - XCTAssertEqual(result, expectation) - } - - func testWhenAddNumberToNonEmptyInput_ThanResultCorrectPercent() { - let text = "1 %" - let addingSymbol = "2" - let expectation = "12 %" - moneyPresentable.amount = text - let result = moneyPresentable.add(addingSymbol) - - XCTAssertEqual(result, expectation) - } - - func testWhenSetNumberWithPercent_ThanResultCorrectPercent() { - let setAmount = "5%" - let expectation = "5 %" - - let result = moneyPresentable.set(setAmount) - - XCTAssertEqual(result, expectation) - } - - func testWhenSetNumber_ThanResultCorrectPercent() { - let setAmount = "2.5" - let expectation = "2.5 %" - - let result = moneyPresentable.set(setAmount) - - XCTAssertEqual(result, expectation) - } - - func testWhenSetInvalidNumber_ThanResultNotChanged() { - let setAmount = "0.1 %%" - let expectation = "1 %" - moneyPresentable.amount = "1 %" - - let result = moneyPresentable.set(setAmount) - - XCTAssertEqual(result, expectation) - } - -} From 9414a530af1ce0679ade9469f0088bf6830bec64 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 17 Oct 2023 15:23:58 +0300 Subject: [PATCH 042/204] clean up --- novawallet.xcodeproj/project.pbxproj | 12 ------------ .../iconSwapSettings.imageset/Contents.json | 12 ++++++++++++ .../iconSwapSettings.imageset/Vector.pdf | Bin 0 -> 3420 bytes .../Swaps/Setup/SwapSetupViewController.swift | 3 +-- .../Slippage/SwapSlippageViewFactory.swift | 2 +- .../Swaps/Slippage/SwapSlippageViewLayout.swift | 3 ++- .../SwapSlippage/SwapSlippageTests.swift | 16 ---------------- 7 files changed, 16 insertions(+), 32 deletions(-) create mode 100644 novawallet/Assets.xcassets/iconSwapSettings.imageset/Contents.json create mode 100644 novawallet/Assets.xcassets/iconSwapSettings.imageset/Vector.pdf delete mode 100644 novawalletTests/Modules/SwapSlippage/SwapSlippageTests.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index b2250cd5c9..9c430f6c23 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -3368,7 +3368,6 @@ 8CF040889DBCA0E9D40BDC82 /* LedgerDiscoverViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67CAEB35921A61A8EC131AF8 /* LedgerDiscoverViewFactory.swift */; }; 8D9BC9C36DC891CDD900A895 /* AccountConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3104ABC4BECF08B0BA836AA /* AccountConfirmViewController.swift */; }; 8DA9BFE7774B292664FD843F /* DAppPhishingProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A6C6207095F63972E14618 /* DAppPhishingProtocols.swift */; }; - 8DCFB5C717B37AF2F0E22F85 /* SwapSlippageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD97740BCB718BAB9E3CAAC /* SwapSlippageTests.swift */; }; 8DF76D04C127E0048B253343 /* DAppListViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07D08AF2C744DF2073702499 /* DAppListViewFactory.swift */; }; 8E74A13BA73160F88B2B0948 /* AddDelegationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0492106F3E5C20019137E9AA /* AddDelegationViewController.swift */; }; 8EECC23DA32547DAAFC260BE /* ParaStkStakeConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9925D7FC8A58695700B4A308 /* ParaStkStakeConfirmPresenter.swift */; }; @@ -7698,7 +7697,6 @@ BA518E1D79D86360F145B428 /* TokensAddSelectNetworkInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensAddSelectNetworkInteractor.swift; sourceTree = ""; }; BA7DAF20C447065DA5467696 /* TransactionHistoryViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransactionHistoryViewLayout.swift; sourceTree = ""; }; BAB2478DE3AF0885A3ED7ED8 /* StakingRedeemPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRedeemPresenter.swift; sourceTree = ""; }; - BAD97740BCB718BAB9E3CAAC /* SwapSlippageTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSlippageTests.swift; sourceTree = ""; }; BAF9ED27CF12B7DA8B1378CF /* MarkdownDescriptionViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MarkdownDescriptionViewFactory.swift; sourceTree = ""; }; BB1A1934B76A5DCC65855EE1 /* TransactionHistoryWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransactionHistoryWireframe.swift; sourceTree = ""; }; BB494F0B16C9588325CF0D84 /* SwapSetupPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSetupPresenter.swift; sourceTree = ""; }; @@ -9234,14 +9232,6 @@ path = Instructions; sourceTree = ""; }; - 3CB2CEC27DA500C00B69D056 /* SwapSlippage */ = { - isa = PBXGroup; - children = ( - BAD97740BCB718BAB9E3CAAC /* SwapSlippageTests.swift */, - ); - path = SwapSlippage; - sourceTree = ""; - }; 3F64CF6065463B01222F1B8F /* ExportMnemonic */ = { isa = PBXGroup; children = ( @@ -14532,7 +14522,6 @@ 84B7C705289BFA79001A3566 /* AccountManagement */, 84B7C708289BFA79001A3566 /* WalletList */, 84B7C70A289BFA79001A3566 /* ControllerAccount */, - 3CB2CEC27DA500C00B69D056 /* SwapSlippage */, ); path = Modules; sourceTree = ""; @@ -23238,7 +23227,6 @@ 84B7C748289BFA79001A3566 /* WalletListTests.swift in Sources */, 84B7C720289BFA79001A3566 /* ReferralCrowdloanTests.swift in Sources */, F4897BB126AED13D0075F291 /* EraCountdownOperationFactoryStub.swift in Sources */, - 8DCFB5C717B37AF2F0E22F85 /* SwapSlippageTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/novawallet/Assets.xcassets/iconSwapSettings.imageset/Contents.json b/novawallet/Assets.xcassets/iconSwapSettings.imageset/Contents.json new file mode 100644 index 0000000000..c6f97a05d9 --- /dev/null +++ b/novawallet/Assets.xcassets/iconSwapSettings.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Vector.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconSwapSettings.imageset/Vector.pdf b/novawallet/Assets.xcassets/iconSwapSettings.imageset/Vector.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3a6480bc77c02f3abdaa25b92b9189ecb1038688 GIT binary patch literal 3420 zcmZWs%Z?j25WMp%crjofO!55y3wlaM_m|Jo0ldXTalV|U?W5Sdp8k2*O}Fn}$(s-BzjkNwm)N{9^8PH0 z;ttqD&x7Y<^zFCtY4^CS9Mi_&ygP`fQmBEWs=u{-6j|9EEjAliQ6V{emqB^w`KhR4 zY{bBWkVzQqgel6lU<7N_J;503k^$Pm&&}V~P>-<_3uskN+F=t!OC~{XX;lc(mqN$| zleHnFBfqVw5bRVc=Tg^VA;awjGxG*Rs1hTwnmsccEQdw|*@>X6aXzeOIaO>Zeig~7 zkdv|G+V|TDJ?p*VrIm9bm6}uIIyOS$E-0eMGb zSVO^Ra>J*RLS>t6;^|UponpZ{r&9C@*q)fnKGn=k7imV>;3MoTZoCk=`yh_5}z+$p2A#yMlXIU}rT<7Yt4#12mA+r3U zLiB?8X0FYl;y`vHK@^t|e_&ae=nR4)QZ5#Qth}yUlY?n`0rQq=RIVKC(b3*QVq&QF zTzLUYV8*s7+Qu5Kw~B-0uJyt|Mpx#UG*k_qwTVbYa{%Q7?Sx2j+z(|H5-IPKMASt+b*EtB#&C1^N~Z)i9WR-zdvZ6JFq2biK)Dc8DL zP>#A4cvVoD8PZ4PjV%xfaj-3qdu=WaWreJbZb26Z17=`@EUMAA2B%T=Ma(M{y+#2R zv%^dsB2IAScEn(Jp4wt&Kc9*+O)^^8qhjEAFsF*nd1@8lc`&F&5BOmd350vd0bw6p zi?BUJAE42!EtWM#b2y|3=OZ>YUfIr?4n9e8;XZcq9d;Kpjk~q6W6;X4KSX#-61aBWLzg z=FQ5Utvd~DXnZv@lz^MQ&V)a*DUbyW5`N8jyuvWD%E2OAqW~==O4Pt4XGYQ1A|eW# z#N5gr9bVZR*hs|v%vAwS$@SyW!i+2V6^6pZwbf5#nd$MmXnu)^FidjC88*kPj12sX z?E|!VCQz`)mShC9xh7ESO+lkHwD~4bxGz1(49fwAmlCb{3S0DC&&;sT)_jHjS@kT< zK)E*^v>FfY9;7_;tbPd<3``Ww-+9g`sv+r)X8LH2;9#V!`JoA&6etvfl~QHg^YBut zywu{N^1xWlpTt4@BKW}Vuh;ph%U5lCD%<4BcmewX^9B9g;c&d1<;UOfqQ=kW*MI+< zOX4@%%XWXf zzh3&~G<_1d-$ZT3){!v;7bM{RgOY0>4g*Ty$sajGc*2)&paLPTW;q>^^)+PNvhZ@+ zKJ2HH;OTw;8ge{6A0MY@^6A6fvnI;s_ISKVCR_#He0cb80{i;^o9*dR(?Jtv@#M*y HpWpruNDr*E literal 0 HcmV?d00001 diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index bf44eb0c53..bee1582760 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -100,12 +100,11 @@ final class SwapSetupViewController: UIViewController, ViewHolder { private func setupNavigationItem() { navigationItem.rightBarButtonItem = UIBarButtonItem( - image: R.image.iconAssetsSettings()?.withRenderingMode(.alwaysTemplate), + image: R.image.iconSwapSettings(), style: .plain, target: self, action: #selector(settingsAction) ) - navigationItem.rightBarButtonItem?.tintColor = R.color.colorIconPrimary() } @objc private func selectPayTokenAction() { diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift index 07f64e69f4..870613bdcc 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift @@ -11,7 +11,7 @@ struct SwapSlippageViewFactory { let wireframe = SwapSlippageWireframe() let amountFormatter = NumberFormatter.amount - let percentFormatter = NumberFormatter.percent + let percentFormatter = NumberFormatter.percentSingle let presenter = SwapSlippagePresenter( interactor: interactor, diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift index bbc3310838..047d32a3ad 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift @@ -11,6 +11,7 @@ final class SwapSlippageViewLayout: ScrollableContainerLayoutView { $0.imageWithTitleView?.titleFont = .regularFootnote $0.imageWithTitleView?.spacingBetweenLabelAndIcon = 4 $0.imageWithTitleView?.layoutType = .horizontalLabelFirst + $0.contentInsets = .init(top: 12, left: 0, bottom: 12, right: 0) } let amountInput = SwapSlippageInputView() @@ -25,7 +26,7 @@ final class SwapSlippageViewLayout: ScrollableContainerLayoutView { slippageButton, FlexibleSpaceView() ]) - addArrangedSubview(title, spacingAfter: 12) + addArrangedSubview(title) slippageButton.setContentHuggingPriority(.low, for: .horizontal) addArrangedSubview(amountInput) diff --git a/novawalletTests/Modules/SwapSlippage/SwapSlippageTests.swift b/novawalletTests/Modules/SwapSlippage/SwapSlippageTests.swift deleted file mode 100644 index ab96eccc16..0000000000 --- a/novawalletTests/Modules/SwapSlippage/SwapSlippageTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -import XCTest - -class SwapSlippageTests: XCTestCase { - - override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() { - XCTFail("Did you forget to add tests?") - } -} From ae7aa3e5e52a01837d057aee3d99f0b3fa4ecdcd Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 18 Oct 2023 11:18:05 +0300 Subject: [PATCH 043/204] add fee asset --- Podfile.lock | 2 +- .../Swaps/Setup/SwapSetupInteractor.swift | 61 ++++++---- .../Swaps/Setup/SwapSetupPresenter.swift | 109 +++++++++++------- .../Swaps/Setup/SwapSetupProtocols.swift | 1 + 4 files changed, 110 insertions(+), 63 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 9500b91c99..7581bee044 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -305,4 +305,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f37e3724d47617fb7ce7ed5e0a583491617b5899 -COCOAPODS: 1.12.1 +COCOAPODS: 1.13.0 diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 46236fcf1e..2fc8b2be63 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -19,9 +19,11 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning private var runtimeOperationCall: CancellableCall? private var extrinsicService: ExtrinsicServiceProtocol? - private var payPriceProvider: StreamableProvider? - private var receivePriceProvider: StreamableProvider? - private var balanceProvider: StreamableProvider? + private var payAssetPriceProvider: StreamableProvider? + private var receiveAssetPriceProvider: StreamableProvider? + private var feeAssetPriceProvider: StreamableProvider? + private var payAssetBalanceProvider: StreamableProvider? + private var feeAssetBalanceProvider: StreamableProvider? init( assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, @@ -143,35 +145,54 @@ extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { } func update(receiveChainAsset: ChainAsset?) { - clear(streamableProvider: &receivePriceProvider) + clear(streamableProvider: &receiveAssetPriceProvider) if let receiveChainAsset = receiveChainAsset { - receivePriceProvider = priceSubscription(chainAsset: receiveChainAsset) + receiveAssetPriceProvider = priceSubscription(chainAsset: receiveChainAsset) } } func update(payChainAsset: ChainAsset?) { - clear(streamableProvider: &payPriceProvider) - clear(streamableProvider: &balanceProvider) + guard let payChainAsset = payChainAsset else { + extrinsicService = nil + presenter?.didReceive(payAccountId: nil) + return + } - if let payChainAsset = payChainAsset { - payPriceProvider = priceSubscription(chainAsset: payChainAsset) - balanceProvider = assetBalanceSubscription(chainAsset: payChainAsset) + if payAssetPriceProvider !== feeAssetPriceProvider { + clear(streamableProvider: &payAssetPriceProvider) + payAssetPriceProvider = priceSubscription(chainAsset: payChainAsset) + } - if let chainAccount = chainAccountResponse(for: payChainAsset) { - extrinsicService = extrinsicServiceFactory.createService( - account: chainAccount, - chain: payChainAsset.chain - ) - presenter?.didReceive(payAccountId: chainAccount.accountId) - } else { - presenter?.didReceive(payAccountId: nil) - } + if payAssetBalanceProvider !== feeAssetBalanceProvider { + clear(streamableProvider: &payAssetBalanceProvider) + payAssetBalanceProvider = assetBalanceSubscription(chainAsset: payChainAsset) + } + + if let chainAccount = chainAccountResponse(for: payChainAsset) { + extrinsicService = extrinsicServiceFactory.createService( + account: chainAccount, + chain: payChainAsset.chain + ) + presenter?.didReceive(payAccountId: chainAccount.accountId) } else { - extrinsicService = nil presenter?.didReceive(payAccountId: nil) } } + func update(feeChainAsset: ChainAsset?) { + guard let feeChainAsset = feeChainAsset else { + return + } + if feeAssetPriceProvider !== payAssetPriceProvider { + clear(streamableProvider: &feeAssetPriceProvider) + feeAssetPriceProvider = priceSubscription(chainAsset: feeChainAsset) + } + if feeAssetBalanceProvider !== payAssetBalanceProvider { + clear(streamableProvider: &feeAssetBalanceProvider) + feeAssetBalanceProvider = assetBalanceSubscription(chainAsset: feeChainAsset) + } + } + func calculateFee( args: AssetConversion.CallArgs ) { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 75fd5d2bb5..2971d17d1d 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -8,11 +8,15 @@ final class SwapSetupPresenter { let interactor: SwapSetupInteractorInputProtocol let viewModelFactory: SwapsSetupViewModelFactoryProtocol - private var assetBalance: AssetBalance? + private var payAssetBalance: AssetBalance? + private var feeAssetBalance: AssetBalance? private var payChainAsset: ChainAsset? + private var receiveChainAsset: ChainAsset? + private var feeChainAsset: ChainAsset? private var payAssetPriceData: PriceData? private var receiveAssetPriceData: PriceData? - private var receiveChainAsset: ChainAsset? + private var feeAssetPriceData: PriceData? + private var payAmountInput: AmountInputResult? private var receiveAmountInput: Decimal? private var fee: BigUInt? @@ -25,7 +29,7 @@ final class SwapSetupPresenter { private var feeIdentifier: String? private var accountId: AccountId? - private var splippage: BigRational = .percent(of: 1) + private var slippage: BigRational = .percent(of: 1) init( interactor: SwapSetupInteractorInputProtocol, @@ -55,7 +59,7 @@ final class SwapSetupPresenter { private func providePayTitle() { let payTitleViewModel = viewModelFactory.payTitleViewModel( assetDisplayInfo: payChainAsset?.assetDisplayInfo, - maxValue: assetBalance?.transferable, + maxValue: payAssetBalance?.transferable, locale: selectedLocale ) view?.didReceiveTitle(payViewModel: payTitleViewModel) @@ -135,28 +139,10 @@ final class SwapSetupPresenter { } private func getPayAmount(for input: AmountInputResult?) -> Decimal? { - guard - let input = input, - let payChainAsset = payChainAsset else { - return nil - } - let includedFee: BigUInt - switch input { - case .rate: - includedFee = fee ?? 0 - case .absolute: - includedFee = 0 - } - - let transferable = assetBalance?.transferable ?? 0 - let balance = max(transferable - includedFee, 0) - guard let balanceDecimal = Decimal.fromSubstrateAmount( - balance, - precision: payChainAsset.asset.displayInfo.assetPrecision - ) else { + guard let input = input, let balanceMinusFee = balanceMinusFee() else { return nil } - return input.absoluteValue(from: balanceDecimal) + return input.absoluteValue(from: balanceMinusFee) } private func providePayAssetViews() { @@ -196,7 +182,7 @@ final class SwapSetupPresenter { } private func provideFeeViewModel() { - guard let payChainAsset = payChainAsset, receiveChainAsset != nil else { + guard quoteArgs != nil, let feeChainAsset = feeChainAsset else { return } guard let fee = fee else { @@ -205,8 +191,8 @@ final class SwapSetupPresenter { } let viewModel = viewModelFactory.feeViewModel( amount: fee, - assetDisplayInfo: payChainAsset.assetDisplayInfo, - priceData: payAssetPriceData, + assetDisplayInfo: feeChainAsset.assetDisplayInfo, + priceData: feeAssetPriceData, locale: selectedLocale ) @@ -225,7 +211,7 @@ final class SwapSetupPresenter { amountOut: quote.amountOut, receiver: accountId, direction: quoteArgs.direction, - slippage: splippage + slippage: slippage ) guard args.identifier != feeIdentifier else { @@ -294,6 +280,24 @@ final class SwapSetupPresenter { provideRateViewModel() provideFeeViewModel() } + + private func balanceMinusFee() -> Decimal? { + guard let payChainAsset = payChainAsset, let feeChainAsset = feeChainAsset else { + return nil + } + let balanceValue = payAssetBalance?.transferable ?? 0 + let feeValue = payChainAsset == feeChainAsset ? fee : 0 + + let precision = Int16(feeChainAsset.asset.precision) + + guard + let balance = Decimal.fromSubstrateAmount(balanceValue, precision: precision), + let fee = Decimal.fromSubstrateAmount(feeValue ?? 0, precision: precision) else { + return 0 + } + + return balance - fee + } } extension SwapSetupPresenter: SwapSetupPresenterProtocol { @@ -308,6 +312,10 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { func selectPayToken() { wireframe.showPayTokenSelection(from: view, chainAsset: receiveChainAsset) { [weak self] chainAsset in self?.payChainAsset = chainAsset + // TODO: select fee asset + self?.feeChainAsset = chainAsset.chain.utilityAsset().map { + ChainAsset(chain: chainAsset.chain, asset: $0) + } self?.providePayAssetViews() self?.provideButtonState() self?.refreshQuote(direction: .sell, forceUpdate: false) @@ -421,13 +429,17 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { self?.estimateFee() } case let .assetBalance(_, chainAssetId, accountId): - guard accountId == self.accountId, - let payChainAsset = payChainAsset, - payChainAsset.chainAssetId == chainAssetId else { - return - } - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.interactor.update(payChainAsset: payChainAsset) + switch chainAssetId { + case payChainAsset?.chainAssetId: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.payChainAsset.map { self?.interactor.update(payChainAsset: $0) } + } + case feeChainAsset?.chainAssetId: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.feeChainAsset.map { self?.interactor.update(feeChainAsset: $0) } + } + default: + break } } } @@ -474,12 +486,18 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { } func didReceive(price: PriceData?, priceId: AssetModel.PriceId) { - if payChainAsset?.asset.priceId == priceId { + switch priceId { + case payChainAsset?.asset.priceId: payAssetPriceData = price providePayInputPriceViewModel() - } else if receiveChainAsset?.asset.priceId == priceId { + case receiveChainAsset?.asset.priceId: receiveAssetPriceData = price provideReceiveInputPriceViewModel() + case feeChainAsset?.asset.priceId: + feeAssetPriceData = price + provideFeeViewModel() + default: + break } } @@ -487,12 +505,19 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { accountId = payAccountId } - func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId: AccountId) { - guard accountId == self.accountId, payChainAsset?.chainAssetId == chainAsset else { - return + func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId _: AccountId) { + if chainAsset == payChainAsset?.chainAssetId { + payAssetBalance = balance + providePayTitle() + } + if chainAsset == feeChainAsset?.chainAssetId { + feeAssetBalance = balance + if case let .rate = payAmountInput { + providePayInputPriceViewModel() + providePayAmountInputViewModel() + provideButtonState() + } } - assetBalance = balance - providePayTitle() } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 2454a7ec2b..a35ce39d96 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -34,6 +34,7 @@ protocol SwapSetupInteractorInputProtocol: AnyObject { func setup() func update(receiveChainAsset: ChainAsset?) func update(payChainAsset: ChainAsset?) + func update(feeChainAsset: ChainAsset?) func calculateQuote(for args: AssetConversion.QuoteArgs) func calculateFee(args: AssetConversion.CallArgs) } From 2fddff97ef86ebe3bd18603155c155c40044bd95 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 18 Oct 2023 11:47:25 +0300 Subject: [PATCH 044/204] bugfixes --- .../TitleDetailsSheetViewFactory.swift | 2 +- .../Swaps/Setup/SwapSetupInteractor.swift | 38 +++++++++---------- .../Swaps/Setup/SwapSetupPresenter.swift | 15 ++++---- .../Swaps/Setup/SwapSetupWireframe.swift | 2 +- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/novawallet/Modules/MessageSheet/TitleDetails/TitleDetailsSheetViewFactory.swift b/novawallet/Modules/MessageSheet/TitleDetails/TitleDetailsSheetViewFactory.swift index 4415716417..1eb659c27a 100644 --- a/novawallet/Modules/MessageSheet/TitleDetails/TitleDetailsSheetViewFactory.swift +++ b/novawallet/Modules/MessageSheet/TitleDetails/TitleDetailsSheetViewFactory.swift @@ -25,7 +25,7 @@ struct TitleDetailsSheetViewFactory { return view } - static func createSelfSizedView( + static func createContentSizedView( from viewModel: TitleDetailsSheetViewModel, allowsSwipeDown: Bool = true ) -> MessageSheetViewProtocol { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 2fc8b2be63..5aa3cdb899 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -129,10 +129,17 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning operationQueue.addOperation(runtimeCoderFactoryOperation) } - func chainAccountResponse(for chainAsset: ChainAsset) -> ChainAccountResponse? { + private func chainAccountResponse(for chainAsset: ChainAsset) -> ChainAccountResponse? { let metaChainAccountResponse = selectedAccount.fetchMetaChainAccount(for: chainAsset.chain.accountRequest()) return metaChainAccountResponse?.chainAccount } + + private func providersEqual(_ provider1: StreamableProvider?, _ provider2: StreamableProvider?) -> Bool { + if provider1 == nil, provider2 == nil { + return false + } + return provider1 === provider2 + } } extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { @@ -152,44 +159,37 @@ extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { } func update(payChainAsset: ChainAsset?) { - guard let payChainAsset = payChainAsset else { - extrinsicService = nil - presenter?.didReceive(payAccountId: nil) - return - } - - if payAssetPriceProvider !== feeAssetPriceProvider { + if !providersEqual(payAssetPriceProvider, feeAssetPriceProvider) { clear(streamableProvider: &payAssetPriceProvider) - payAssetPriceProvider = priceSubscription(chainAsset: payChainAsset) + payAssetPriceProvider = payChainAsset.map { priceSubscription(chainAsset: $0) } ?? nil } - if payAssetBalanceProvider !== feeAssetBalanceProvider { + if !providersEqual(payAssetBalanceProvider, feeAssetBalanceProvider) { clear(streamableProvider: &payAssetBalanceProvider) - payAssetBalanceProvider = assetBalanceSubscription(chainAsset: payChainAsset) + payAssetBalanceProvider = payChainAsset.map { assetBalanceSubscription(chainAsset: $0) } ?? nil } - if let chainAccount = chainAccountResponse(for: payChainAsset) { + if let payChainAsset = payChainAsset, + let chainAccount = chainAccountResponse(for: payChainAsset) { extrinsicService = extrinsicServiceFactory.createService( account: chainAccount, chain: payChainAsset.chain ) presenter?.didReceive(payAccountId: chainAccount.accountId) } else { + extrinsicService = nil presenter?.didReceive(payAccountId: nil) } } func update(feeChainAsset: ChainAsset?) { - guard let feeChainAsset = feeChainAsset else { - return - } - if feeAssetPriceProvider !== payAssetPriceProvider { + if !providersEqual(feeAssetPriceProvider, payAssetPriceProvider) { clear(streamableProvider: &feeAssetPriceProvider) - feeAssetPriceProvider = priceSubscription(chainAsset: feeChainAsset) + feeAssetPriceProvider = feeChainAsset.map { priceSubscription(chainAsset: $0) } ?? nil } - if feeAssetBalanceProvider !== payAssetBalanceProvider { + if !providersEqual(feeAssetBalanceProvider, payAssetBalanceProvider) { clear(streamableProvider: &feeAssetBalanceProvider) - feeAssetBalanceProvider = assetBalanceSubscription(chainAsset: feeChainAsset) + feeAssetBalanceProvider = feeChainAsset.map { assetBalanceSubscription(chainAsset: $0) } ?? nil } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 2971d17d1d..eb3e2dc793 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -282,13 +282,13 @@ final class SwapSetupPresenter { } private func balanceMinusFee() -> Decimal? { - guard let payChainAsset = payChainAsset, let feeChainAsset = feeChainAsset else { + guard let payChainAsset = payChainAsset else { return nil } let balanceValue = payAssetBalance?.transferable ?? 0 let feeValue = payChainAsset == feeChainAsset ? fee : 0 - let precision = Int16(feeChainAsset.asset.precision) + let precision = Int16(payChainAsset.asset.precision) guard let balance = Decimal.fromSubstrateAmount(balanceValue, precision: precision), @@ -486,18 +486,17 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { } func didReceive(price: PriceData?, priceId: AssetModel.PriceId) { - switch priceId { - case payChainAsset?.asset.priceId: + if priceId == payChainAsset?.asset.priceId { payAssetPriceData = price providePayInputPriceViewModel() - case receiveChainAsset?.asset.priceId: + } + if priceId == receiveChainAsset?.asset.priceId { receiveAssetPriceData = price provideReceiveInputPriceViewModel() - case feeChainAsset?.asset.priceId: + } + if priceId == feeChainAsset?.asset.priceId { feeAssetPriceData = price provideFeeViewModel() - default: - break } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index bb28b6d2ab..48a6923f63 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -61,7 +61,7 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { secondaryAction: nil ) - let bottomSheet = TitleDetailsSheetViewFactory.createSelfSizedView(from: viewModel) + let bottomSheet = TitleDetailsSheetViewFactory.createContentSizedView(from: viewModel) let factory = ModalSheetPresentationFactory(configuration: ModalSheetPresentationConfiguration.nova) From 3a3279ad266e18ed0081369212f9a9b984de2955 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 18 Oct 2023 12:11:44 +0300 Subject: [PATCH 045/204] settings state --- novawallet.xcodeproj/project.pbxproj | 8 ++++---- .../Modules/Swaps/Setup/SwapSetupPresenter.swift | 7 +++++++ .../Modules/Swaps/Setup/SwapSetupProtocols.swift | 3 ++- .../Modules/Swaps/Setup/SwapSetupViewController.swift | 4 ++++ .../{Percent.swift => SlippagePercentViewModel.swift} | 2 +- .../Swaps/Slippage/SwapSlippageInputView.swift | 11 +++++++---- .../Swaps/Slippage/SwapSlippagePresenter.swift | 4 ++-- .../Swaps/Slippage/SwapSlippageProtocols.swift | 4 ++-- .../Swaps/Slippage/SwapSlippageViewController.swift | 4 ++-- 9 files changed, 31 insertions(+), 16 deletions(-) rename novawallet/Modules/Swaps/Slippage/Model/{Percent.swift => SlippagePercentViewModel.swift} (66%) diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 9c430f6c23..e7cd117784 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -708,7 +708,7 @@ 77740BBE2AD4A7F500E8C06F /* SwapDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */; }; 77740BC02AD4A80D00E8C06F /* SwapRateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */; }; 77740BC42AD8145500E8C06F /* SwapSlippageInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BC32AD8145500E8C06F /* SwapSlippageInputView.swift */; }; - 77740BC62AD849D100E8C06F /* Percent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BC52AD849D100E8C06F /* Percent.swift */; }; + 77740BC62AD849D100E8C06F /* SlippagePercentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BC52AD849D100E8C06F /* SlippagePercentViewModel.swift */; }; 77799ADD2A74219A00B7E564 /* ButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799ADC2A74219A00B7E564 /* ButtonState.swift */; }; 77799ADF2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799ADE2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift */; }; 77799AE52A792AE700B7E564 /* StakingTypeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799AE42A792AE700B7E564 /* StakingTypeViewModel.swift */; }; @@ -4722,7 +4722,7 @@ 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapDetailsView.swift; sourceTree = ""; }; 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRateView.swift; sourceTree = ""; }; 77740BC32AD8145500E8C06F /* SwapSlippageInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapSlippageInputView.swift; sourceTree = ""; }; - 77740BC52AD849D100E8C06F /* Percent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Percent.swift; sourceTree = ""; }; + 77740BC52AD849D100E8C06F /* SlippagePercentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlippagePercentViewModel.swift; sourceTree = ""; }; 77799ADC2A74219A00B7E564 /* ButtonState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonState.swift; sourceTree = ""; }; 77799ADE2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoRelaychainWireframe.swift; sourceTree = ""; }; 77799AE42A792AE700B7E564 /* StakingTypeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingTypeViewModel.swift; sourceTree = ""; }; @@ -9593,7 +9593,7 @@ 7752E16B2AD878A4006E2F92 /* Model */ = { isa = PBXGroup; children = ( - 77740BC52AD849D100E8C06F /* Percent.swift */, + 77740BC52AD849D100E8C06F /* SlippagePercentViewModel.swift */, ); path = Model; sourceTree = ""; @@ -19670,7 +19670,7 @@ 84B018B026E0450F00C75E28 /* ValidatorStateView.swift in Sources */, AEE0C43A272A8B1F009F9AD5 /* AddChainAccount+AccountCreateWireframe.swift in Sources */, 0C7C9B992ABFF355009A0362 /* String+Html.swift in Sources */, - 77740BC62AD849D100E8C06F /* Percent.swift in Sources */, + 77740BC62AD849D100E8C06F /* SlippagePercentViewModel.swift in Sources */, 0C7E7FAB2A9F27FB00596628 /* NominationPoolsRedeemCall.swift in Sources */, 84038FF226FFBE1900C73F3F /* JsonLocalSubscriptionHandler.swift in Sources */, 842898D1265A955A002D5D65 /* ImageViewModel.swift in Sources */, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 3c9565a216..1f7d3f1884 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -136,6 +136,10 @@ final class SwapSetupPresenter { view?.didReceiveAmount(receiveInputViewModel: amountInputViewModel) } + private func provideSettingsState() { + view?.didReceiveSettingsState(isAvailable: payChainAsset != nil) + } + private func absoluteValue(for input: AmountInputResult?) -> Decimal? { guard let input = input, @@ -291,6 +295,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideReceiveAssetViews() provideDetailsViewModel(isAvailable: false) provideButtonState() + provideSettingsState() // TODO: get from settings slippage = .percent(of: 1) interactor.setup() @@ -301,6 +306,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { self?.payChainAsset = chainAsset self?.providePayAssetViews() self?.provideButtonState() + self?.provideSettingsState() self?.refreshQuote(direction: .sell) self?.interactor.update(payChainAsset: chainAsset) } @@ -333,6 +339,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { providePayAssetViews() provideReceiveAssetViews() provideButtonState() + provideSettingsState() refreshQuote(direction: quoteArgs?.direction ?? .sell) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 7b24002145..16c0fd6e43 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -13,6 +13,7 @@ protocol SwapSetupViewProtocol: ControllerBackedProtocol { func didReceiveRate(viewModel: LoadableViewModelState) func didReceiveNetworkFee(viewModel: LoadableViewModelState) func didReceiveDetailsState(isAvailable: Bool) + func didReceiveSettingsState(isAvailable: Bool) } protocol SwapSetupPresenterProtocol: AnyObject { @@ -34,7 +35,7 @@ protocol SwapSetupInteractorInputProtocol: AnyObject { func update(receiveChainAsset: ChainAsset) func update(payChainAsset: ChainAsset) func calculateQuote(for args: AssetConversion.QuoteArgs) - func calculateFee(args: AssetConversion.CallArgs) -> Void + func calculateFee(args: AssetConversion.CallArgs) } protocol SwapSetupInteractorOutputProtocol: AnyObject { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index bee1582760..ef283b4454 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -214,6 +214,10 @@ extension SwapSetupViewController: SwapSetupViewProtocol { func didReceiveNetworkFee(viewModel: LoadableViewModelState) { rootView.networkFeeCell.bind(loadableViewModel: viewModel) } + + func didReceiveSettingsState(isAvailable: Bool) { + navigationItem.rightBarButtonItem?.isEnabled = isAvailable + } } extension SwapSetupViewController: Localizable { diff --git a/novawallet/Modules/Swaps/Slippage/Model/Percent.swift b/novawallet/Modules/Swaps/Slippage/Model/SlippagePercentViewModel.swift similarity index 66% rename from novawallet/Modules/Swaps/Slippage/Model/Percent.swift rename to novawallet/Modules/Swaps/Slippage/Model/SlippagePercentViewModel.swift index b22ac5c347..2879f2dbac 100644 --- a/novawallet/Modules/Swaps/Slippage/Model/Percent.swift +++ b/novawallet/Modules/Swaps/Slippage/Model/SlippagePercentViewModel.swift @@ -1,6 +1,6 @@ import Foundation -struct Percent { +struct SlippagePercentViewModel { let value: Decimal let title: String } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift index 18d2d59a78..7ac3613b3c 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift @@ -2,7 +2,7 @@ import SnapKit import SoraUI protocol SwapSlippageInputViewDelegateProtocol: AnyObject { - func didSelect(percent: Percent, sender: Any?) + func didSelect(percent: SlippagePercentViewModel, sender: Any?) } final class SwapSlippageInputView: BackgroundedContentControl { @@ -45,6 +45,7 @@ final class SwapSlippageInputView: BackgroundedContentControl { weak var delegate: SwapSlippageInputViewDelegateProtocol? private(set) var inputViewModel: AmountInputViewModelProtocol? + private var viewModel: [SlippagePercentViewModel] = [] override init(frame: CGRect) { super.init(frame: frame) @@ -82,6 +83,8 @@ final class SwapSlippageInputView: BackgroundedContentControl { width: symbolLabel.intrinsicContentSize.width, height: symbolLabel.intrinsicContentSize.height ) + } else { + symbolLabel.frame = .zero } if !buttonsStack.isHidden, !buttonsStack.arrangedSubviews.isEmpty { @@ -100,6 +103,8 @@ final class SwapSlippageInputView: BackgroundedContentControl { width: buttonsWidth, height: height ) + } else { + buttonsStack.frame = .zero } backgroundView?.frame = bounds @@ -149,8 +154,6 @@ final class SwapSlippageInputView: BackgroundedContentControl { } } - private var viewModel: [Percent] = [] - private func createButton(title: String) -> RoundedButton { let button = RoundedButton() button.applyAccessoryStyle() @@ -196,7 +199,7 @@ extension SwapSlippageInputView: AmountInputViewModelObserver { } extension SwapSlippageInputView { - func bind(viewModel: [Percent]) { + func bind(viewModel: [SlippagePercentViewModel]) { buttonsStack.arrangedSubviews.forEach { $0.removeFromSuperview() } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index 230faa4885..f7a359fe81 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -73,7 +73,7 @@ final class SwapSlippagePresenter { extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { func setup() { let viewModel = prefilledPercents.map { - Percent( + SlippagePercentViewModel( value: $0, title: title(for: $0 / (percentFormatter.multiplier?.decimalValue ?? 1)) ) @@ -89,7 +89,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { view?.didReceivePreFilledPercents(viewModel: viewModel) } - func select(percent: Percent) { + func select(percent: SlippagePercentViewModel) { amountInput = percent.value provideAmountViewModel() provideResetButtonState() diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift index f9ebc069bd..b0f0c7a079 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift @@ -1,14 +1,14 @@ import Foundation protocol SwapSlippageViewProtocol: ControllerBackedProtocol { - func didReceivePreFilledPercents(viewModel: [Percent]) + func didReceivePreFilledPercents(viewModel: [SlippagePercentViewModel]) func didReceiveInput(viewModel: AmountInputViewModelProtocol) func didReceiveResetState(available: Bool) } protocol SwapSlippagePresenterProtocol: AnyObject { func setup() - func select(percent: Percent) + func select(percent: SlippagePercentViewModel) func updateAmount(_ amount: Decimal?) func apply() func showSlippageInfo() diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift index 90e871e28d..3f755fa27a 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift @@ -100,7 +100,7 @@ final class SwapSlippageViewController: UIViewController, ViewHolder { } extension SwapSlippageViewController: SwapSlippageViewProtocol { - func didReceivePreFilledPercents(viewModel: [Percent]) { + func didReceivePreFilledPercents(viewModel: [SlippagePercentViewModel]) { rootView.amountInput.bind(viewModel: viewModel) } @@ -115,7 +115,7 @@ extension SwapSlippageViewController: SwapSlippageViewProtocol { } extension SwapSlippageViewController: SwapSlippageInputViewDelegateProtocol { - func didSelect(percent: Percent, sender _: Any?) { + func didSelect(percent: SlippagePercentViewModel, sender _: Any?) { presenter.select(percent: percent) } } From 4a65218911c380509a086ada0b43fb2e73c02b1f Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 18 Oct 2023 14:56:01 +0300 Subject: [PATCH 046/204] add error --- novawallet.xcodeproj/project.pbxproj | 4 -- .../Extension/UIKit/Style/UILabel+Style.swift | 5 ++ .../Swaps/Setup/SwapSetupPresenter.swift | 2 +- .../Slippage/SwapSlippageInputView.swift | 32 ++++++++++- .../Slippage/SwapSlippageInteractor.swift | 7 --- .../Slippage/SwapSlippagePresenter.swift | 53 ++++++++++++++----- .../Slippage/SwapSlippageProtocols.swift | 5 +- .../Slippage/SwapSlippageViewController.swift | 11 +++- .../Slippage/SwapSlippageViewFactory.swift | 3 -- .../Slippage/SwapSlippageViewLayout.swift | 12 ++++- novawallet/en.lproj/Localizable.strings | 1 + novawallet/ru.lproj/Localizable.strings | 1 + 12 files changed, 98 insertions(+), 38 deletions(-) delete mode 100644 novawallet/Modules/Swaps/Slippage/SwapSlippageInteractor.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index e7cd117784..6362e8bf89 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -3423,7 +3423,6 @@ 9B6CD060F0EB77C162D90D3E /* ChainAddressDetailsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781FA4C896AF31B4035AFB38 /* ChainAddressDetailsViewFactory.swift */; }; 9BADFCBF3AF5186094DB8D67 /* DAppTxDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB4A14C99D151B41F61F474 /* DAppTxDetailsInteractor.swift */; }; 9C223E4BF19F7314A9E6F1CA /* NominationPoolSearchViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686A91FF92C89FE8937EF5A /* NominationPoolSearchViewLayout.swift */; }; - 9C8C32AFF22AC1165FA7FDDA /* SwapSlippageInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF43098AE15348910BA4627 /* SwapSlippageInteractor.swift */; }; 9D509AD640B01CAB872E0E71 /* NominationPoolSearchViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15AC3897EC736F5096949BBC /* NominationPoolSearchViewFactory.swift */; }; 9D5926790B055C56FB74B282 /* AccountManagementProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B5072E250B7277F605855B3 /* AccountManagementProtocols.swift */; }; 9DE1757D047A4D1E97913774 /* GovernanceUnlockConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D3FE2CE7F9F2836755DBA63 /* GovernanceUnlockConfirmProtocols.swift */; }; @@ -7645,7 +7644,6 @@ AEE5FB1726457AC1002B8FDC /* StakingRewardDestSetupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardDestSetupViewController.swift; sourceTree = ""; }; AEE5FB1926457AE9002B8FDC /* StakingRewardDestSetupProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardDestSetupProtocols.swift; sourceTree = ""; }; AEE5FB1B264A610C002B8FDC /* StakingRewardDestSetupLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardDestSetupLayout.swift; sourceTree = ""; }; - AEF43098AE15348910BA4627 /* SwapSlippageInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapSlippageInteractor.swift; sourceTree = ""; }; AEF50585261EE6230098574D /* PurchaseProviderPickerTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseProviderPickerTableViewCell.swift; sourceTree = ""; }; AEF5058A261EF27B0098574D /* PurchaseProviderPickerTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PurchaseProviderPickerTableViewCell.xib; sourceTree = ""; }; AEF505A82620249F0098574D /* UIColor+HEX.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+HEX.swift"; sourceTree = ""; }; @@ -8971,7 +8969,6 @@ 2AE0E677E64DAC7E93562412 /* SwapSlippageProtocols.swift */, 088C765E5A0F81B96ADE72D8 /* SwapSlippageWireframe.swift */, 0816F2A4A5CC1F111E626188 /* SwapSlippagePresenter.swift */, - AEF43098AE15348910BA4627 /* SwapSlippageInteractor.swift */, F9F3BD600F80ED0426141843 /* SwapSlippageViewController.swift */, 2E965356C7C646CB86BBEBB6 /* SwapSlippageViewLayout.swift */, 28294C13CF8F62D2FE4D0427 /* SwapSlippageViewFactory.swift */, @@ -23063,7 +23060,6 @@ 94A6747402550FB39D2E2BE7 /* SwapSlippageProtocols.swift in Sources */, 2451E27286A176CDA2DC040D /* SwapSlippageWireframe.swift in Sources */, 07D1F0F4FAE24BED8A1CF257 /* SwapSlippagePresenter.swift in Sources */, - 9C8C32AFF22AC1165FA7FDDA /* SwapSlippageInteractor.swift in Sources */, 143F6C9044429A337265DF39 /* SwapSlippageViewController.swift in Sources */, F9CEF01779F811AEEED06C43 /* SwapSlippageViewLayout.swift in Sources */, 80603DA36CD481AE310CDFE1 /* SwapSlippageViewFactory.swift in Sources */, diff --git a/novawallet/Common/Extension/UIKit/Style/UILabel+Style.swift b/novawallet/Common/Extension/UIKit/Style/UILabel+Style.swift index 0de735147e..b38d484afb 100644 --- a/novawallet/Common/Extension/UIKit/Style/UILabel+Style.swift +++ b/novawallet/Common/Extension/UIKit/Style/UILabel+Style.swift @@ -96,6 +96,11 @@ extension UILabel.Style { font: .caption1 ) + static let caption1Negative = UILabel.Style( + textColor: R.color.colorTextNegative(), + font: .caption1 + ) + static let caption2Secondary = UILabel.Style( textColor: R.color.colorTextSecondary(), font: .caption2 diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 1f7d3f1884..9e60fe4a77 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -215,7 +215,7 @@ final class SwapSetupPresenter { guard let quote = quote, let accountId = accountId, let quoteArgs = quoteArgs, - let slippage = slippage else { + let slippage = self.slippage else { return } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift index 7ac3613b3c..5f768f5091 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift @@ -178,12 +178,17 @@ extension SwapSlippageInputView: UITextFieldDelegate { replacementString string: String ) -> Bool { let shouldChangeCharacters = inputViewModel?.didReceiveReplacement(string, for: range) ?? false - updateViewsVisablilty(for: string) + updateViewsVisablilty(for: inputViewModel?.displayAmount) return shouldChangeCharacters } - func textFieldShouldClear(_: UITextField) -> Bool { + func textFieldShouldClear(_ textField: UITextField) -> Bool { updateViewsVisablilty(for: "") + if let text = textField.text { + inputViewModel?.didReceiveReplacement("", for: NSRange(location: 0, length: text.count)) + textField.text = "" + return false + } return true } @@ -220,3 +225,26 @@ extension SwapSlippageInputView { updateViewsVisablilty(for: textField.text) } } + +extension SwapSlippageInputView { + enum Style { + case error + case normal + } + + func apply(style: Style) { + switch style { + case .error: + let color = R.color.colorTextNegative()! + roundedBackgroundView?.strokeWidth = 0.5 + roundedBackgroundView?.strokeColor = color + textField.textColor = color + symbolLabel.textColor = color + case .normal: + roundedBackgroundView?.strokeWidth = 0 + roundedBackgroundView?.strokeColor = R.color.colorActiveBorder()! + textField.textColor = R.color.colorTextPrimary() + symbolLabel.textColor = R.color.colorTextPrimary() + } + } +} diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageInteractor.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageInteractor.swift deleted file mode 100644 index 8d62554d33..0000000000 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageInteractor.swift +++ /dev/null @@ -1,7 +0,0 @@ -import UIKit - -final class SwapSlippageInteractor { - weak var presenter: SwapSlippageInteractorOutputProtocol? -} - -extension SwapSlippageInteractor: SwapSlippageInteractorInputProtocol {} diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index f7a359fe81..b6578a4065 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -5,20 +5,20 @@ import BigInt final class SwapSlippagePresenter { weak var view: SwapSlippageViewProtocol? let wireframe: SwapSlippageWireframeProtocol - let interactor: SwapSlippageInteractorInputProtocol let numberFormatterLocalizable: LocalizableResource let percentFormatterLocalizable: LocalizableResource let completionHandler: (BigRational) -> Void let prefilledPercents: [Decimal] = [0.1, 1, 3] let initPercent: BigRational? let chainAsset: ChainAsset + let minAmount: Decimal = 0.01 + let maxAmount: Decimal = 50 private var percentFormatter: NumberFormatter private var numberFormatter: NumberFormatter private var amountInput: Decimal? init( - interactor: SwapSlippageInteractorInputProtocol, wireframe: SwapSlippageWireframeProtocol, numberFormatterLocalizable: LocalizableResource, percentFormatterLocalizable: LocalizableResource, @@ -27,7 +27,6 @@ final class SwapSlippagePresenter { chainAsset: ChainAsset, completionHandler: @escaping (BigRational) -> Void ) { - self.interactor = interactor self.wireframe = wireframe self.numberFormatterLocalizable = numberFormatterLocalizable self.percentFormatterLocalizable = percentFormatterLocalizable @@ -40,14 +39,15 @@ final class SwapSlippagePresenter { } private func title(for percent: Decimal) -> String { - percentFormatter.stringFromDecimal(percent) ?? "" + let value = percent / (percentFormatter.multiplier?.decimalValue ?? 1) + return percentFormatter.stringFromDecimal(value) ?? "" } - func provideAmountViewModel() { + private func provideAmountViewModel() { let inputViewModel = AmountInputViewModel( symbol: "", amount: amountInput, - limit: 50, + limit: 100, formatter: numberFormatter, inputLocale: selectedLocale, precision: 1 @@ -56,14 +56,38 @@ final class SwapSlippagePresenter { view?.didReceiveInput(viewModel: inputViewModel) } - func provideResetButtonState() { + private func provideResetButtonState() { let amountChanged = amountInput.map { fraction(from: $0) } != initPercent view?.didReceiveResetState(available: amountChanged) } - func fraction(from number: Decimal) -> BigRational { - let decimalNumber = NSDecimalNumber(decimal: number) - let scale = -number.exponent + private func provideErrors() { + if let amountInput = amountInput, amountInput < minAmount || amountInput > maxAmount { + let minAmountString = title(for: minAmount) + let maxAmountString = title(for: maxAmount) + let error = R.string.localizable.swapsSetupSlippageErrorAmountBounds( + minAmountString, + maxAmountString, + preferredLanguages: selectedLocale.rLanguages + ) + view?.didReceiveInput(error: error) + } else { + view?.didReceiveInput(error: nil) + } + } + + private func fraction(from number: Decimal) -> BigRational { + var roundedNumber = Decimal() + var value = number + NSDecimalRound( + &roundedNumber, + &value, + percentFormatter.maximumFractionDigits, + NSDecimalNumber.RoundingMode.plain + ) + + let decimalNumber = NSDecimalNumber(decimal: roundedNumber) + let scale = -roundedNumber.exponent let numerator = decimalNumber.multiplying(byPowerOf10: Int16(scale)).intValue let denominator = Int(truncating: pow(10, scale) as NSNumber) return .init(numerator: BigUInt(numerator), denominator: BigUInt(denominator)) @@ -75,7 +99,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { let viewModel = prefilledPercents.map { SlippagePercentViewModel( value: $0, - title: title(for: $0 / (percentFormatter.multiplier?.decimalValue ?? 1)) + title: title(for: $0) ) } @@ -93,11 +117,13 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { amountInput = percent.value provideAmountViewModel() provideResetButtonState() + provideErrors() } func updateAmount(_ amount: Decimal?) { amountInput = amount provideResetButtonState() + provideErrors() } func showSlippageInfo() { @@ -113,19 +139,18 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { provideAmountViewModel() provideResetButtonState() + provideErrors() } func apply() { if let amountInput = amountInput { let rational = fraction(from: amountInput) completionHandler(rational) + wireframe.close(from: view) } - wireframe.close(from: view) } } -extension SwapSlippagePresenter: SwapSlippageInteractorOutputProtocol {} - extension SwapSlippagePresenter: Localizable { func applyLocalization() { percentFormatter = percentFormatterLocalizable.value(for: selectedLocale) diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift index b0f0c7a079..d4ea44a2bd 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift @@ -3,6 +3,7 @@ import Foundation protocol SwapSlippageViewProtocol: ControllerBackedProtocol { func didReceivePreFilledPercents(viewModel: [SlippagePercentViewModel]) func didReceiveInput(viewModel: AmountInputViewModelProtocol) + func didReceiveInput(error: String?) func didReceiveResetState(available: Bool) } @@ -15,10 +16,6 @@ protocol SwapSlippagePresenterProtocol: AnyObject { func reset() } -protocol SwapSlippageInteractorInputProtocol: AnyObject {} - -protocol SwapSlippageInteractorOutputProtocol: AnyObject {} - protocol SwapSlippageWireframeProtocol: AnyObject { func close(from view: ControllerBackedProtocol?) } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift index 3f755fa27a..d339942a30 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift @@ -49,7 +49,7 @@ final class SwapSlippageViewController: UIViewController, ViewHolder { private func setupHandlers() { rootView.amountInput.delegate = self rootView.actionButton.addTarget(self, action: #selector(applyButtonAction), for: .touchUpInside) - rootView.amountInput.textField.addTarget(self, action: #selector(inputEditingAction), for: .editingChanged) + rootView.amountInput.addTarget(self, action: #selector(inputEditingAction), for: .editingChanged) rootView.slippageButton.addTarget(self, action: #selector(slippageInfoAction), for: .touchUpInside) } @@ -73,7 +73,8 @@ final class SwapSlippageViewController: UIViewController, ViewHolder { } private func updateActionButton() { - rootView.actionButton.isEnabled = rootView.amountInput.inputViewModel?.isValid == true + let inputValid = rootView.amountInput.inputViewModel?.isValid == true + rootView.actionButton.isEnabled = inputValid && rootView.errorLabel.isHidden } @objc private func applyButtonAction() { @@ -112,11 +113,17 @@ extension SwapSlippageViewController: SwapSlippageViewProtocol { func didReceiveResetState(available: Bool) { navigationItem.rightBarButtonItem?.isEnabled = available } + + func didReceiveInput(error: String?) { + rootView.set(error: error) + updateActionButton() + } } extension SwapSlippageViewController: SwapSlippageInputViewDelegateProtocol { func didSelect(percent: SlippagePercentViewModel, sender _: Any?) { presenter.select(percent: percent) + updateActionButton() } } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift index 870613bdcc..ce4f173737 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift @@ -7,14 +7,12 @@ struct SwapSlippageViewFactory { chainAsset: ChainAsset, completionHandler: @escaping (BigRational) -> Void ) -> SwapSlippageViewProtocol? { - let interactor = SwapSlippageInteractor() let wireframe = SwapSlippageWireframe() let amountFormatter = NumberFormatter.amount let percentFormatter = NumberFormatter.percentSingle let presenter = SwapSlippagePresenter( - interactor: interactor, wireframe: wireframe, numberFormatterLocalizable: amountFormatter.localizableResource(), percentFormatterLocalizable: percentFormatter.localizableResource(), @@ -30,7 +28,6 @@ struct SwapSlippageViewFactory { ) presenter.view = view - interactor.presenter = presenter return view } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift index 047d32a3ad..2eda1ec618 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift @@ -20,6 +20,8 @@ final class SwapSlippageViewLayout: ScrollableContainerLayoutView { $0.applyDefaultStyle() } + let errorLabel = UILabel(style: .caption1Negative, textAlignment: .left, numberOfLines: 0) + override func setupLayout() { super.setupLayout() let title = UIView.hStack([ @@ -28,12 +30,14 @@ final class SwapSlippageViewLayout: ScrollableContainerLayoutView { ]) addArrangedSubview(title) slippageButton.setContentHuggingPriority(.low, for: .horizontal) - addArrangedSubview(amountInput) + addArrangedSubview(amountInput, spacingAfter: 8) amountInput.snp.makeConstraints { $0.height.equalTo(48) } + addArrangedSubview(errorLabel) + addSubview(actionButton) actionButton.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) @@ -41,4 +45,10 @@ final class SwapSlippageViewLayout: ScrollableContainerLayoutView { make.height.equalTo(UIConstants.actionHeight) } } + + func set(error: String?) { + errorLabel.text = error + errorLabel.isHidden = error.isNilOrEmpty + amountInput.apply(style: error.isNilOrEmpty ? .normal : .error) + } } diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 25ba84b586..ed5edf18ef 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1393,3 +1393,4 @@ "swaps.receive.token.selection.title" = "Token to receive"; "swaps.setup.settings.title" = "Swap settings"; "swaps.setup.slippage" = "Slippage"; +"swaps.setup.slippage.error.amount.bounds" = "Enter a value between %@ and %@"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 40c23d458f..bbad6290dd 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1394,3 +1394,4 @@ "swaps.receive.token.selection.title" = "Токен для получения"; "swaps.setup.settings.title" = "Настройки обмена"; "swaps.setup.slippage" = "Slippage"; +"swaps.setup.slippage.error.amount.bounds" = "Введите значение между %@ и %@"; From 587c6b5616bd14b13e9f9d719f98832b5a6e9c64 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 18 Oct 2023 16:17:30 +0300 Subject: [PATCH 047/204] bugfixes --- .../Slippage/SwapSlippageInputView.swift | 2 +- .../Slippage/SwapSlippagePresenter.swift | 47 +++++++++---------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift index 5f768f5091..0e58a84b75 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift @@ -241,7 +241,7 @@ extension SwapSlippageInputView { textField.textColor = color symbolLabel.textColor = color case .normal: - roundedBackgroundView?.strokeWidth = 0 + roundedBackgroundView?.strokeWidth = textField.isFirstResponder ? 0.5 : 0.0 roundedBackgroundView?.strokeColor = R.color.colorActiveBorder()! textField.textColor = R.color.colorTextPrimary() symbolLabel.textColor = R.color.colorTextPrimary() diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index b6578a4065..f85fa4a06a 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -39,8 +39,21 @@ final class SwapSlippagePresenter { } private func title(for percent: Decimal) -> String { - let value = percent / (percentFormatter.multiplier?.decimalValue ?? 1) - return percentFormatter.stringFromDecimal(value) ?? "" + percentFormatter.stringFromDecimal(value(for: percent)) ?? "" + } + + private func value(for percent: Decimal) -> Decimal { + percent / (percentFormatter.multiplier?.decimalValue ?? 1) + } + + private func initialPercent() -> Decimal? { + if let percent = initPercent, percent.denominator != 0 { + let numerator = percent.numerator.decimal(precision: 0) + let denominator = percent.denominator.decimal(precision: 0) + return numerator / denominator + } else { + return nil + } } private func provideAmountViewModel() { @@ -57,7 +70,7 @@ final class SwapSlippagePresenter { } private func provideResetButtonState() { - let amountChanged = amountInput.map { fraction(from: $0) } != initPercent + let amountChanged = amountInput != initialPercent() view?.didReceiveResetState(available: amountChanged) } @@ -77,17 +90,12 @@ final class SwapSlippagePresenter { } private func fraction(from number: Decimal) -> BigRational { - var roundedNumber = Decimal() - var value = number - NSDecimalRound( - &roundedNumber, - &value, - percentFormatter.maximumFractionDigits, - NSDecimalNumber.RoundingMode.plain - ) + let decimalNumber = NSDecimalNumber(decimal: number) + guard decimalNumber.doubleValue.remainder(dividingBy: 1) != 0 else { + return .init(numerator: BigUInt(decimalNumber.intValue), denominator: 1) + } - let decimalNumber = NSDecimalNumber(decimal: roundedNumber) - let scale = -roundedNumber.exponent + let scale = -number.exponent let numerator = decimalNumber.multiplying(byPowerOf10: Int16(scale)).intValue let denominator = Int(truncating: pow(10, scale) as NSNumber) return .init(numerator: BigUInt(numerator), denominator: BigUInt(denominator)) @@ -103,11 +111,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { ) } - if let percent = initPercent, percent.denominator != 0 { - let numerator = percent.numerator.decimal(precision: chainAsset.asset.precision) - let denominator = percent.denominator.decimal(precision: chainAsset.asset.precision) - amountInput = numerator / denominator - } + amountInput = initialPercent() provideResetButtonState() provideAmountViewModel() view?.didReceivePreFilledPercents(viewModel: viewModel) @@ -131,12 +135,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { } func reset() { - if let initPercent = initPercent, initPercent.denominator != 0 { - amountInput = initPercent.numerator.decimal(precision: chainAsset.asset.precision) / initPercent.denominator.decimal(precision: chainAsset.asset.precision) - } else { - amountInput = nil - } - + amountInput = initialPercent() provideAmountViewModel() provideResetButtonState() provideErrors() From 4017c9b21bbba08cbaf297e030cd448c7d81977e Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 18 Oct 2023 16:46:56 +0300 Subject: [PATCH 048/204] add warning --- .../Slippage/SwapSlippagePresenter.swift | 34 ++++++++++++++++--- .../Slippage/SwapSlippageProtocols.swift | 1 + .../Slippage/SwapSlippageViewController.swift | 4 +++ .../Slippage/SwapSlippageViewLayout.swift | 13 ++++++- novawallet/en.lproj/Localizable.strings | 2 ++ novawallet/ru.lproj/Localizable.strings | 2 ++ 6 files changed, 50 insertions(+), 6 deletions(-) diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index f85fa4a06a..b819965bc7 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -11,8 +11,8 @@ final class SwapSlippagePresenter { let prefilledPercents: [Decimal] = [0.1, 1, 3] let initPercent: BigRational? let chainAsset: ChainAsset - let minAmount: Decimal = 0.01 - let maxAmount: Decimal = 50 + let amountRestriction: (lower: Decimal, upper: Decimal) = (lower: 0.01, upper: 50) + let amountRecommendation: (lower: Decimal, upper: Decimal) = (lower: 0.1, upper: 2.3) private var percentFormatter: NumberFormatter private var numberFormatter: NumberFormatter @@ -75,9 +75,10 @@ final class SwapSlippagePresenter { } private func provideErrors() { - if let amountInput = amountInput, amountInput < minAmount || amountInput > maxAmount { - let minAmountString = title(for: minAmount) - let maxAmountString = title(for: maxAmount) + if let amountInput = amountInput, + amountInput < amountRestriction.lower || amountInput > amountRestriction.upper { + let minAmountString = title(for: amountRestriction.lower) + let maxAmountString = title(for: amountRestriction.upper) let error = R.string.localizable.swapsSetupSlippageErrorAmountBounds( minAmountString, maxAmountString, @@ -89,6 +90,26 @@ final class SwapSlippagePresenter { } } + private func provideWarnings() { + guard let amountInput = amountInput, amountInput > 0 else { + view?.didReceiveInput(warning: nil) + return + } + if amountInput <= amountRecommendation.lower { + let warning = R.string.localizable.swapsSetupSlippageWarningLowAmount( + preferredLanguages: selectedLocale.rLanguages + ) + view?.didReceiveInput(warning: warning) + } else if amountInput >= amountRecommendation.upper { + let warning = R.string.localizable.swapsSetupSlippageWarningHighAmount( + preferredLanguages: selectedLocale.rLanguages + ) + view?.didReceiveInput(warning: warning) + } else { + view?.didReceiveInput(warning: nil) + } + } + private func fraction(from number: Decimal) -> BigRational { let decimalNumber = NSDecimalNumber(decimal: number) guard decimalNumber.doubleValue.remainder(dividingBy: 1) != 0 else { @@ -122,12 +143,14 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { provideAmountViewModel() provideResetButtonState() provideErrors() + provideWarnings() } func updateAmount(_ amount: Decimal?) { amountInput = amount provideResetButtonState() provideErrors() + provideWarnings() } func showSlippageInfo() { @@ -139,6 +162,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { provideAmountViewModel() provideResetButtonState() provideErrors() + provideWarnings() } func apply() { diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift index d4ea44a2bd..fca6c7c17c 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift @@ -4,6 +4,7 @@ protocol SwapSlippageViewProtocol: ControllerBackedProtocol { func didReceivePreFilledPercents(viewModel: [SlippagePercentViewModel]) func didReceiveInput(viewModel: AmountInputViewModelProtocol) func didReceiveInput(error: String?) + func didReceiveInput(warning: String?) func didReceiveResetState(available: Bool) } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift index d339942a30..6a99f90c23 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift @@ -118,6 +118,10 @@ extension SwapSlippageViewController: SwapSlippageViewProtocol { rootView.set(error: error) updateActionButton() } + + func didReceiveInput(warning: String?) { + rootView.set(warning: warning) + } } extension SwapSlippageViewController: SwapSlippageInputViewDelegateProtocol { diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift index 2eda1ec618..438da94941 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift @@ -21,6 +21,7 @@ final class SwapSlippageViewLayout: ScrollableContainerLayoutView { } let errorLabel = UILabel(style: .caption1Negative, textAlignment: .left, numberOfLines: 0) + private var warningView: InlineAlertView? override func setupLayout() { super.setupLayout() @@ -36,7 +37,8 @@ final class SwapSlippageViewLayout: ScrollableContainerLayoutView { $0.height.equalTo(48) } - addArrangedSubview(errorLabel) + errorLabel.isHidden = true + addArrangedSubview(errorLabel, spacingAfter: 8) addSubview(actionButton) actionButton.snp.makeConstraints { make in @@ -51,4 +53,13 @@ final class SwapSlippageViewLayout: ScrollableContainerLayoutView { errorLabel.isHidden = error.isNilOrEmpty amountInput.apply(style: error.isNilOrEmpty ? .normal : .error) } + + func set(warning: String?) { + applyWarning( + on: &warningView, + after: errorLabel, + text: warning, + spacing: 16 + ) + } } diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index ed5edf18ef..39b5897f79 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1394,3 +1394,5 @@ "swaps.setup.settings.title" = "Swap settings"; "swaps.setup.slippage" = "Slippage"; "swaps.setup.slippage.error.amount.bounds" = "Enter a value between %@ and %@"; +"swaps.setup.slippage.warning.low.amount" = "Transaction might be reverted because of low slippage tolerance."; +"swaps.setup.slippage.warning.high.amount" = "Transaction might be frontrun because of high slippage."; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index bbad6290dd..7001cdcff5 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1395,3 +1395,5 @@ "swaps.setup.settings.title" = "Настройки обмена"; "swaps.setup.slippage" = "Slippage"; "swaps.setup.slippage.error.amount.bounds" = "Введите значение между %@ и %@"; +"swaps.setup.slippage.warning.low.amount" = "Транзакция может быть отменена из-за низкого значения проскальзывания."; +"swaps.setup.slippage.warning.high.amount" = "Транзакция может быть приостановлена из-за высокого значения проскальзывания."; From 922c957b90f92c23eab2bf62dfb8b41a3d1f6ded Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 19 Oct 2023 12:18:38 +0300 Subject: [PATCH 049/204] validator protocol --- Podfile.lock | 2 +- novawallet.xcodeproj/project.pbxproj | 16 ++++ .../Swaps/Setup/SwapSetupPresenter.swift | 73 +++++++++++++++++++ .../Validation/SwapDataValidatorFactory.swift | 23 ++++++ .../Validation/SwapErrorPresentable.swift | 7 ++ 5 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift create mode 100644 novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift diff --git a/Podfile.lock b/Podfile.lock index 9500b91c99..7581bee044 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -305,4 +305,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f37e3724d47617fb7ce7ed5e0a583491617b5899 -COCOAPODS: 1.12.1 +COCOAPODS: 1.13.0 diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 5ebc3a5f67..31104104ee 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -676,6 +676,8 @@ 770F578B2A8A48FF005FD7C1 /* ButtonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770F578A2A8A48FF005FD7C1 /* ButtonViewModel.swift */; }; 77171CA82A98BBA10032B387 /* NominationPoolErrorPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77171CA72A98BBA10032B387 /* NominationPoolErrorPresentable.swift */; }; 77171CAA2A98BC420032B387 /* NominationPoolDataValidatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77171CA92A98BC420032B387 /* NominationPoolDataValidatorFactory.swift */; }; + 7719018C2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719018B2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift */; }; + 7719018E2AE0E71F00D9C918 /* SwapErrorPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719018D2AE0E71F00D9C918 /* SwapErrorPresentable.swift */; }; 77204EA42A1CD59100BBDE4A /* GenericBorderedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */; }; 77204EA62A1E0EAA00BBDE4A /* WalletConnectionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA52A1E0EAA00BBDE4A /* WalletConnectionsView.swift */; }; 7725062C2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725062B2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift */; }; @@ -4678,6 +4680,8 @@ 770F578A2A8A48FF005FD7C1 /* ButtonViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonViewModel.swift; sourceTree = ""; }; 77171CA72A98BBA10032B387 /* NominationPoolErrorPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolErrorPresentable.swift; sourceTree = ""; }; 77171CA92A98BC420032B387 /* NominationPoolDataValidatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolDataValidatorFactory.swift; sourceTree = ""; }; + 7719018B2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapDataValidatorFactory.swift; sourceTree = ""; }; + 7719018D2AE0E71F00D9C918 /* SwapErrorPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapErrorPresentable.swift; sourceTree = ""; }; 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericBorderedView.swift; sourceTree = ""; }; 77204EA52A1E0EAA00BBDE4A /* WalletConnectionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectionsView.swift; sourceTree = ""; }; 7725062B2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferendumSearchViewLayout.swift; sourceTree = ""; }; @@ -9505,6 +9509,15 @@ path = Model; sourceTree = ""; }; + 7719018A2AE0E62500D9C918 /* Validation */ = { + isa = PBXGroup; + children = ( + 7719018B2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift */, + 7719018D2AE0E71F00D9C918 /* SwapErrorPresentable.swift */, + ); + path = Validation; + sourceTree = ""; + }; 7726E232D196BDD627329E24 /* ParaStkStakeSetup */ = { isa = PBXGroup; children = ( @@ -9721,6 +9734,7 @@ 77C9BCBF2ACD2E0300022EA2 /* Swaps */ = { isa = PBXGroup; children = ( + 7719018A2AE0E62500D9C918 /* Validation */, 29BD7DA0076BA8BC3411221A /* Setup */, ); path = Swaps; @@ -19956,6 +19970,7 @@ 0C154A0E2A45995500932C3F /* CompoundComparator.swift in Sources */, 88A0E10028A284C700A9C940 /* CurrencyManager.swift in Sources */, 848DE76826D64F7F0045CD29 /* UserStorageVersion.swift in Sources */, + 7719018E2AE0E71F00D9C918 /* SwapErrorPresentable.swift in Sources */, 84DB9E982640A49E00F23DD3 /* StakingRedeemViewModelFactory.swift in Sources */, 84FBECFD292764DA00FBEB83 /* WalletRemoteEvmSubscriptionService.swift in Sources */, 84ACEBF2261E664900AAE665 /* WalletHistoryFilterViewModel.swift in Sources */, @@ -21565,6 +21580,7 @@ 84F6B6502619E1ED0038F10D /* Int+Operations.swift in Sources */, 8487010E2907DF2F00F2C0C3 /* MultiValueView+Style.swift in Sources */, 84770F2A291F864500852A33 /* GovernanceUnlockInitData.swift in Sources */, + 7719018C2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift in Sources */, 85547F698B551ACD387D84E2 /* SelectValidatorsStartViewController.swift in Sources */, 845B07F129159AE7005785D3 /* DemocracyVoting.swift in Sources */, 41B29C1C9239BB2DCB7903A7 /* SelectValidatorsStartViewFactory.swift in Sources */, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 80ef295465..474ca23f5b 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -7,6 +7,7 @@ final class SwapSetupPresenter { let wireframe: SwapSetupWireframeProtocol let interactor: SwapSetupInteractorInputProtocol let viewModelFactory: SwapsSetupViewModelFactoryProtocol + let dataValidatingFactory: SwapDataValidatorFactoryProtocol private var assetBalance: AssetBalance? private var payChainAsset: ChainAsset? @@ -429,3 +430,75 @@ extension SwapSetupPresenter: Localizable { } } } +// +// +//extension SwapSetupPresenter { +// func validators(spendingAmount: Decimal, +// payChainAsset: ChainAsset, +// feeChainAsset: ChainAsset) -> [DataValidating] { +// var validators: [DataValidating] = [ +// dataValidatingFactory.has(fee: fee?.value, locale: selectedLocale) { [weak self] in +// self?.estimateFee() +// }, +// +// dataValidatingFactory.canSpendAmountInPlank( +// balance: assetBalance?.transferable, +// spendingAmount: spendingAmount, +// asset: payChainAsset.assetDisplayInfo, +// locale: selectedLocale +// ), +// +// dataValidatingFactory.canPayFeeSpendingAmountInPlank( +// balance: ass, +// fee: fee?.value, +// spendingAmount: isUtilityTransfer ? sendingAmount : nil, +// asset: utilityAssetInfo, +// locale: selectedLocale +// ), +// +// dataValidatingFactory.notViolatingMinBalancePaying( +// fee: fee?.value, +// total: senderUtilityAssetTotal, +// minBalance: isUtilityTransfer ? sendingAssetExistence?.minBalance : utilityAssetMinBalance, +// locale: selectedLocale +// ), +// +// dataValidatingFactory.receiverWillHaveAssetAccount( +// sendingAmount: sendingAmount, +// totalAmount: recepientSendingAssetBalance?.totalInPlank, +// minBalance: sendingAssetExistence?.minBalance, +// locale: selectedLocale +// ), +// +// dataValidatingFactory.receiverNotBlocked( +// recepientSendingAssetBalance?.blocked, +// locale: selectedLocale +// ) +// ] +// +// if !isUtilityTransfer { +// let accountProviderValidation = dataValidatingFactory.receiverHasAccountProvider( +// utilityTotalAmount: recepientUtilityAssetBalance?.totalInPlank, +// utilityMinBalance: utilityAssetMinBalance, +// assetExistence: sendingAssetExistence, +// locale: selectedLocale +// ) +// +// 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 +// } +//} diff --git a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift new file mode 100644 index 0000000000..37b48f273f --- /dev/null +++ b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift @@ -0,0 +1,23 @@ +import Foundation +import BigInt +import SoraFoundation + +protocol SwapDataValidatorFactoryProtocol: BaseDataValidatingFactoryProtocol { +} + +final class SwapDataValidatorFactoryProtocol: SwapDataValidatorFactoryProtocol { + weak var view: (Localizable & ControllerBackedProtocol)? + + var basePresentable: BaseErrorPresentable { presentable } + + let presentable: SwapErrorPresentable + + init( + presentable: TransferErrorPresentable, + assetDisplayInfo: AssetBalanceDisplayInfo, + utilityAssetInfo: AssetBalanceDisplayInfo?, + priceAssetInfoFactory: PriceAssetInfoFactoryProtocol + ) { + self.presentable = presentable + } +} diff --git a/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift b/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift new file mode 100644 index 0000000000..99cc0d9cd9 --- /dev/null +++ b/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift @@ -0,0 +1,7 @@ +import Foundation + +protocol SwapErrorPresentable: BaseErrorPresentable { +} + +extension SwapErrorPresentable where Self: AlertPresentable & ErrorPresentable { +} From 2298d9e87909ea10354b4aca014ac2e11194cf08 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 20 Oct 2023 14:21:15 +0300 Subject: [PATCH 050/204] add validations --- novawallet.xcodeproj/project.pbxproj | 24 ++ .../Setup/SwapSetupPresenter+Validating.swift | 45 ++++ .../Swaps/Setup/SwapSetupPresenter.swift | 235 ++++++++---------- .../Swaps/Setup/SwapSetupProtocols.swift | 3 +- .../Swaps/Setup/SwapSetupViewFactory.swift | 6 + .../Validation/SwapDataValidatorFactory.swift | 116 ++++++++- .../Validation/SwapErrorPresentable.swift | 62 +++++ .../Swaps/Validation/SwapFeeParams.swift | 74 ++++++ .../Swaps/Validation/SwapMaxErrorParams.swift | 11 + novawallet/en.lproj/Localizable.strings | 4 + novawallet/ru.lproj/Localizable.strings | 4 + .../Modules/Swaps/SwapsValidationTests.swift | 94 +++++++ 12 files changed, 540 insertions(+), 138 deletions(-) create mode 100644 novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift create mode 100644 novawallet/Modules/Swaps/Validation/SwapFeeParams.swift create mode 100644 novawallet/Modules/Swaps/Validation/SwapMaxErrorParams.swift create mode 100644 novawalletTests/Modules/Swaps/SwapsValidationTests.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 1c4ddf7194..71a95e2f51 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -678,6 +678,10 @@ 77171CAA2A98BC420032B387 /* NominationPoolDataValidatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77171CA92A98BC420032B387 /* NominationPoolDataValidatorFactory.swift */; }; 7719018C2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719018B2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift */; }; 7719018E2AE0E71F00D9C918 /* SwapErrorPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719018D2AE0E71F00D9C918 /* SwapErrorPresentable.swift */; }; + 771901902AE2424B00D9C918 /* SwapsValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719018F2AE2424B00D9C918 /* SwapsValidationTests.swift */; }; + 771901932AE2736E00D9C918 /* SwapFeeParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901922AE2736E00D9C918 /* SwapFeeParams.swift */; }; + 771901952AE2739800D9C918 /* SwapMaxErrorParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901942AE2739800D9C918 /* SwapMaxErrorParams.swift */; }; + 771901972AE2897F00D9C918 /* SwapSetupPresenter+Validating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901962AE2897F00D9C918 /* SwapSetupPresenter+Validating.swift */; }; 77204EA42A1CD59100BBDE4A /* GenericBorderedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */; }; 77204EA62A1E0EAA00BBDE4A /* WalletConnectionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA52A1E0EAA00BBDE4A /* WalletConnectionsView.swift */; }; 7725062C2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725062B2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift */; }; @@ -4682,6 +4686,10 @@ 77171CA92A98BC420032B387 /* NominationPoolDataValidatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolDataValidatorFactory.swift; sourceTree = ""; }; 7719018B2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapDataValidatorFactory.swift; sourceTree = ""; }; 7719018D2AE0E71F00D9C918 /* SwapErrorPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapErrorPresentable.swift; sourceTree = ""; }; + 7719018F2AE2424B00D9C918 /* SwapsValidationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapsValidationTests.swift; sourceTree = ""; }; + 771901922AE2736E00D9C918 /* SwapFeeParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapFeeParams.swift; sourceTree = ""; }; + 771901942AE2739800D9C918 /* SwapMaxErrorParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapMaxErrorParams.swift; sourceTree = ""; }; + 771901962AE2897F00D9C918 /* SwapSetupPresenter+Validating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwapSetupPresenter+Validating.swift"; sourceTree = ""; }; 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericBorderedView.swift; sourceTree = ""; }; 77204EA52A1E0EAA00BBDE4A /* WalletConnectionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectionsView.swift; sourceTree = ""; }; 7725062B2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferendumSearchViewLayout.swift; sourceTree = ""; }; @@ -8961,6 +8969,7 @@ C585109AC3A2580AB1253C31 /* SwapSetupInteractor.swift */, BBEB2BC52266AF493D310834 /* SwapSetupViewController.swift */, 6A6BCE8F8DB7576E1E5B5974 /* SwapSetupViewFactory.swift */, + 771901962AE2897F00D9C918 /* SwapSetupPresenter+Validating.swift */, ); path = Setup; sourceTree = ""; @@ -9514,10 +9523,20 @@ children = ( 7719018B2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift */, 7719018D2AE0E71F00D9C918 /* SwapErrorPresentable.swift */, + 771901922AE2736E00D9C918 /* SwapFeeParams.swift */, + 771901942AE2739800D9C918 /* SwapMaxErrorParams.swift */, ); path = Validation; sourceTree = ""; }; + 771901912AE2425400D9C918 /* Swaps */ = { + isa = PBXGroup; + children = ( + 7719018F2AE2424B00D9C918 /* SwapsValidationTests.swift */, + ); + path = Swaps; + sourceTree = ""; + }; 7726E232D196BDD627329E24 /* ParaStkStakeSetup */ = { isa = PBXGroup; children = ( @@ -14467,6 +14486,7 @@ 84B7C680289BFA78001A3566 /* Modules */ = { isa = PBXGroup; children = ( + 771901912AE2425400D9C918 /* Swaps */, 8440F4A3295AB4C200CAFBF9 /* SecurityLayer */, 843461F8290E55BE00379936 /* Governance */, 84B7C683289BFA78001A3566 /* Settings */, @@ -20435,6 +20455,7 @@ 842876AA24AE049B00D91AD8 /* SelectionTitleTableViewCell.swift in Sources */, 840E59B92A187E0700BA6ADD /* GladingPatternModel.swift in Sources */, 84CA68DD26BEA60A003B9453 /* ConnectionFactory.swift in Sources */, + 771901972AE2897F00D9C918 /* SwapSetupPresenter+Validating.swift in Sources */, 84A58FD428A05820003F6ABF /* HardwareSigningError.swift in Sources */, F4B39C53273270A300BB6E10 /* AcalaContributionSetupPresenter.swift in Sources */, 842A737E27DCD1A0006EE1EA /* OperationDetailsExtrinsicView.swift in Sources */, @@ -20615,6 +20636,7 @@ 84FBED0329279CF200FBEB83 /* ContractTransactionHistoryUpdater.swift in Sources */, 84893BFE24DA0000008F6A3F /* FieldStatus.swift in Sources */, 84F51053263AB440005D15AE /* StakingUnbondSetupLayout.swift in Sources */, + 771901952AE2739800D9C918 /* SwapMaxErrorParams.swift in Sources */, 77F033952A8142B0006BC67E /* StakingTypeValidatorView.swift in Sources */, 84BC704B289F1338008A9758 /* ExpirationTimeViewModel.swift in Sources */, 849014BF24AA87E4008F705E /* ScreenAuthorizationProtocol.swift in Sources */, @@ -21676,6 +21698,7 @@ 880E40FF298CF1ED0077B18B /* VotesProtocols.swift in Sources */, 84FEF3E528089FFB0042CBE7 /* TextInputField.swift in Sources */, 84D17EE12805A62600F7BAFF /* DAppAlertPresentable.swift in Sources */, + 771901932AE2736E00D9C918 /* SwapFeeParams.swift in Sources */, A871B6ABACAE8A811010F792 /* StakingPayoutConfirmationWireframe.swift in Sources */, 1795E946F1E386442E96E2BC /* StakingPayoutConfirmationPresenter.swift in Sources */, AEFA82BC4285117096BCBB16 /* StakingPayoutConfirmationInteractor.swift in Sources */, @@ -23161,6 +23184,7 @@ 84B66A1426FDF29A0038B963 /* WalletLocalSubscriptionFactoryStub.swift in Sources */, 887248082924F54900B0D2CC /* URL+Matchable.swift in Sources */, 8440F4A5295AB4E300CAFBF9 /* SecurityLayerTests.swift in Sources */, + 771901902AE2424B00D9C918 /* SwapsValidationTests.swift in Sources */, 84B7C718289BFA79001A3566 /* DAppOperationConfirmTests.swift in Sources */, 844D229E25EE79EA00C022F7 /* ExtrinsicServiceStub.swift in Sources */, 84F4A910255001D2000CF0A3 /* KeystoreExportWrapperTests.swift in Sources */, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift new file mode 100644 index 0000000000..b7fcb2f897 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift @@ -0,0 +1,45 @@ +import Foundation +import BigInt + +extension SwapSetupPresenter { + func validators( + spendingAmount: Decimal?, + payChainAsset: ChainAsset, + feeChainAsset: ChainAsset + ) -> [DataValidating] { + let feeDecimal = fee.map { Decimal.fromSubstrateAmount( + $0, + precision: Int16(feeChainAsset.asset.precision) + ) } ?? nil + + var validators: [DataValidating] = [ + dataValidatingFactory.has(fee: feeDecimal, locale: selectedLocale) { [weak self] in + self?.estimateFee() + }, + dataValidatingFactory.canSpendAmountInPlank( + balance: payAssetBalance?.transferable, + spendingAmount: spendingAmount, + asset: payChainAsset.assetDisplayInfo, + locale: selectedLocale + ), + dataValidatingFactory.canPayFeeSpendingAmountInPlank( + balance: payAssetBalance?.transferable, + fee: payChainAsset == feeChainAsset ? fee : nil, + spendingAmount: spendingAmount, + asset: feeChainAsset.assetDisplayInfo, + locale: selectedLocale + ), + dataValidatingFactory.has( + quote: quote, + payChainAssetId: payChainAsset.chainAssetId, + receiveChainAssetId: receiveChainAsset?.chainAssetId, + locale: selectedLocale, + onError: { [weak self] in + self?.refreshQuote(direction: self?.quoteArgs?.direction ?? .sell) + } + ) + ] + + return validators + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 8cd5835e66..984339054a 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -9,20 +9,20 @@ final class SwapSetupPresenter { let viewModelFactory: SwapsSetupViewModelFactoryProtocol let dataValidatingFactory: SwapDataValidatorFactoryProtocol - private var payAssetBalance: AssetBalance? - private var feeAssetBalance: AssetBalance? - private var payChainAsset: ChainAsset? - private var receiveChainAsset: ChainAsset? - private var feeChainAsset: ChainAsset? - private var payAssetPriceData: PriceData? - private var receiveAssetPriceData: PriceData? - private var feeAssetPriceData: PriceData? - - private var payAmountInput: AmountInputResult? - private var receiveAmountInput: Decimal? - private var fee: BigUInt? - private var quote: AssetConversion.Quote? - private var quoteArgs: AssetConversion.QuoteArgs? { + private(set) var payAssetBalance: AssetBalance? + private(set) var feeAssetBalance: AssetBalance? + private(set) var payChainAsset: ChainAsset? + private(set) var receiveChainAsset: ChainAsset? + private(set) var feeChainAsset: ChainAsset? + private(set) var payAssetPriceData: PriceData? + private(set) var receiveAssetPriceData: PriceData? + private(set) var feeAssetPriceData: PriceData? + + private(set) var payAmountInput: AmountInputResult? + private(set) var receiveAmountInput: Decimal? + private(set) var fee: BigUInt? + private(set) var quote: AssetConversion.Quote? + private(set) var quoteArgs: AssetConversion.QuoteArgs? { didSet { provideDetailsViewModel(isAvailable: quoteArgs != nil) } @@ -36,11 +36,14 @@ final class SwapSetupPresenter { interactor: SwapSetupInteractorInputProtocol, wireframe: SwapSetupWireframeProtocol, viewModelFactory: SwapsSetupViewModelFactoryProtocol, + dataValidatingFactory: SwapDataValidatorFactoryProtocol, localizationManager: LocalizationManagerProtocol ) { self.interactor = interactor self.wireframe = wireframe self.viewModelFactory = viewModelFactory + self.dataValidatingFactory = dataValidatingFactory + self.localizationManager = localizationManager } @@ -146,6 +149,19 @@ final class SwapSetupPresenter { return input.absoluteValue(from: balanceMinusFee) } + private func getSpendingAmount() -> Decimal? { + guard let input = payAmountInput, let payChainAsset = payChainAsset else { + return nil + } + guard let transferableBalance = Decimal.fromSubstrateAmount( + payAssetBalance?.transferable ?? 0, + precision: Int16(payChainAsset.asset.precision) + ) else { + return nil + } + return input.absoluteValue(from: transferableBalance) + } + private func providePayAssetViews() { providePayTitle() providePayAssetViewModel() @@ -200,7 +216,7 @@ final class SwapSetupPresenter { view?.didReceiveNetworkFee(viewModel: .loaded(value: viewModel)) } - private func estimateFee() { + func estimateFee() { guard let quote = quote, let quoteArgs = quoteArgs, let accountId = accountId else { return } @@ -223,7 +239,7 @@ final class SwapSetupPresenter { interactor.calculateFee(args: args) } - private func refreshQuote(direction: AssetConversion.Direction, forceUpdate: Bool = true) { + func refreshQuote(direction: AssetConversion.Direction, forceUpdate: Bool = true) { guard let payChainAsset = payChainAsset, let receiveChainAsset = receiveChainAsset else { @@ -234,55 +250,71 @@ final class SwapSetupPresenter { switch direction { case .buy: - if - let receiveInPlank = receiveAmountInput?.toSubstrateAmount( - precision: receiveChainAsset.assetDisplayInfo.assetPrecision - ), - receiveInPlank > 0 { - let quoteArgs = AssetConversion.QuoteArgs( - assetIn: payChainAsset.chainAssetId, - assetOut: receiveChainAsset.chainAssetId, - amount: receiveInPlank, - direction: direction - ) - self.quoteArgs = quoteArgs - interactor.calculateQuote(for: quoteArgs) - } else { - quoteArgs = nil - if forceUpdate { - payAmountInput = nil - providePayAmountInputViewModel() - } else { - refreshQuote(direction: .sell) - } - } + refreshQuoteForBuy( + payChainAsset: payChainAsset, + receiveChainAsset: receiveChainAsset, + forceUpdate: forceUpdate + ) case .sell: - if let payInPlank = getPayAmount(for: payAmountInput)?.toSubstrateAmount( - precision: Int16(payChainAsset.asset.precision)), payInPlank > 0 { - let quoteArgs = AssetConversion.QuoteArgs( - assetIn: payChainAsset.chainAssetId, - assetOut: receiveChainAsset.chainAssetId, - amount: payInPlank, - direction: direction - ) - self.quoteArgs = quoteArgs - interactor.calculateQuote(for: quoteArgs) - } else { - quoteArgs = nil - if forceUpdate { - receiveAmountInput = nil - provideReceiveAmountInputViewModel() - } else { - refreshQuote(direction: .buy) - } - } + refreshQuoteForSell( + payChainAsset: payChainAsset, + receiveChainAsset: receiveChainAsset, + forceUpdate: forceUpdate + ) } provideRateViewModel() provideFeeViewModel() } - private func balanceMinusFee() -> Decimal? { + private func refreshQuoteForBuy(payChainAsset: ChainAsset, receiveChainAsset: ChainAsset, forceUpdate: Bool) { + if + let receiveInPlank = receiveAmountInput?.toSubstrateAmount( + precision: receiveChainAsset.assetDisplayInfo.assetPrecision + ), + receiveInPlank > 0 { + let quoteArgs = AssetConversion.QuoteArgs( + assetIn: payChainAsset.chainAssetId, + assetOut: receiveChainAsset.chainAssetId, + amount: receiveInPlank, + direction: .buy + ) + self.quoteArgs = quoteArgs + interactor.calculateQuote(for: quoteArgs) + } else { + quoteArgs = nil + if forceUpdate { + payAmountInput = nil + providePayAmountInputViewModel() + } else { + refreshQuote(direction: .sell) + } + } + } + + private func refreshQuoteForSell(payChainAsset: ChainAsset, receiveChainAsset: ChainAsset, forceUpdate: Bool) { + if let payInPlank = getPayAmount(for: payAmountInput)?.toSubstrateAmount( + precision: Int16(payChainAsset.asset.precision)), payInPlank > 0 { + let quoteArgs = AssetConversion.QuoteArgs( + assetIn: payChainAsset.chainAssetId, + assetOut: receiveChainAsset.chainAssetId, + amount: payInPlank, + direction: .sell + ) + self.quoteArgs = quoteArgs + interactor.calculateQuote(for: quoteArgs) + } else { + quoteArgs = nil + if forceUpdate { + receiveAmountInput = nil + provideReceiveAmountInputViewModel() + } else { + refreshQuote(direction: .buy) + } + } + } + + func balanceMinusFee() -> Decimal? { guard let payChainAsset = payChainAsset else { return nil } @@ -404,8 +436,19 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { ) } - // TODO: navigate to confirm screen - func proceed() {} + func proceed() { + guard let payChainAsset = payChainAsset, let feeChainAsset = feeChainAsset else { + return + } + let validators = validators( + spendingAmount: getSpendingAmount(), + payChainAsset: payChainAsset, + feeChainAsset: feeChainAsset + ) + DataValidationRunner(validators: validators).runValidation { [weak self] in + // TODO: Show confirm screen + } + } } extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { @@ -528,75 +571,3 @@ extension SwapSetupPresenter: Localizable { } } } -// -// -//extension SwapSetupPresenter { -// func validators(spendingAmount: Decimal, -// payChainAsset: ChainAsset, -// feeChainAsset: ChainAsset) -> [DataValidating] { -// var validators: [DataValidating] = [ -// dataValidatingFactory.has(fee: fee?.value, locale: selectedLocale) { [weak self] in -// self?.estimateFee() -// }, -// -// dataValidatingFactory.canSpendAmountInPlank( -// balance: assetBalance?.transferable, -// spendingAmount: spendingAmount, -// asset: payChainAsset.assetDisplayInfo, -// locale: selectedLocale -// ), -// -// dataValidatingFactory.canPayFeeSpendingAmountInPlank( -// balance: ass, -// fee: fee?.value, -// spendingAmount: isUtilityTransfer ? sendingAmount : nil, -// asset: utilityAssetInfo, -// locale: selectedLocale -// ), -// -// dataValidatingFactory.notViolatingMinBalancePaying( -// fee: fee?.value, -// total: senderUtilityAssetTotal, -// minBalance: isUtilityTransfer ? sendingAssetExistence?.minBalance : utilityAssetMinBalance, -// locale: selectedLocale -// ), -// -// dataValidatingFactory.receiverWillHaveAssetAccount( -// sendingAmount: sendingAmount, -// totalAmount: recepientSendingAssetBalance?.totalInPlank, -// minBalance: sendingAssetExistence?.minBalance, -// locale: selectedLocale -// ), -// -// dataValidatingFactory.receiverNotBlocked( -// recepientSendingAssetBalance?.blocked, -// locale: selectedLocale -// ) -// ] -// -// if !isUtilityTransfer { -// let accountProviderValidation = dataValidatingFactory.receiverHasAccountProvider( -// utilityTotalAmount: recepientUtilityAssetBalance?.totalInPlank, -// utilityMinBalance: utilityAssetMinBalance, -// assetExistence: sendingAssetExistence, -// locale: selectedLocale -// ) -// -// 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 -// } -//} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index a35ce39d96..34badef8cd 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -48,7 +48,8 @@ protocol SwapSetupInteractorOutputProtocol: AnyObject { func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId: AccountId) } -protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, ErrorPresentable { +protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, + ErrorPresentable, SwapErrorPresentable { func showPayTokenSelection( from view: ControllerBackedProtocol?, chainAsset: ChainAsset?, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index e20538832a..4849cb078b 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -21,11 +21,16 @@ struct SwapSetupViewFactory { balanceViewModelFactoryFacade: balanceViewModelFactoryFacade, networkViewModelFactory: NetworkViewModelFactory() ) + let dataValidatingFactory = SwapDataValidatorFactory( + presentable: wireframe, + balanceViewModelFactoryFacade: balanceViewModelFactoryFacade + ) let presenter = SwapSetupPresenter( interactor: interactor, wireframe: wireframe, viewModelFactory: viewModelFactory, + dataValidatingFactory: dataValidatingFactory, localizationManager: LocalizationManager.shared ) @@ -35,6 +40,7 @@ struct SwapSetupViewFactory { ) presenter.view = view + dataValidatingFactory.view = view interactor.presenter = presenter return view diff --git a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift index 37b48f273f..e103257e10 100644 --- a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift +++ b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift @@ -3,21 +3,127 @@ import BigInt import SoraFoundation protocol SwapDataValidatorFactoryProtocol: BaseDataValidatingFactoryProtocol { + func has( + quote: AssetConversion.Quote?, + payChainAssetId: ChainAssetId?, + receiveChainAssetId: ChainAssetId?, + locale: Locale, + onError: (() -> Void)? + ) -> DataValidating + func canPayFeeSpendingAmount( + params: SwapFeeParams, + swapAmount: Decimal?, + locale: Locale + ) -> DataValidating } -final class SwapDataValidatorFactoryProtocol: SwapDataValidatorFactoryProtocol { +final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { weak var view: (Localizable & ControllerBackedProtocol)? var basePresentable: BaseErrorPresentable { presentable } let presentable: SwapErrorPresentable + let balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol init( - presentable: TransferErrorPresentable, - assetDisplayInfo: AssetBalanceDisplayInfo, - utilityAssetInfo: AssetBalanceDisplayInfo?, - priceAssetInfoFactory: PriceAssetInfoFactoryProtocol + presentable: SwapErrorPresentable, + balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol ) { self.presentable = presentable + self.balanceViewModelFactoryFacade = balanceViewModelFactoryFacade + } + + func has( + quote: AssetConversion.Quote?, + payChainAssetId: ChainAssetId?, + receiveChainAssetId: ChainAssetId?, + locale: Locale, + onError: (() -> Void)? + ) -> DataValidating { + ErrorConditionViolation(onError: { [weak self] in + defer { + onError?() + } + + guard let view = self?.view else { + return + } + self?.presentable.presentNotEnoughLiquidity(from: view, locale: locale) + }, preservesCondition: { + guard let quote = quote else { + return false + } + return quote.assetIn == payChainAssetId && quote.assetOut == receiveChainAssetId + }) + } + + func canPayFeeSpendingAmount( + params: SwapFeeParams, + swapAmount: Decimal?, + locale: Locale + ) -> DataValidating { + let preparedValues = params.prepare(swapAmount: swapAmount) + + return WarningConditionViolation(onWarning: { [weak self] delegate in + guard let self = self, let view = self.view else { + return + } + let availableToPayString = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: params.feeChainAsset.assetDisplayInfo, + value: preparedValues.availableToPay + ).value(for: locale) + let feeString = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: params.feeChainAsset.assetDisplayInfo, + value: preparedValues.feeDecimal + ).value(for: locale) + let errorParams: SwapMaxErrorParams + + if preparedValues.toBuyED != 0 { + let diffString = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: params.feeChainAsset.assetDisplayInfo, + value: preparedValues.diff + ).value(for: locale) + let edDepositInFeeTokenString = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: params.feeChainAsset.assetDisplayInfo, + value: preparedValues.edDepositInFeeTokenDecimal + ).value(for: locale) + let edString = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: params.edChainAsset.assetDisplayInfo, + value: preparedValues.edDecimal + ).value(for: locale) + let edToken = params.edChainAsset.asset.symbol + errorParams = .init( + maxSwap: availableToPayString, + fee: feeString, + existentialDeposit: SwapMaxErrorParams.ExistensialDepositErrorParams( + fee: diffString, + value: edString, + token: edToken + ) + ) + } else { + errorParams = .init( + maxSwap: availableToPayString, + fee: feeString, + existentialDeposit: nil + ) + } + + let action = { [preparedValues] in + if preparedValues.availableToPay > 0 { + params.amountUpdateClosure(preparedValues.availableToPay) + delegate.didCompleteWarningHandling() + } + } + + self.presentable.presentSwapAll( + from: view, + errorParams: errorParams, + action: action, + locale: locale + ) + }, preservesCondition: { + preparedValues.feeTokenBalanceDecimal >= preparedValues.swapAmountInFeeToken + preparedValues.feeDecimal + preparedValues.toBuyED + }) } } diff --git a/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift b/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift index 99cc0d9cd9..7bb25a2073 100644 --- a/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift +++ b/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift @@ -1,7 +1,69 @@ import Foundation protocol SwapErrorPresentable: BaseErrorPresentable { + func presentNotEnoughLiquidity(from view: ControllerBackedProtocol, locale: Locale?) + func presentSwapAll( + from view: ControllerBackedProtocol?, + errorParams: SwapMaxErrorParams, + action: @escaping () -> Void, + locale: Locale + ) } extension SwapErrorPresentable where Self: AlertPresentable & ErrorPresentable { + func presentNotEnoughLiquidity(from view: ControllerBackedProtocol, locale: Locale?) { + let title = R.string.localizable.swapsSetupErrorNotEnoughLiquidityTitle( + preferredLanguages: locale?.rLanguages) + let closeAction = R.string.localizable.commonClose( + preferredLanguages: locale?.rLanguages) + + present(message: nil, title: title, closeAction: closeAction, from: view) + } + + func presentSwapAll( + from view: ControllerBackedProtocol?, + errorParams: SwapMaxErrorParams, + action: @escaping () -> Void, + locale: Locale + ) { + let title = R.string.localizable.commonInsufficientBalance(preferredLanguages: locale.rLanguages) + let message: String + + if let edError = errorParams.existentialDeposit { + message = R.string.localizable.swapsSetupErrorInsufficientBalanceEdMessage( + errorParams.maxSwap, + errorParams.fee, + edError.fee, + edError.value, + edError.token, + preferredLanguages: locale.rLanguages + ) + } else { + message = R.string.localizable.swapsSetupErrorInsufficientBalanceMessage( + errorParams.maxSwap, + errorParams.fee, + preferredLanguages: locale.rLanguages + ) + } + + let cancelAction = AlertPresentableAction( + title: R.string.localizable.commonCancel(preferredLanguages: locale.rLanguages) + ) + + let swapAllAction = AlertPresentableAction( + title: R.string.localizable.swapsSetupErrorInsufficientBalanceAction( + preferredLanguages: locale.rLanguages + ), + handler: action + ) + + let viewModel = AlertPresentableViewModel( + title: title, + message: message, + actions: [cancelAction, swapAllAction], + closeAction: nil + ) + + present(viewModel: viewModel, style: .alert, from: view) + } } diff --git a/novawallet/Modules/Swaps/Validation/SwapFeeParams.swift b/novawallet/Modules/Swaps/Validation/SwapFeeParams.swift new file mode 100644 index 0000000000..4c54284b08 --- /dev/null +++ b/novawallet/Modules/Swaps/Validation/SwapFeeParams.swift @@ -0,0 +1,74 @@ +import BigInt + +struct SwapFeeParams { + let fee: BigUInt? + let feeChainAsset: ChainAsset + let feeAssetBalance: AssetBalance? + let edAmount: BigUInt? + let edAmountInFeeToken: BigUInt? + let edChainAsset: ChainAsset + let edChainAssetBalance: AssetBalance? + let payChainAsset: ChainAsset + let amountUpdateClosure: (Decimal) -> Void +} + +extension SwapFeeParams { + func prepare(swapAmount: Decimal?) -> SwapFeeResult { + let params = self + let fee = params.fee ?? 0 + let feeDecimal = Decimal.fromSubstrateAmount( + fee, + precision: Int16(params.feeChainAsset.asset.precision) + ) ?? 0 + let feeTokenBalance = params.feeAssetBalance?.transferable ?? 0 + let feeTokenBalanceDecimal = Decimal.fromSubstrateAmount( + feeTokenBalance, + precision: Int16(params.feeChainAsset.asset.precision) + ) ?? 0 + + let edBalance = params.edAmount ?? 0 + let edDecimal = Decimal.fromSubstrateAmount( + edBalance, + precision: Int16(params.edChainAsset.asset.precision) + ) ?? 0 + let edBalanceTransferrable = params.edChainAssetBalance?.transferable ?? 0 + let edBalanceTransferrableDecimal = Decimal.fromSubstrateAmount( + edBalanceTransferrable, + precision: Int16(params.edChainAsset.asset.precision) + ) ?? 0 + let edDepositInFeeToken = params.edAmountInFeeToken ?? 0 + let edDepositInFeeTokenDecimal = Decimal.fromSubstrateAmount( + edDepositInFeeToken, + precision: Int16(params.feeChainAsset.asset.precision) + ) ?? 0 + + let toBuyED = params.edChainAsset != params.feeChainAsset && edBalanceTransferrableDecimal == 0 ? edDepositInFeeTokenDecimal : 0 + let swapAmount = swapAmount ?? 0 + let swapAmountInFeeToken = params.payChainAsset == params.feeChainAsset ? swapAmount : 0 + let needToPay = swapAmountInFeeToken + feeDecimal + toBuyED + let diff = needToPay - feeTokenBalanceDecimal + let availableToPay = feeTokenBalanceDecimal - diff + + return .init( + availableToPay: availableToPay, + feeDecimal: feeDecimal, + toBuyED: toBuyED, + edDepositInFeeTokenDecimal: edDepositInFeeTokenDecimal, + diff: diff, + edDecimal: edDecimal, + feeTokenBalanceDecimal: feeTokenBalanceDecimal, + swapAmountInFeeToken: swapAmountInFeeToken + ) + } + + struct SwapFeeResult { + let availableToPay: Decimal + let feeDecimal: Decimal + let toBuyED: Decimal + let edDepositInFeeTokenDecimal: Decimal + let diff: Decimal + let edDecimal: Decimal + let feeTokenBalanceDecimal: Decimal + let swapAmountInFeeToken: Decimal + } +} diff --git a/novawallet/Modules/Swaps/Validation/SwapMaxErrorParams.swift b/novawallet/Modules/Swaps/Validation/SwapMaxErrorParams.swift new file mode 100644 index 0000000000..4a2fe1e4de --- /dev/null +++ b/novawallet/Modules/Swaps/Validation/SwapMaxErrorParams.swift @@ -0,0 +1,11 @@ +struct SwapMaxErrorParams { + let maxSwap: String + let fee: String + let existentialDeposit: ExistensialDepositErrorParams? + + struct ExistensialDepositErrorParams { + let fee: String + let value: String + let token: String + } +} diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 2731c17641..c49137d3b3 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1393,3 +1393,7 @@ "swaps.receive.token.selection.title" = "Token to receive"; "swaps.rate.description" = "Exchange rate between two different cryptocurrencies. It represents how much of one cryptocurrency you can get in exchange for a certain amount of another cryptocurrency."; "swaps.network.fee.description" = "A network fees charged by the blockchain to process and validate any transactions. May vary depending on network conditions or transaction speed."; +"swaps.setup.error.not.enough.liquidity.title" = "Pool doesn’t have enough liquidity to swap"; +"swaps.setup.error.insufficient.balance.ed.message" = "You can swap up to %@ since you need to pay %@ for network fee and also convert %@ to %@ to meet %@ minimum balance."; +"swaps.setup.error.insufficient.balance.message" = "You can swap up to %@ since you need to pay %@ for network fee."; +"swaps.setup.error.insufficient.balance.action" = "Swap max"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 2d9cc99c1d..d683322748 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1394,3 +1394,7 @@ "swaps.receive.token.selection.title" = "Токен для получения"; "swaps.rate.description" = "Обменный курс между двумя различными криптовалютами. Он представляет, сколько одной криптовалюты вы можете получить в обмен на определенное количество другой криптовалюты."; "swaps.network.fee.description" = "Это комиссия сети, взимаемая блокчейном за обработку и подтверждение любых транзакций. Она может изменяться в зависимости от условий в сети или скорости выполнения транзакции."; +"swaps.setup.error.not.enough.liquidity.title" = "В пуле недостаточно ликвидности для обмена"; +"swaps.setup.error.insufficient.balance.ed.message" = "You can swap up to %@ since you need to pay %@ for network fee and also convert %@ to %@ to meet %@ minimum balance."; +"swaps.setup.error.insufficient.balance.message" = "You can swap up to %@ since you need to pay %@ for network fee."; +"swaps.setup.error.insufficient.balance.action" = "Swap max"; diff --git a/novawalletTests/Modules/Swaps/SwapsValidationTests.swift b/novawalletTests/Modules/Swaps/SwapsValidationTests.swift new file mode 100644 index 0000000000..d5089201ed --- /dev/null +++ b/novawalletTests/Modules/Swaps/SwapsValidationTests.swift @@ -0,0 +1,94 @@ +import XCTest +@testable import novawallet +import SoraKeystore +import Cuckoo +import BigInt + +final class SwapsValidationTests: XCTestCase { + private func amountInPlank(_ amount: Decimal, _ chainAsset: ChainAsset) -> BigUInt { + amount.toSubstrateAmount(precision: chainAsset.assetDisplayInfo.assetPrecision) ?? 0 + } + + func testCalculatedFeeWithoutED() throws { + let chain = ChainModelGenerator.generateChain(generatingAssets: 3, addressPrefix: 42) + let utilityChainAsset = ChainAsset(chain: chain, asset: chain.assets.first(where: { $0.assetId == 0 })!) + let payChainAsset = ChainAsset(chain: chain, asset: chain.assets.first(where: { $0.assetId == 2 })!) + let feeChainAsset = payChainAsset + let accountId = try WestendStub.address.toAccountId() + + let freeBalance = amountInPlank(50, payChainAsset) + let payAssetBalance = AssetBalance(chainAssetId: payChainAsset.chainAssetId, + accountId: accountId, + freeInPlank: freeBalance, + reservedInPlank: 0, + frozenInPlank: 0, + blocked: false) + let utilityAssetBalance = AssetBalance(chainAssetId: utilityChainAsset.chainAssetId, + accountId: accountId, + freeInPlank: 0, + reservedInPlank: 0, + frozenInPlank: 0, + blocked: false) + let existentialDeposit = amountInPlank(1, utilityChainAsset) + let fee = amountInPlank(0.1, payChainAsset) + let existentialDepositInFeeToken = amountInPlank(0.01, payChainAsset) + + let params = SwapFeeParams( + fee: fee, + feeChainAsset: payChainAsset, + feeAssetBalance: payAssetBalance, + edAmount: existentialDeposit, + edAmountInFeeToken: existentialDepositInFeeToken, + edChainAsset: utilityChainAsset, + edChainAssetBalance: utilityAssetBalance, + payChainAsset: payChainAsset, + amountUpdateClosure: { _ in }) + + let result = params.prepare(swapAmount: 50) + + XCTAssertEqual(result.availableToPay, 49.89) + + } + + func testCalculatedFeeWithED() throws { + let chain = ChainModelGenerator.generateChain(generatingAssets: 3, addressPrefix: 42) + let utilityChainAsset = ChainAsset(chain: chain, asset: chain.assets.first(where: { $0.assetId == 0 })!) + let ksmChainAsset = ChainAsset(chain: chain, asset: chain.assets.first(where: { $0.assetId == 1 })!) + let payChainAsset = ChainAsset(chain: chain, asset: chain.assets.first(where: { $0.assetId == 2 })!) + let feeChainAsset = utilityChainAsset + let accountId = try WestendStub.address.toAccountId() + + let freeBalance = amountInPlank(50, payChainAsset) + let payAssetBalance = AssetBalance(chainAssetId: payChainAsset.chainAssetId, + accountId: accountId, + freeInPlank: freeBalance, + reservedInPlank: 0, + frozenInPlank: 0, + blocked: false) + let utilityAssetBalance = AssetBalance(chainAssetId: utilityChainAsset.chainAssetId, + accountId: accountId, + freeInPlank: 10, + reservedInPlank: 0, + frozenInPlank: 0, + blocked: false) + let existentialDeposit = amountInPlank(1, utilityChainAsset) + let fee = amountInPlank(0.1, payChainAsset) + let existentialDepositInFeeToken = amountInPlank(0.01, payChainAsset) + + let params = SwapFeeParams( + fee: fee, + feeChainAsset: payChainAsset, + feeAssetBalance: payAssetBalance, + edAmount: existentialDeposit, + edAmountInFeeToken: existentialDepositInFeeToken, + edChainAsset: utilityChainAsset, + edChainAssetBalance: utilityAssetBalance, + payChainAsset: payChainAsset, + amountUpdateClosure: { _ in }) + + let result = params.prepare(swapAmount: 50) + + XCTAssertEqual(result.availableToPay, 49.9) + + } +} From eb3f5324a7340a8581eb45f01d5870427786b7fc Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 20 Oct 2023 15:49:27 +0300 Subject: [PATCH 051/204] PR fixes --- .../Swaps/Setup/SwapSetupInteractor.swift | 110 +++++++++++------- .../Swaps/Setup/SwapSetupPresenter.swift | 30 ++--- 2 files changed, 86 insertions(+), 54 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 5aa3cdb899..fcc451de20 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -19,11 +19,36 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning private var runtimeOperationCall: CancellableCall? private var extrinsicService: ExtrinsicServiceProtocol? - private var payAssetPriceProvider: StreamableProvider? - private var receiveAssetPriceProvider: StreamableProvider? - private var feeAssetPriceProvider: StreamableProvider? - private var payAssetBalanceProvider: StreamableProvider? - private var feeAssetBalanceProvider: StreamableProvider? + private var priceProviders: [ChainAssetId: StreamableProvider] = [:] + private var assetBalanceProviders: [ChainAssetId: StreamableProvider] = [:] + + private var receiveChainAsset: ChainAsset? { + didSet { + updateSubscriptions() + } + } + + private var payChainAsset: ChainAsset? { + didSet { + updateSubscriptions() + } + } + + private var feeChainAsset: ChainAsset? { + didSet { + updateSubscriptions() + } + } + + private var activeChainAssets: Set { + Set( + [ + receiveChainAsset?.chainAssetId, + payChainAsset?.chainAssetId, + feeChainAsset?.chainAssetId + ].compactMap { $0 } + ) + } init( assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, @@ -49,12 +74,27 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning self.operationQueue = operationQueue } + private func updateSubscriptions() { + priceProviders = clear(providers: priceProviders) + assetBalanceProviders = clear(providers: assetBalanceProviders) + } + + private func clear(providers: [ChainAssetId: StreamableProvider]) -> [ChainAssetId: StreamableProvider] { + providers.reduce(into: [ChainAssetId: StreamableProvider]()) { + if !activeChainAssets.contains($1.key) { + $1.value.removeObserver(self) + } else { + $0[$1.key] = $1.value + } + } + } + private func priceSubscription(chainAsset: ChainAsset) -> StreamableProvider? { guard let priceId = chainAsset.asset.priceId else { return nil } - return subscribeToPrice( + return priceProviders[chainAsset.chainAssetId] ?? subscribeToPrice( for: priceId, currency: currencyManager.selectedCurrency ) @@ -65,7 +105,7 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning return nil } let chainAssetId = chainAsset.chainAssetId - return subscribeToAssetBalanceProvider( + return assetBalanceProviders[chainAssetId] ?? subscribeToAssetBalanceProvider( for: accountId, chainId: chainAssetId.chainId, assetId: chainAssetId.assetId @@ -133,13 +173,6 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning let metaChainAccountResponse = selectedAccount.fetchMetaChainAccount(for: chainAsset.chain.accountRequest()) return metaChainAccountResponse?.chainAccount } - - private func providersEqual(_ provider1: StreamableProvider?, _ provider2: StreamableProvider?) -> Bool { - if provider1 == nil, provider2 == nil { - return false - } - return provider1 === provider2 - } } extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { @@ -152,45 +185,40 @@ extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { } func update(receiveChainAsset: ChainAsset?) { - clear(streamableProvider: &receiveAssetPriceProvider) - if let receiveChainAsset = receiveChainAsset { - receiveAssetPriceProvider = priceSubscription(chainAsset: receiveChainAsset) + self.receiveChainAsset = receiveChainAsset + + if let chainAsset = receiveChainAsset { + priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) } } func update(payChainAsset: ChainAsset?) { - if !providersEqual(payAssetPriceProvider, feeAssetPriceProvider) { - clear(streamableProvider: &payAssetPriceProvider) - payAssetPriceProvider = payChainAsset.map { priceSubscription(chainAsset: $0) } ?? nil - } - - if !providersEqual(payAssetBalanceProvider, feeAssetBalanceProvider) { - clear(streamableProvider: &payAssetBalanceProvider) - payAssetBalanceProvider = payChainAsset.map { assetBalanceSubscription(chainAsset: $0) } ?? nil - } + self.payChainAsset = payChainAsset - if let payChainAsset = payChainAsset, - let chainAccount = chainAccountResponse(for: payChainAsset) { - extrinsicService = extrinsicServiceFactory.createService( - account: chainAccount, - chain: payChainAsset.chain - ) - presenter?.didReceive(payAccountId: chainAccount.accountId) - } else { + guard let chainAsset = payChainAsset, + let chainAccount = chainAccountResponse(for: chainAsset) else { extrinsicService = nil presenter?.didReceive(payAccountId: nil) + return } + priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) + assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) + + extrinsicService = extrinsicServiceFactory.createService( + account: chainAccount, + chain: chainAsset.chain + ) + presenter?.didReceive(payAccountId: chainAccount.accountId) } func update(feeChainAsset: ChainAsset?) { - if !providersEqual(feeAssetPriceProvider, payAssetPriceProvider) { - clear(streamableProvider: &feeAssetPriceProvider) - feeAssetPriceProvider = feeChainAsset.map { priceSubscription(chainAsset: $0) } ?? nil - } - if !providersEqual(feeAssetBalanceProvider, payAssetBalanceProvider) { - clear(streamableProvider: &feeAssetBalanceProvider) - feeAssetBalanceProvider = feeChainAsset.map { assetBalanceSubscription(chainAsset: $0) } ?? nil + self.feeChainAsset = feeChainAsset + + guard let chainAsset = feeChainAsset else { + return } + priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) + assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) } func calculateFee( diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index eb3e2dc793..244203f78c 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -286,7 +286,7 @@ final class SwapSetupPresenter { return nil } let balanceValue = payAssetBalance?.transferable ?? 0 - let feeValue = payChainAsset == feeChainAsset ? fee : 0 + let feeValue = payChainAsset.chainAssetId == feeChainAsset?.chainAssetId ? fee : 0 let precision = Int16(payChainAsset.asset.precision) @@ -298,6 +298,21 @@ final class SwapSetupPresenter { return balance - fee } + + private func handleAssetBalanceError(chainAssetId: ChainAssetId) { + switch chainAssetId { + case payChainAsset?.chainAssetId: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.payChainAsset.map { self?.interactor.update(payChainAsset: $0) } + } + case feeChainAsset?.chainAssetId: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.feeChainAsset.map { self?.interactor.update(feeChainAsset: $0) } + } + default: + break + } + } } extension SwapSetupPresenter: SwapSetupPresenterProtocol { @@ -429,18 +444,7 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { self?.estimateFee() } case let .assetBalance(_, chainAssetId, accountId): - switch chainAssetId { - case payChainAsset?.chainAssetId: - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.payChainAsset.map { self?.interactor.update(payChainAsset: $0) } - } - case feeChainAsset?.chainAssetId: - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.feeChainAsset.map { self?.interactor.update(feeChainAsset: $0) } - } - default: - break - } + handleAssetBalanceError(chainAssetId: chainAssetId) } } From 916e03237c8a7388d2de12cb0fccb088d3980479 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 20 Oct 2023 16:04:29 +0300 Subject: [PATCH 052/204] handle price error --- .../Swaps/Setup/SwapSetupInteractor.swift | 4 ++++ .../Swaps/Setup/SwapSetupPresenter.swift | 18 ++++++++++++++---- .../Swaps/Setup/SwapSetupProtocols.swift | 1 + 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index fcc451de20..4de4b05a19 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -226,6 +226,10 @@ extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { ) { fee(args: args) } + + func remakePriceSubscription(for chainAsset: ChainAsset) { + priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) + } } extension SwapSetupInteractor: ExtrinsicFeeProxyDelegate { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 244203f78c..435a4edd64 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -313,6 +313,18 @@ final class SwapSetupPresenter { break } } + + func handlePriceError(priceId: AssetModel.PriceId) { + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + guard let self = self else { + return + } + [self.payChainAsset, self.receiveChainAsset, self.feeChainAsset] + .compactMap { $0 } + .filter { $0.asset.priceId == priceId } + .forEach(self.interactor.remakePriceSubscription) + } + } } extension SwapSetupPresenter: SwapSetupPresenterProtocol { @@ -439,10 +451,8 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in self?.estimateFee() } - case .price: - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.estimateFee() - } + case let .price(_, priceId): + handlePriceError(priceId: priceId) case let .assetBalance(_, chainAssetId, accountId): handleAssetBalanceError(chainAssetId: chainAssetId) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index a35ce39d96..d5d2bfa0e7 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -37,6 +37,7 @@ protocol SwapSetupInteractorInputProtocol: AnyObject { func update(feeChainAsset: ChainAsset?) func calculateQuote(for args: AssetConversion.QuoteArgs) func calculateFee(args: AssetConversion.CallArgs) + func remakePriceSubscription(for chainAsset: ChainAsset) } protocol SwapSetupInteractorOutputProtocol: AnyObject { From a0b3e7a336a17641afcc56d95a5bf834138920ec Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Sun, 22 Oct 2023 18:26:47 +0300 Subject: [PATCH 053/204] PR fixes --- novawallet.xcodeproj/project.pbxproj | 8 +++---- novawallet/Common/Model/BigRational.swift | 14 +++++++++++ .../TextInputView/PercentInputView.swift} | 24 +++++++++---------- .../Swaps/Setup/SwapSetupPresenter.swift | 4 ++-- .../Slippage/SwapSlippagePresenter.swift | 14 +---------- .../Slippage/SwapSlippageViewController.swift | 2 +- .../Slippage/SwapSlippageViewLayout.swift | 2 +- 7 files changed, 35 insertions(+), 33 deletions(-) rename novawallet/{Modules/Swaps/Slippage/SwapSlippageInputView.swift => Common/View/TextInputView/PercentInputView.swift} (92%) diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 6362e8bf89..7fc08c188e 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -707,7 +707,7 @@ 77740BBC2AD4A7B800E8C06F /* CollapsableContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBB2AD4A7B800E8C06F /* CollapsableContainerView.swift */; }; 77740BBE2AD4A7F500E8C06F /* SwapDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */; }; 77740BC02AD4A80D00E8C06F /* SwapRateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */; }; - 77740BC42AD8145500E8C06F /* SwapSlippageInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BC32AD8145500E8C06F /* SwapSlippageInputView.swift */; }; + 77740BC42AD8145500E8C06F /* PercentInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BC32AD8145500E8C06F /* PercentInputView.swift */; }; 77740BC62AD849D100E8C06F /* SlippagePercentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BC52AD849D100E8C06F /* SlippagePercentViewModel.swift */; }; 77799ADD2A74219A00B7E564 /* ButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799ADC2A74219A00B7E564 /* ButtonState.swift */; }; 77799ADF2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799ADE2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift */; }; @@ -4720,7 +4720,7 @@ 77740BBB2AD4A7B800E8C06F /* CollapsableContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsableContainerView.swift; sourceTree = ""; }; 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapDetailsView.swift; sourceTree = ""; }; 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRateView.swift; sourceTree = ""; }; - 77740BC32AD8145500E8C06F /* SwapSlippageInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapSlippageInputView.swift; sourceTree = ""; }; + 77740BC32AD8145500E8C06F /* PercentInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PercentInputView.swift; sourceTree = ""; }; 77740BC52AD849D100E8C06F /* SlippagePercentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlippagePercentViewModel.swift; sourceTree = ""; }; 77799ADC2A74219A00B7E564 /* ButtonState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonState.swift; sourceTree = ""; }; 77799ADE2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoRelaychainWireframe.swift; sourceTree = ""; }; @@ -8972,7 +8972,6 @@ F9F3BD600F80ED0426141843 /* SwapSlippageViewController.swift */, 2E965356C7C646CB86BBEBB6 /* SwapSlippageViewLayout.swift */, 28294C13CF8F62D2FE4D0427 /* SwapSlippageViewFactory.swift */, - 77740BC32AD8145500E8C06F /* SwapSlippageInputView.swift */, ); path = Slippage; sourceTree = ""; @@ -10360,6 +10359,7 @@ 8422F2ED2887E3D300C7B840 /* TextInputView.swift */, 847999B72889510C00D1BAD2 /* TextInputViewDelegate.swift */, 849F1452294477DA00D9F9BA /* TextWithServiceInputView.swift */, + 77740BC32AD8145500E8C06F /* PercentInputView.swift */, ); path = TextInputView; sourceTree = ""; @@ -22845,7 +22845,7 @@ 1232A714A96F937330FC0AFA /* GovernanceDelegateConfirmViewFactory.swift in Sources */, 4B1FA597B618713C75917816 /* GovernanceYourDelegationsProtocols.swift in Sources */, 15B079FA97C96327FD4A2E16 /* GovernanceYourDelegationsWireframe.swift in Sources */, - 77740BC42AD8145500E8C06F /* SwapSlippageInputView.swift in Sources */, + 77740BC42AD8145500E8C06F /* PercentInputView.swift in Sources */, 75249684C6F3EE4E553DABA1 /* GovernanceYourDelegationsPresenter.swift in Sources */, EB20C6B406155664B981BA94 /* GovernanceYourDelegationsInteractor.swift in Sources */, 59A0AF440ABAAA459EF7D993 /* GovernanceYourDelegationsViewController.swift in Sources */, diff --git a/novawallet/Common/Model/BigRational.swift b/novawallet/Common/Model/BigRational.swift index 1d12890064..04143acfd0 100644 --- a/novawallet/Common/Model/BigRational.swift +++ b/novawallet/Common/Model/BigRational.swift @@ -15,3 +15,17 @@ extension BigRational { .init(numerator: numerator, denominator: 100) } } + +extension BigRational { + static func fraction(from number: Decimal) -> BigRational { + let decimalNumber = NSDecimalNumber(decimal: number) + guard decimalNumber.doubleValue.remainder(dividingBy: 1) != 0 else { + return .init(numerator: BigUInt(decimalNumber.intValue), denominator: 1) + } + + let scale = -number.exponent + let numerator = decimalNumber.multiplying(byPowerOf10: Int16(scale)).intValue + let denominator = Int(truncating: pow(10, scale) as NSNumber) + return .init(numerator: BigUInt(numerator), denominator: BigUInt(denominator)) + } +} diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift b/novawallet/Common/View/TextInputView/PercentInputView.swift similarity index 92% rename from novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift rename to novawallet/Common/View/TextInputView/PercentInputView.swift index 0e58a84b75..4810f4fd28 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageInputView.swift +++ b/novawallet/Common/View/TextInputView/PercentInputView.swift @@ -1,11 +1,11 @@ import SnapKit import SoraUI -protocol SwapSlippageInputViewDelegateProtocol: AnyObject { +protocol PercentInputViewDelegateProtocol: AnyObject { func didSelect(percent: SlippagePercentViewModel, sender: Any?) } -final class SwapSlippageInputView: BackgroundedContentControl { +final class PercentInputView: BackgroundedContentControl { let textField: UITextField = .create { $0.font = UIFont.regularSubheadline $0.textColor = R.color.colorTextPrimary() @@ -43,7 +43,7 @@ final class SwapSlippageInputView: BackgroundedContentControl { [] ) - weak var delegate: SwapSlippageInputViewDelegateProtocol? + weak var delegate: PercentInputViewDelegateProtocol? private(set) var inputViewModel: AmountInputViewModelProtocol? private var viewModel: [SlippagePercentViewModel] = [] @@ -163,7 +163,7 @@ final class SwapSlippageInputView: BackgroundedContentControl { return button } - private func updateViewsVisablilty(for text: String?) { + private func updateViewsVisibility(for text: String?) { symbolLabel.isHidden = text.isNilOrEmpty buttonsStack.isHidden = !text.isNilOrEmpty setNeedsLayout() @@ -171,19 +171,19 @@ final class SwapSlippageInputView: BackgroundedContentControl { } } -extension SwapSlippageInputView: UITextFieldDelegate { +extension PercentInputView: UITextFieldDelegate { func textField( _: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String ) -> Bool { let shouldChangeCharacters = inputViewModel?.didReceiveReplacement(string, for: range) ?? false - updateViewsVisablilty(for: inputViewModel?.displayAmount) + updateViewsVisibility(for: inputViewModel?.displayAmount) return shouldChangeCharacters } func textFieldShouldClear(_ textField: UITextField) -> Bool { - updateViewsVisablilty(for: "") + updateViewsVisibility(for: "") if let text = textField.text { inputViewModel?.didReceiveReplacement("", for: NSRange(location: 0, length: text.count)) textField.text = "" @@ -194,16 +194,16 @@ extension SwapSlippageInputView: UITextFieldDelegate { } } -extension SwapSlippageInputView: AmountInputViewModelObserver { +extension PercentInputView: AmountInputViewModelObserver { func amountInputDidChange() { textField.text = inputViewModel?.displayAmount - updateViewsVisablilty(for: textField.text) + updateViewsVisibility(for: textField.text) sendActions(for: .editingChanged) } } -extension SwapSlippageInputView { +extension PercentInputView { func bind(viewModel: [SlippagePercentViewModel]) { buttonsStack.arrangedSubviews.forEach { $0.removeFromSuperview() @@ -222,11 +222,11 @@ extension SwapSlippageInputView { self.inputViewModel = inputViewModel textField.text = inputViewModel.displayAmount - updateViewsVisablilty(for: textField.text) + updateViewsVisibility(for: textField.text) } } -extension SwapSlippageInputView { +extension PercentInputView { enum Style { case error case normal diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 9e60fe4a77..830849ebae 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -215,7 +215,7 @@ final class SwapSetupPresenter { guard let quote = quote, let accountId = accountId, let quoteArgs = quoteArgs, - let slippage = self.slippage else { + let slippage = slippage else { return } @@ -364,7 +364,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { percent: slippage, chainAsset: payChainAsset ) { [weak self, payChainAsset] slippageValue in - guard payChainAsset == self?.payChainAsset else { + guard payChainAsset.chainAssetId == self?.payChainAsset?.chainAssetId else { return } self?.slippage = slippageValue diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index b819965bc7..0e7a524dca 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -109,18 +109,6 @@ final class SwapSlippagePresenter { view?.didReceiveInput(warning: nil) } } - - private func fraction(from number: Decimal) -> BigRational { - let decimalNumber = NSDecimalNumber(decimal: number) - guard decimalNumber.doubleValue.remainder(dividingBy: 1) != 0 else { - return .init(numerator: BigUInt(decimalNumber.intValue), denominator: 1) - } - - let scale = -number.exponent - let numerator = decimalNumber.multiplying(byPowerOf10: Int16(scale)).intValue - let denominator = Int(truncating: pow(10, scale) as NSNumber) - return .init(numerator: BigUInt(numerator), denominator: BigUInt(denominator)) - } } extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { @@ -167,7 +155,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { func apply() { if let amountInput = amountInput { - let rational = fraction(from: amountInput) + let rational = BigRational.fraction(from: amountInput) completionHandler(rational) wireframe.close(from: view) } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift index 6a99f90c23..c67aa32536 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift @@ -124,7 +124,7 @@ extension SwapSlippageViewController: SwapSlippageViewProtocol { } } -extension SwapSlippageViewController: SwapSlippageInputViewDelegateProtocol { +extension SwapSlippageViewController: PercentInputViewDelegateProtocol { func didSelect(percent: SlippagePercentViewModel, sender _: Any?) { presenter.select(percent: percent) updateActionButton() diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift index 438da94941..cbef633fbe 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift @@ -14,7 +14,7 @@ final class SwapSlippageViewLayout: ScrollableContainerLayoutView { $0.contentInsets = .init(top: 12, left: 0, bottom: 12, right: 0) } - let amountInput = SwapSlippageInputView() + let amountInput = PercentInputView() let actionButton: TriangularedButton = .create { $0.applyDefaultStyle() From 7a75dee7f0293fde2a69413a5ebb055f15595847 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Sun, 22 Oct 2023 19:03:09 +0300 Subject: [PATCH 054/204] PR fixes --- .../iconOptions.imageset/Contents.json | 2 +- .../iconOptions.imageset/options-nav-bar.svg | 3 --- .../options.pdf} | Bin 3420 -> 3420 bytes .../iconSwapSettings.imageset/Contents.json | 12 ------------ novawallet/Common/Model/BigRational.swift | 14 ++++++++++++-- .../Swaps/Setup/SwapSetupViewController.swift | 2 +- .../Slippage/SwapSlippagePresenter.swift | 12 +++--------- 7 files changed, 17 insertions(+), 28 deletions(-) delete mode 100644 novawallet/Assets.xcassets/iconOptions.imageset/options-nav-bar.svg rename novawallet/Assets.xcassets/{iconSwapSettings.imageset/Vector.pdf => iconOptions.imageset/options.pdf} (96%) delete mode 100644 novawallet/Assets.xcassets/iconSwapSettings.imageset/Contents.json diff --git a/novawallet/Assets.xcassets/iconOptions.imageset/Contents.json b/novawallet/Assets.xcassets/iconOptions.imageset/Contents.json index f0ea4c388b..22c5f4d411 100644 --- a/novawallet/Assets.xcassets/iconOptions.imageset/Contents.json +++ b/novawallet/Assets.xcassets/iconOptions.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "options-nav-bar.svg", + "filename" : "options.pdf", "idiom" : "universal" } ], diff --git a/novawallet/Assets.xcassets/iconOptions.imageset/options-nav-bar.svg b/novawallet/Assets.xcassets/iconOptions.imageset/options-nav-bar.svg deleted file mode 100644 index 947fcf23dd..0000000000 --- a/novawallet/Assets.xcassets/iconOptions.imageset/options-nav-bar.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/novawallet/Assets.xcassets/iconSwapSettings.imageset/Vector.pdf b/novawallet/Assets.xcassets/iconOptions.imageset/options.pdf similarity index 96% rename from novawallet/Assets.xcassets/iconSwapSettings.imageset/Vector.pdf rename to novawallet/Assets.xcassets/iconOptions.imageset/options.pdf index 3a6480bc77c02f3abdaa25b92b9189ecb1038688..30156b9a2bd04d9fa1d9c52a379d8d30b7ec80e3 100644 GIT binary patch delta 38 kcmca3bw_H#PA(%o129l9*?9OG7q BigRational { let decimalNumber = NSDecimalNumber(decimal: number) guard decimalNumber.doubleValue.remainder(dividingBy: 1) != 0 else { - return .init(numerator: BigUInt(decimalNumber.intValue), denominator: 1) + return .init(numerator: BigUInt(decimalNumber.int64Value), denominator: 1) } - let scale = -number.exponent let numerator = decimalNumber.multiplying(byPowerOf10: Int16(scale)).intValue let denominator = Int(truncating: pow(10, scale) as NSNumber) return .init(numerator: BigUInt(numerator), denominator: BigUInt(denominator)) } } + +extension BigRational { + var decimalValue: Decimal? { + guard denominator != 0 else { + return nil + } + let numerator = numerator.decimal(precision: 0) + let denominator = denominator.decimal(precision: 0) + return numerator / denominator + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index ef283b4454..d39871cfe0 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -100,7 +100,7 @@ final class SwapSetupViewController: UIViewController, ViewHolder { private func setupNavigationItem() { navigationItem.rightBarButtonItem = UIBarButtonItem( - image: R.image.iconSwapSettings(), + image: R.image.iconOptions(), style: .plain, target: self, action: #selector(settingsAction) diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index 0e7a524dca..7a85d1e662 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -11,8 +11,8 @@ final class SwapSlippagePresenter { let prefilledPercents: [Decimal] = [0.1, 1, 3] let initPercent: BigRational? let chainAsset: ChainAsset - let amountRestriction: (lower: Decimal, upper: Decimal) = (lower: 0.01, upper: 50) - let amountRecommendation: (lower: Decimal, upper: Decimal) = (lower: 0.1, upper: 2.3) + let amountRestriction: (lower: Decimal, upper: Decimal) = (lower: 0.1, upper: 50) + let amountRecommendation: (lower: Decimal, upper: Decimal) = (lower: 0.1, upper: 5) private var percentFormatter: NumberFormatter private var numberFormatter: NumberFormatter @@ -47,13 +47,7 @@ final class SwapSlippagePresenter { } private func initialPercent() -> Decimal? { - if let percent = initPercent, percent.denominator != 0 { - let numerator = percent.numerator.decimal(precision: 0) - let denominator = percent.denominator.decimal(precision: 0) - return numerator / denominator - } else { - return nil - } + initPercent?.decimalValue } private func provideAmountViewModel() { From dc3e0715c4f8663578e4f454c237e9e8327ab4f5 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Mon, 23 Oct 2023 12:02:35 +0300 Subject: [PATCH 055/204] bugfixes --- novawallet/Common/Model/BigRational.swift | 15 ++++++++++----- .../View/TextInputView/PercentInputView.swift | 3 ++- .../Modules/Swaps/Setup/SwapSetupPresenter.swift | 3 +-- .../Swaps/Slippage/SwapSlippagePresenter.swift | 5 +++-- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/novawallet/Common/Model/BigRational.swift b/novawallet/Common/Model/BigRational.swift index 81ffb67e3f..76ef67d6d6 100644 --- a/novawallet/Common/Model/BigRational.swift +++ b/novawallet/Common/Model/BigRational.swift @@ -17,15 +17,20 @@ extension BigRational { } extension BigRational { - static func fraction(from number: Decimal) -> BigRational { + static func fraction(from number: Decimal) -> BigRational? { let decimalNumber = NSDecimalNumber(decimal: number) guard decimalNumber.doubleValue.remainder(dividingBy: 1) != 0 else { - return .init(numerator: BigUInt(decimalNumber.int64Value), denominator: 1) + return number.toSubstrateAmount(precision: 0).map { + BigRational(numerator: $0, denominator: 1) + } } let scale = -number.exponent - let numerator = decimalNumber.multiplying(byPowerOf10: Int16(scale)).intValue - let denominator = Int(truncating: pow(10, scale) as NSNumber) - return .init(numerator: BigUInt(numerator), denominator: BigUInt(denominator)) + if let numerator = number.toSubstrateAmount(precision: Int16(scale)), + let denominator = Decimal(1).toSubstrateAmount(precision: Int16(scale)) { + return .init(numerator: numerator, denominator: denominator) + } + + return nil } } diff --git a/novawallet/Common/View/TextInputView/PercentInputView.swift b/novawallet/Common/View/TextInputView/PercentInputView.swift index 4810f4fd28..667dd93b02 100644 --- a/novawallet/Common/View/TextInputView/PercentInputView.swift +++ b/novawallet/Common/View/TextInputView/PercentInputView.swift @@ -21,7 +21,7 @@ final class PercentInputView: BackgroundedContentControl { ) $0.keyboardType = .decimalPad - $0.clearButtonMode = .whileEditing + $0.clearButtonMode = .always } let symbolLabel: UILabel = .create { @@ -166,6 +166,7 @@ final class PercentInputView: BackgroundedContentControl { private func updateViewsVisibility(for text: String?) { symbolLabel.isHidden = text.isNilOrEmpty buttonsStack.isHidden = !text.isNilOrEmpty + textField.clearButtonMode = text.isNilOrEmpty ? .never : .always setNeedsLayout() layoutIfNeeded() } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 830849ebae..d753152004 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -27,7 +27,6 @@ final class SwapSetupPresenter { private var feeIdentifier: String? private var accountId: AccountId? - private var splippage: BigRational = .percent(of: 1) init( interactor: SwapSetupInteractorInputProtocol, @@ -297,7 +296,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideButtonState() provideSettingsState() // TODO: get from settings - slippage = .percent(of: 1) + slippage = .init(numerator: 1, denominator: 10) interactor.setup() } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index 7a85d1e662..13490082f0 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -117,6 +117,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { amountInput = initialPercent() provideResetButtonState() provideAmountViewModel() + provideWarnings() view?.didReceivePreFilledPercents(viewModel: viewModel) } @@ -148,8 +149,8 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { } func apply() { - if let amountInput = amountInput { - let rational = BigRational.fraction(from: amountInput) + if let amountInput = amountInput, + let rational = BigRational.fraction(from: amountInput) { completionHandler(rational) wireframe.close(from: view) } From 2b16816a48a14b3ffe68ec7f9d35395a3742b670 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Mon, 23 Oct 2023 12:36:46 +0300 Subject: [PATCH 056/204] add info --- novawallet.xcodeproj/project.pbxproj | 8 +++-- .../Protocols/ShortTextInfoPresentable.swift | 34 +++++++++++++++++++ .../Swaps/Setup/SwapSetupPresenter.swift | 4 +-- .../Swaps/Setup/SwapSetupProtocols.swift | 2 +- .../Swaps/Setup/SwapSetupWireframe.swift | 22 +----------- .../Slippage/SwapSlippagePresenter.swift | 12 ++++++- .../Slippage/SwapSlippageProtocols.swift | 2 +- .../Slippage/SwapSlippageViewLayout.swift | 4 +-- novawallet/en.lproj/Localizable.strings | 1 + novawallet/ru.lproj/Localizable.strings | 1 + 10 files changed, 60 insertions(+), 30 deletions(-) create mode 100644 novawallet/Common/Protocols/ShortTextInfoPresentable.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 615158fec6..f61bdfc5d5 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -682,6 +682,7 @@ 770F578B2A8A48FF005FD7C1 /* ButtonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770F578A2A8A48FF005FD7C1 /* ButtonViewModel.swift */; }; 77171CA82A98BBA10032B387 /* NominationPoolErrorPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77171CA72A98BBA10032B387 /* NominationPoolErrorPresentable.swift */; }; 77171CAA2A98BC420032B387 /* NominationPoolDataValidatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77171CA92A98BC420032B387 /* NominationPoolDataValidatorFactory.swift */; }; + 7719019B2AE670AE00D9C918 /* ShortTextInfoPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719019A2AE670AE00D9C918 /* ShortTextInfoPresentable.swift */; }; 77204EA42A1CD59100BBDE4A /* GenericBorderedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */; }; 77204EA62A1E0EAA00BBDE4A /* WalletConnectionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA52A1E0EAA00BBDE4A /* WalletConnectionsView.swift */; }; 7725062C2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725062B2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift */; }; @@ -710,9 +711,9 @@ 77740BBC2AD4A7B800E8C06F /* CollapsableContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBB2AD4A7B800E8C06F /* CollapsableContainerView.swift */; }; 77740BBE2AD4A7F500E8C06F /* SwapDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */; }; 77740BC02AD4A80D00E8C06F /* SwapRateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */; }; + 77740BC22AD69E3400E8C06F /* SwapMaxButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BC12AD69E3400E8C06F /* SwapMaxButtonView.swift */; }; 77740BC42AD8145500E8C06F /* PercentInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BC32AD8145500E8C06F /* PercentInputView.swift */; }; 77740BC62AD849D100E8C06F /* SlippagePercentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BC52AD849D100E8C06F /* SlippagePercentViewModel.swift */; }; - 77740BC22AD69E3400E8C06F /* SwapMaxButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BC12AD69E3400E8C06F /* SwapMaxButtonView.swift */; }; 77799ADD2A74219A00B7E564 /* ButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799ADC2A74219A00B7E564 /* ButtonState.swift */; }; 77799ADF2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799ADE2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift */; }; 77799AE52A792AE700B7E564 /* StakingTypeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799AE42A792AE700B7E564 /* StakingTypeViewModel.swift */; }; @@ -4697,6 +4698,7 @@ 770F578A2A8A48FF005FD7C1 /* ButtonViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonViewModel.swift; sourceTree = ""; }; 77171CA72A98BBA10032B387 /* NominationPoolErrorPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolErrorPresentable.swift; sourceTree = ""; }; 77171CA92A98BC420032B387 /* NominationPoolDataValidatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolDataValidatorFactory.swift; sourceTree = ""; }; + 7719019A2AE670AE00D9C918 /* ShortTextInfoPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortTextInfoPresentable.swift; sourceTree = ""; }; 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericBorderedView.swift; sourceTree = ""; }; 77204EA52A1E0EAA00BBDE4A /* WalletConnectionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectionsView.swift; sourceTree = ""; }; 7725062B2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferendumSearchViewLayout.swift; sourceTree = ""; }; @@ -4726,9 +4728,9 @@ 77740BBB2AD4A7B800E8C06F /* CollapsableContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsableContainerView.swift; sourceTree = ""; }; 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapDetailsView.swift; sourceTree = ""; }; 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRateView.swift; sourceTree = ""; }; + 77740BC12AD69E3400E8C06F /* SwapMaxButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapMaxButtonView.swift; sourceTree = ""; }; 77740BC32AD8145500E8C06F /* PercentInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PercentInputView.swift; sourceTree = ""; }; 77740BC52AD849D100E8C06F /* SlippagePercentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlippagePercentViewModel.swift; sourceTree = ""; }; - 77740BC12AD69E3400E8C06F /* SwapMaxButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapMaxButtonView.swift; sourceTree = ""; }; 77799ADC2A74219A00B7E564 /* ButtonState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonState.swift; sourceTree = ""; }; 77799ADE2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoRelaychainWireframe.swift; sourceTree = ""; }; 77799AE42A792AE700B7E564 /* StakingTypeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingTypeViewModel.swift; sourceTree = ""; }; @@ -13271,6 +13273,7 @@ 0CC2E5692A6E6EBB004092E7 /* LocalStorageProviderObserving.swift */, 772540352AC45CDB002B3FD4 /* PurchaseFlowManaging.swift */, 772540372AC45D22002B3FD4 /* PurchasePresentable.swift */, + 7719019A2AE670AE00D9C918 /* ShortTextInfoPresentable.swift */, ); path = Protocols; sourceTree = ""; @@ -21005,6 +21008,7 @@ 848DE76226D62AFE0045CD29 /* PersistentStoreCoordinator+Extensions.swift in Sources */, 845FB6752A2274890003BCA6 /* MultistakingRepositoryFactory.swift in Sources */, 9942034DCB680824831B0AC1 /* AccountCreatePresenter.swift in Sources */, + 7719019B2AE670AE00D9C918 /* ShortTextInfoPresentable.swift in Sources */, 840E59B72A187A9200BA6ADD /* GladingPatternView.swift in Sources */, AEA2C1B82681E9BD0069492E /* ValidatorSearchWireframe.swift in Sources */, 846952A62852A1E60083E0B4 /* AuraStakingDurationFactory.swift in Sources */, diff --git a/novawallet/Common/Protocols/ShortTextInfoPresentable.swift b/novawallet/Common/Protocols/ShortTextInfoPresentable.swift new file mode 100644 index 0000000000..5603e2caea --- /dev/null +++ b/novawallet/Common/Protocols/ShortTextInfoPresentable.swift @@ -0,0 +1,34 @@ +import SoraFoundation +import SoraUI + +protocol ShortTextInfoPresentable { + func showInfo( + from view: ControllerBackedProtocol?, + title: LocalizableResource, + details: LocalizableResource + ) +} + +extension ShortTextInfoPresentable { + func showInfo( + from view: ControllerBackedProtocol?, + title: LocalizableResource, + details: LocalizableResource + ) { + let viewModel = TitleDetailsSheetViewModel( + title: title, + message: details, + mainAction: nil, + secondaryAction: nil + ) + + let bottomSheet = TitleDetailsSheetViewFactory.createContentSizedView(from: viewModel) + + let factory = ModalSheetPresentationFactory(configuration: ModalSheetPresentationConfiguration.nova) + + bottomSheet.controller.modalTransitioningFactory = factory + bottomSheet.controller.modalPresentationStyle = .custom + + view?.controller.present(bottomSheet.controller, animated: true) + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 8fd8085bab..b6fb072d85 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -139,11 +139,11 @@ final class SwapSetupPresenter { view?.didReceiveAmount(receiveInputViewModel: amountInputViewModel) } - private func provideSettingsState() { + private func provideSettingsState() { view?.didReceiveSettingsState(isAvailable: payChainAsset != nil) } - private func getPayAmount(for input: AmountInputResult?) -> Decimal? { + private func getPayAmount(for input: AmountInputResult?) -> Decimal? { guard let input = input, let balanceMinusFee = balanceMinusFee() else { return nil } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 530a9f5ef6..4ac3a68446 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -51,7 +51,7 @@ protocol SwapSetupInteractorOutputProtocol: AnyObject { func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId: AccountId) } -protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, ErrorPresentable { +protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, ErrorPresentable, ShortTextInfoPresentable { func showPayTokenSelection( from view: ControllerBackedProtocol?, chainAsset: ChainAsset?, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index b3b6eb9045..e0aceeaee8 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -48,6 +48,7 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { view?.controller.present(navigationController, animated: true, completion: nil) } + func showSettings( from view: ControllerBackedProtocol?, percent: BigRational?, @@ -67,25 +68,4 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { animated: true ) } - func showInfo( - from view: ControllerBackedProtocol?, - title: LocalizableResource, - details: LocalizableResource - ) { - let viewModel = TitleDetailsSheetViewModel( - title: title, - message: details, - mainAction: nil, - secondaryAction: nil - ) - - let bottomSheet = TitleDetailsSheetViewFactory.createContentSizedView(from: viewModel) - - let factory = ModalSheetPresentationFactory(configuration: ModalSheetPresentationConfiguration.nova) - - bottomSheet.controller.modalTransitioningFactory = factory - bottomSheet.controller.modalPresentationStyle = .custom - - view?.controller.present(bottomSheet.controller, animated: true) - } } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index 13490082f0..da83ebc7b3 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -137,7 +137,17 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { } func showSlippageInfo() { - // TODO: show bottomsheet + let title = LocalizableResource { + R.string.localizable.swapsSetupSlippage(preferredLanguages: $0.rLanguages) + } + let details = LocalizableResource { + R.string.localizable.swapsSetupSlippageDescription(preferredLanguages: $0.rLanguages) + } + wireframe.showInfo( + from: view, + title: title, + details: details + ) } func reset() { diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift index fca6c7c17c..07c73056c1 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift @@ -17,6 +17,6 @@ protocol SwapSlippagePresenterProtocol: AnyObject { func reset() } -protocol SwapSlippageWireframeProtocol: AnyObject { +protocol SwapSlippageWireframeProtocol: AnyObject, ShortTextInfoPresentable { func close(from view: ControllerBackedProtocol?) } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift index cbef633fbe..a4048bd332 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewLayout.swift @@ -8,10 +8,10 @@ final class SwapSlippageViewLayout: ScrollableContainerLayoutView { with: R.color.colorIconSecondary()! ) $0.imageWithTitleView?.titleColor = R.color.colorTextPrimary() - $0.imageWithTitleView?.titleFont = .regularFootnote + $0.imageWithTitleView?.titleFont = .semiBoldBody $0.imageWithTitleView?.spacingBetweenLabelAndIcon = 4 $0.imageWithTitleView?.layoutType = .horizontalLabelFirst - $0.contentInsets = .init(top: 12, left: 0, bottom: 12, right: 0) + $0.contentInsets = .init(top: 0, left: 0, bottom: 12, right: 0) } let amountInput = PercentInputView() diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 2a53dcfd8a..062e018ebd 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1394,6 +1394,7 @@ "swaps.setup.slippage.error.amount.bounds" = "Enter a value between %@ and %@"; "swaps.setup.slippage.warning.low.amount" = "Transaction might be reverted because of low slippage tolerance."; "swaps.setup.slippage.warning.high.amount" = "Transaction might be frontrun because of high slippage."; +"swaps.setup.slippage.description" = "Swap slippage is a common occurrence in decentralized trading where the final price of a swap transaction might slightly differ from the expected price, due to changing market conditions."; "swaps.rate.description" = "Exchange rate between two different cryptocurrencies. It represents how much of one cryptocurrency you can get in exchange for a certain amount of another cryptocurrency."; "swaps.network.fee.description" = "A network fees charged by the blockchain to process and validate any transactions. May vary depending on network conditions or transaction speed."; "common.alert.external.link.disclaimer.title" = "Continue in browser?"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 224da90b00..9427b37472 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1394,6 +1394,7 @@ "swaps.setup.slippage.error.amount.bounds" = "Введите значение между %@ и %@"; "swaps.setup.slippage.warning.low.amount" = "Транзакция может быть отменена из-за низкого значения проскальзывания."; "swaps.setup.slippage.warning.high.amount" = "Транзакция может быть приостановлена из-за высокого значения проскальзывания."; +"swaps.setup.slippage.description" = "Обменное проскальзывание - обычное явление в децентрализованной торговле, где окончательная цена транзакции обмена может незначительно отличаться от ожидаемой цены из-за изменения рыночных условий."; "swaps.rate.description" = "Обменный курс между двумя различными криптовалютами. Он представляет, сколько одной криптовалюты вы можете получить в обмен на определенное количество другой криптовалюты."; "swaps.network.fee.description" = "Это комиссия сети, взимаемая блокчейном за обработку и подтверждение любых транзакций. Она может изменяться в зависимости от условий в сети или скорости выполнения транзакции."; "common.alert.external.link.disclaimer.title" = "Продолжить в браузере?"; From 71fff9da9b6038d100c665c8fb881d5420d3da6e Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Mon, 23 Oct 2023 12:52:20 +0300 Subject: [PATCH 057/204] PR fixes --- .../Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift | 2 +- novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift index b7fcb2f897..fa0345d5f3 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift @@ -24,7 +24,7 @@ extension SwapSetupPresenter { ), dataValidatingFactory.canPayFeeSpendingAmountInPlank( balance: payAssetBalance?.transferable, - fee: payChainAsset == feeChainAsset ? fee : nil, + fee: payChainAsset.chainAssetId == feeChainAsset.chainAssetId ? fee : nil, spendingAmount: spendingAmount, asset: feeChainAsset.assetDisplayInfo, locale: selectedLocale diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 984339054a..2fbd134ccc 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -155,7 +155,7 @@ final class SwapSetupPresenter { } guard let transferableBalance = Decimal.fromSubstrateAmount( payAssetBalance?.transferable ?? 0, - precision: Int16(payChainAsset.asset.precision) + precision: Int16(payChainAsset.assetDisplayInfo.assetPrecision) ) else { return nil } @@ -294,7 +294,7 @@ final class SwapSetupPresenter { private func refreshQuoteForSell(payChainAsset: ChainAsset, receiveChainAsset: ChainAsset, forceUpdate: Bool) { if let payInPlank = getPayAmount(for: payAmountInput)?.toSubstrateAmount( - precision: Int16(payChainAsset.asset.precision)), payInPlank > 0 { + precision: Int16(payChainAsset.assetDisplayInfo.assetPrecision)), payInPlank > 0 { let quoteArgs = AssetConversion.QuoteArgs( assetIn: payChainAsset.chainAssetId, assetOut: receiveChainAsset.chainAssetId, From b6c49217f04a45afb37c1d3f4a275085196d0cc5 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 23 Oct 2023 13:57:21 +0200 Subject: [PATCH 058/204] fix slippage conversion --- Podfile.lock | 2 +- novawallet/Common/Model/BigRational.swift | 16 ++++++++++++++++ .../Modules/Swaps/Setup/SwapSetupPresenter.swift | 6 +++--- .../Swaps/Slippage/SwapSlippagePresenter.swift | 2 +- .../Swaps/Slippage/SwapSlippageViewFactory.swift | 2 +- novawallet/ru.lproj/Localizable.strings | 4 ++-- 6 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 7581bee044..9500b91c99 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -305,4 +305,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f37e3724d47617fb7ce7ed5e0a583491617b5899 -COCOAPODS: 1.13.0 +COCOAPODS: 1.12.1 diff --git a/novawallet/Common/Model/BigRational.swift b/novawallet/Common/Model/BigRational.swift index 76ef67d6d6..97488e5ddc 100644 --- a/novawallet/Common/Model/BigRational.swift +++ b/novawallet/Common/Model/BigRational.swift @@ -11,6 +11,22 @@ struct BigRational: Hashable { } extension BigRational { + func toPercents() -> BigRational { + let (quotient, reminder) = denominator.quotientAndRemainder(dividingBy: 100) + + if quotient > 0, reminder == 0 { + return .init(numerator: numerator, denominator: quotient) + } else { + return .init(numerator: numerator * 100, denominator: denominator) + } + } + + func fromPercents() -> BigRational { + let newDenominator = denominator * 100 + + return .init(numerator: numerator, denominator: newDenominator) + } + static func percent(of numerator: BigUInt) -> BigRational { .init(numerator: numerator, denominator: 100) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index b6fb072d85..4d3f484ea5 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -343,7 +343,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideButtonState() provideSettingsState() // TODO: get from settings - slippage = .init(numerator: 1, denominator: 10) + slippage = .fraction(from: 0.5)?.fromPercents() interactor.setup() } @@ -483,7 +483,7 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { } case let .price(_, priceId): handlePriceError(priceId: priceId) - case let .assetBalance(_, chainAssetId, accountId): + case let .assetBalance(_, chainAssetId, _): handleAssetBalanceError(chainAssetId: chainAssetId) } } @@ -555,7 +555,7 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { } if chainAsset == feeChainAsset?.chainAssetId { feeAssetBalance = balance - if case let .rate = payAmountInput { + if case .rate = payAmountInput { providePayInputPriceViewModel() providePayAmountInputViewModel() provideButtonState() diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index da83ebc7b3..726a886c84 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -160,7 +160,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { func apply() { if let amountInput = amountInput, - let rational = BigRational.fraction(from: amountInput) { + let rational = BigRational.fraction(from: amountInput)?.fromPercents() { completionHandler(rational) wireframe.close(from: view) } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift index ce4f173737..e1371b60cc 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift @@ -17,7 +17,7 @@ struct SwapSlippageViewFactory { numberFormatterLocalizable: amountFormatter.localizableResource(), percentFormatterLocalizable: percentFormatter.localizableResource(), localizationManager: LocalizationManager.shared, - initPercent: percent, + initPercent: percent?.toPercents(), chainAsset: chainAsset, completionHandler: completionHandler ) diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 9427b37472..e898b25684 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -831,7 +831,7 @@ "account.management.watch.only.hint" = "Это watch-only кошелек, Nova может показать вам баланс и другую информацию, но вы не можете выполнять какие-либо транзакции с этим кошельком."; "common.select.wallet" = "Выберите кошелек"; "welcome.import.subtitle" = "Nova совместима со всеми приложениями"; -"welcome.watch.only.subtitle" = "Отследить л��бой кошелек по его адресу"; +"welcome.watch.only.subtitle" = "Отследить любой кошелек по его адресу"; "common.ok.back" = "Хорошо, назад"; "no.key.title" = "Упс! Ключ отсутствует"; "no.key.message" = "Ваш кошелек доступен только для просмотра, то есть вы не можете выполнять с ним никаких операций"; @@ -1183,7 +1183,7 @@ "gov.remove.votes.ask.details" = "Вы ранее голосовали в референдумах в %@. Для того, чтобы сделать эти треки доступными для делегирования, вам необходимо удалить голоса."; "delegations.list.empty" = "Список делегаций появится здесь"; "gov.add.delegate.voting.error.title" = "Вы уже голосовали"; -"gov.add.delegate.voting.error.message" = "Пожалуйста, удалите голоса в выбра��ных треках для начала делегирования"; +"gov.add.delegate.voting.error.message" = "Пожалуйста, удалите голоса в выбранных треках для начала делегирования"; "gov.revoke.delegate.missing.error.title" = "Делегация уже отменена"; "gov.revoke.delegate.missing.error.message" = "Вы уже отменили делегацию в одном из треков"; "common.multi.tx.error.no.details.message" = "При отправке транзакций возникла ошибка (%@). Вы хотите повторить?"; From 05c465c82b81aa9f5aa944c99d2348d886187f72 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 24 Oct 2023 08:49:44 +0300 Subject: [PATCH 059/204] init --- Podfile.lock | 2 +- novawallet.xcodeproj/project.pbxproj | 56 +++++++++++++ .../iconForward.imageset/Contents.json | 12 +++ .../iconForward.imageset/arrow-forward.pdf | Bin 0 -> 1356 bytes .../Swaps/Confirm/SwapConfirmInteractor.swift | 7 ++ .../Swaps/Confirm/SwapConfirmPresenter.swift | 21 +++++ .../Swaps/Confirm/SwapConfirmProtocols.swift | 11 +++ .../Confirm/SwapConfirmViewController.swift | 29 +++++++ .../Confirm/SwapConfirmViewFactory.swift | 17 ++++ .../Swaps/Confirm/SwapConfirmViewLayout.swift | 43 ++++++++++ .../Swaps/Confirm/SwapConfirmWireframe.swift | 3 + .../Swaps/Confirm/SwapElementView.swift | 77 ++++++++++++++++++ .../Modules/Swaps/Confirm/SwapPairView.swift | 43 ++++++++++ .../SwapConfirm/SwapConfirmTests.swift | 16 ++++ 14 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 novawallet/Assets.xcassets/iconForward.imageset/Contents.json create mode 100644 novawallet/Assets.xcassets/iconForward.imageset/arrow-forward.pdf create mode 100644 novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift create mode 100644 novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift create mode 100644 novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift create mode 100644 novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift create mode 100644 novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift create mode 100644 novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift create mode 100644 novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift create mode 100644 novawallet/Modules/Swaps/Confirm/SwapElementView.swift create mode 100644 novawallet/Modules/Swaps/Confirm/SwapPairView.swift create mode 100644 novawalletTests/Modules/SwapConfirm/SwapConfirmTests.swift diff --git a/Podfile.lock b/Podfile.lock index 9500b91c99..7581bee044 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -305,4 +305,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f37e3724d47617fb7ce7ed5e0a583491617b5899 -COCOAPODS: 1.12.1 +COCOAPODS: 1.13.0 diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 1ddf6eef7f..5d22c160f6 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -350,6 +350,7 @@ 22403E58019260719055E122 /* AdvancedWalletWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0DB5EA5195D9433A4B90793 /* AdvancedWalletWireframe.swift */; }; 2262277544A1D9CB46EF087A /* ParaStkYieldBoostStopProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD0DBD280596DBBC5CE5A8F /* ParaStkYieldBoostStopProtocols.swift */; }; 2272FB0A01000A46D097634E /* GovernanceUnlockConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E067006C1BC9DFCA5E8DB86 /* GovernanceUnlockConfirmWireframe.swift */; }; + 232B205831DF28B772515DF2 /* SwapConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CD1FBC9C063951E2520265D /* SwapConfirmWireframe.swift */; }; 233CB11F486DE1953D977295 /* WalletsListViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7073BBC153295FF46FD06FB3 /* WalletsListViewLayout.swift */; }; 2368E8BFA569B8D007F6244F /* AssetsSettingsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFC5052A062548D20D232DA /* AssetsSettingsWireframe.swift */; }; 2450083471CD071346371995 /* MessageSheetWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14383723F0B56C91A0B3016E /* MessageSheetWireframe.swift */; }; @@ -358,6 +359,7 @@ 255D7AEBA45EFA5324D92371 /* DAppListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED66939D92756F608FA11520 /* DAppListPresenter.swift */; }; 25993E2E536DE682E1DFC9AD /* ParaStkCollatorsSearchInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841C1EE99F5EA1713BA3F313 /* ParaStkCollatorsSearchInteractor.swift */; }; 25E4B008933E2EF7F2FAAA46 /* StakingMoreOptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2FE7131747C08259EB98AC /* StakingMoreOptionsViewController.swift */; }; + 262583559B47705A3021EA67 /* SwapConfirmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7DDDF162206B4BFF6D5935A /* SwapConfirmTests.swift */; }; 26533668754DB6C1DF2425AB /* TokenManageSingleViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19485BAFCB2056BDC135441 /* TokenManageSingleViewFactory.swift */; }; 265C6E00915F2F186551A67B /* NPoolsUnstakeSetupViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3246115B53EFE9461CD2F68B /* NPoolsUnstakeSetupViewFactory.swift */; }; 270C21973CB61F0BF3D2D1E3 /* CrowdloanListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02ACCC85B2CCF3D9392CA9B4 /* CrowdloanListProtocols.swift */; }; @@ -679,6 +681,8 @@ 770F578B2A8A48FF005FD7C1 /* ButtonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770F578A2A8A48FF005FD7C1 /* ButtonViewModel.swift */; }; 77171CA82A98BBA10032B387 /* NominationPoolErrorPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77171CA72A98BBA10032B387 /* NominationPoolErrorPresentable.swift */; }; 77171CAA2A98BC420032B387 /* NominationPoolDataValidatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77171CA92A98BC420032B387 /* NominationPoolDataValidatorFactory.swift */; }; + 7719019D2AE6996600D9C918 /* SwapPairView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719019C2AE6996600D9C918 /* SwapPairView.swift */; }; + 7719019F2AE6C9DC00D9C918 /* SwapElementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719019E2AE6C9DC00D9C918 /* SwapElementView.swift */; }; 77204EA42A1CD59100BBDE4A /* GenericBorderedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */; }; 77204EA62A1E0EAA00BBDE4A /* WalletConnectionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA52A1E0EAA00BBDE4A /* WalletConnectionsView.swift */; }; 7725062C2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725062B2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift */; }; @@ -3355,6 +3359,7 @@ 88FF5C7F29C8364500D1CB5D /* Caip2+ParseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FF5C7E29C8364500D1CB5D /* Caip2+ParseError.swift */; }; 8916E9179CF5409E65D1B3A6 /* NftDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A46EE888D60C1538A0A3EFC /* NftDetailsProtocols.swift */; }; 893CA0DF4EAD03CC118A2733 /* GovernanceRemoveVotesConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75247AFBEFD8347011098F69 /* GovernanceRemoveVotesConfirmViewFactory.swift */; }; + 8960EC452EA6E7E9105F4762 /* SwapConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DEED7526468089FE8A8989C /* SwapConfirmInteractor.swift */; }; 89724EA9F732D0C967253597 /* ReferendumOnChainVotersViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF3C1CFECE4340E82837FC4 /* ReferendumOnChainVotersViewFactory.swift */; }; 8A19EC93E6A6972327116D80 /* ParaStkStakeConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1139FB38E7D8D25A36726089 /* ParaStkStakeConfirmProtocols.swift */; }; 8A23DD1F4146639EA2F7AEF6 /* LocksViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81B239BD9C150BFE9A82B0 /* LocksViewFactory.swift */; }; @@ -3435,6 +3440,7 @@ A07A987DE3047AF1A786D511 /* DAppListViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11BB49F165F64A7EF6418EB4 /* DAppListViewLayout.swift */; }; A090FF206B56A0E465C62072 /* CrowdloanListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86F7A369E31DCB9ABD556EE9 /* CrowdloanListPresenter.swift */; }; A0EFC9C4C6F0AE9AFDA9A3EA /* NominationPoolSearchPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1090AF9D9F18846E5A73F73C /* NominationPoolSearchPresenter.swift */; }; + A0FB39F2C6192F57B2FAFC37 /* SwapConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48E6BE303472080271AAC917 /* SwapConfirmPresenter.swift */; }; A14308E2633921838166C843 /* ParaStkUnstakeConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F2450A063F4D66EFDF6B8A /* ParaStkUnstakeConfirmViewFactory.swift */; }; A1FA23B8A833B6896104ABA6 /* GovernanceSelectTracksWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE82B42E77A241D336A5B65F /* GovernanceSelectTracksWireframe.swift */; }; A265CC9857E951EB71E5E831 /* WalletsListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DBACA1AB17E90565F133C19 /* WalletsListInteractor.swift */; }; @@ -3653,6 +3659,7 @@ C20ED4531583D0C8E38715E0 /* PurchaseProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782CC21A2F9EEF5DBA3AB1AA /* PurchaseProtocols.swift */; }; C21129B2B8D8B33BCBD5843E /* StakingRedeemViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8821119C96944A0E3526E93A /* StakingRedeemViewFactory.swift */; }; C24EC1F763113B2208BE1ABE /* AssetsSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7280DD0C282CB1631C93DFB /* AssetsSettingsPresenter.swift */; }; + C28A0BFC483095E132705C83 /* SwapConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382B8763E4FDA7C4E2EF06A8 /* SwapConfirmViewLayout.swift */; }; C29BA2F7EBAFED5D118E0C0C /* GovernanceRevokeDelegationConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F98F75F1BCA9143356DBF08 /* GovernanceRevokeDelegationConfirmViewFactory.swift */; }; C2DD387223F4520ADC49AE9B /* NftDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6B5B187E83839481846C7E /* NftDetailsInteractor.swift */; }; C317BBF2F1815F0A8D937428 /* DelegateVotedReferendaViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E86B1DF9D9E8E5DD8DC49DE /* DelegateVotedReferendaViewLayout.swift */; }; @@ -3663,6 +3670,7 @@ C5B07E59C0B00CAD1D0D2DFD /* ReferendumSearchWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72EF3BEC3EA07548C10A87FD /* ReferendumSearchWireframe.swift */; }; C5B6C00F8B0E3D89CBF1A8DB /* ParaStkSelectCollatorsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2336E4CAF4A1F627C39093FF /* ParaStkSelectCollatorsViewFactory.swift */; }; C5B833588833888A8AC2B8EE /* TransactionHistoryInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8511A3B5C31B73CDC794F63 /* TransactionHistoryInteractor.swift */; }; + C61286217D7D54EC70E3F1AA /* SwapConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF5DEDDC53639DFCF524D794 /* SwapConfirmViewFactory.swift */; }; C644308270C29AC6F90CFEA6 /* ReferendumDetailsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E2EB9EE4A87BD4A74040784 /* ReferendumDetailsWireframe.swift */; }; C6A2D4CAEE2E6444507EEDDF /* TokensManageAddWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 182E3BCA71937085FBCBDD1D /* TokensManageAddWireframe.swift */; }; C6DA3ABD47E72ED1661830A9 /* GovernanceDelegateSearchPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3D49E5A3BB8ABFA1448BF43 /* GovernanceDelegateSearchPresenter.swift */; }; @@ -3800,10 +3808,12 @@ E9625CE429290F5504728D62 /* NftListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0AFEA616FDDF846F3F3650 /* NftListWireframe.swift */; }; E9B2CD5127B700881A00EC1D /* TokensAddSelectNetworkInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA518E1D79D86360F145B428 /* TokensAddSelectNetworkInteractor.swift */; }; EA44FEB35901A8BD8B1A4B79 /* StakingDashboardWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3348E0824B8D42EC3850EBD5 /* StakingDashboardWireframe.swift */; }; + EA7BA01C6745ABE0E1AF7E1D /* SwapConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2FB715B933FB8E34A553A80 /* SwapConfirmViewController.swift */; }; EAAB9E53189BC6394C5900D2 /* GovernanceSelectTracksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6962C8E51EB317DE3AAE4BDF /* GovernanceSelectTracksViewController.swift */; }; EAAFB082E2BB0CA418714061 /* ReferendumFullDetailsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57CDCB8CF3D8EBE5DA7A5A30 /* ReferendumFullDetailsViewLayout.swift */; }; EB11BF594D7E16A8885D47DD /* WalletConnectServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B193A261FDF933FE6C874B4E /* WalletConnectServiceFactory.swift */; }; EB20C6B406155664B981BA94 /* GovernanceYourDelegationsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46DF4E6D8DAF6913474DED5 /* GovernanceYourDelegationsInteractor.swift */; }; + EB280B977471EA442D91395E /* SwapConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796859464B823B60746C5DE5 /* SwapConfirmProtocols.swift */; }; EB376E61CD1C39AC148DE80C /* NftListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A60B27D3A045E0DEF23775 /* NftListViewController.swift */; }; EB5F587A71CCE1F0F86154CF /* ControllerAccountViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002A29AE58EB53E915330490 /* ControllerAccountViewFactory.swift */; }; EB877554208E91A80985F1E5 /* NPoolsRedeemViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 862545D6BD501914AE04D776 /* NPoolsRedeemViewController.swift */; }; @@ -4405,6 +4415,7 @@ 2BE0492B98AB9C1540846B39 /* StakingPayoutConfirmationViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPayoutConfirmationViewFactory.swift; sourceTree = ""; }; 2C3F5511D2E7AC283FF021E8 /* GovernanceRevokeDelegationConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceRevokeDelegationConfirmProtocols.swift; sourceTree = ""; }; 2DC70DB97D2E9350022A899B /* TokensManagePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManagePresenter.swift; sourceTree = ""; }; + 2DEED7526468089FE8A8989C /* SwapConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapConfirmInteractor.swift; sourceTree = ""; }; 2E4B0600AFFB96A75CF98755 /* StakingRedeemProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRedeemProtocols.swift; sourceTree = ""; }; 2E5C5EE99A4B73789BE23039 /* Pods-novawalletAll-novawalletIntegrationTests.staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawalletIntegrationTests.staging.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests.staging.xcconfig"; sourceTree = ""; }; 2E800814C025B38C87CC282D /* TokensAddSelectNetworkViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensAddSelectNetworkViewFactory.swift; sourceTree = ""; }; @@ -4455,6 +4466,7 @@ 37C7955BBDC93BC07D069B8F /* ParaStkStakeSetupViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkStakeSetupViewFactory.swift; sourceTree = ""; }; 37D17FFDACA017C3460DE280 /* LedgerNetworkSelectionPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerNetworkSelectionPresenter.swift; sourceTree = ""; }; 37FF46AB59967BF656E9EF1C /* AssetsSettingsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetsSettingsViewFactory.swift; sourceTree = ""; }; + 382B8763E4FDA7C4E2EF06A8 /* SwapConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapConfirmViewLayout.swift; sourceTree = ""; }; 38BBECA4E3F54769D95DD21E /* TokensAddSelectNetworkViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensAddSelectNetworkViewController.swift; sourceTree = ""; }; 392B5AA43C68E640C9FDEE04 /* LedgerAccountConfirmationViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerAccountConfirmationViewFactory.swift; sourceTree = ""; }; 3930902D540DB0B9A2CFD21C /* StakingTypeViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingTypeViewController.swift; sourceTree = ""; }; @@ -4519,6 +4531,7 @@ 48C158C8D1855BCE53636934 /* AccountCreateProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountCreateProtocols.swift; sourceTree = ""; }; 48CECA2C5A0EFEBFDBB3C90C /* DAppOperationConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppOperationConfirmWireframe.swift; sourceTree = ""; }; 48E5BB1EB494B5DB92FC3053 /* Pods-novawalletAll-novawalletIntegrationTests.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawalletIntegrationTests.dev.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests.dev.xcconfig"; sourceTree = ""; }; + 48E6BE303472080271AAC917 /* SwapConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapConfirmPresenter.swift; sourceTree = ""; }; 4A191B92AD171FDDDD8C30E2 /* DAppSearchWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppSearchWireframe.swift; sourceTree = ""; }; 4B243F6751E2277D9FC14481 /* AdvancedWalletViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AdvancedWalletViewFactory.swift; sourceTree = ""; }; 4B6060CE86A7EA49AD05329C /* ParaStkUnstakeConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkUnstakeConfirmViewController.swift; sourceTree = ""; }; @@ -4684,6 +4697,8 @@ 770F578A2A8A48FF005FD7C1 /* ButtonViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonViewModel.swift; sourceTree = ""; }; 77171CA72A98BBA10032B387 /* NominationPoolErrorPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolErrorPresentable.swift; sourceTree = ""; }; 77171CA92A98BC420032B387 /* NominationPoolDataValidatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolDataValidatorFactory.swift; sourceTree = ""; }; + 7719019C2AE6996600D9C918 /* SwapPairView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPairView.swift; sourceTree = ""; }; + 7719019E2AE6C9DC00D9C918 /* SwapElementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapElementView.swift; sourceTree = ""; }; 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericBorderedView.swift; sourceTree = ""; }; 77204EA52A1E0EAA00BBDE4A /* WalletConnectionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectionsView.swift; sourceTree = ""; }; 7725062B2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferendumSearchViewLayout.swift; sourceTree = ""; }; @@ -4847,6 +4862,7 @@ 7911693957DFAF141EBDAFEC /* StakingRewardPayoutsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardPayoutsProtocols.swift; sourceTree = ""; }; 793046EA14E4CAB096803BCD /* NftDetailsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsWireframe.swift; sourceTree = ""; }; 793356FB65FB73CE7097C6F1 /* NPoolsUnstakeConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeConfirmProtocols.swift; sourceTree = ""; }; + 796859464B823B60746C5DE5 /* SwapConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapConfirmProtocols.swift; sourceTree = ""; }; 7A092ADC09DA0429548EBC08 /* NftListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftListPresenter.swift; sourceTree = ""; }; 7ACF32611D345B87BCE29FE0 /* DAppAddFavoriteWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAddFavoriteWireframe.swift; sourceTree = ""; }; 7B13D65B93E65B5112272962 /* DelegationReferendumVotersWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegationReferendumVotersWireframe.swift; sourceTree = ""; }; @@ -4856,6 +4872,7 @@ 7C2883649A5E84DF9B861C83 /* DelegateVotedReferendaViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegateVotedReferendaViewController.swift; sourceTree = ""; }; 7C70EBF83B2547452417E588 /* StakingRewardDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardDetailsViewController.swift; sourceTree = ""; }; 7CBA1296E4C6E04EC9C5CA98 /* ParaStkUnstakePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkUnstakePresenter.swift; sourceTree = ""; }; + 7CD1FBC9C063951E2520265D /* SwapConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapConfirmWireframe.swift; sourceTree = ""; }; 7D8A9948F319AD0D154D1EC4 /* DelegationListViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegationListViewFactory.swift; sourceTree = ""; }; 7DDDB2B35CD3299F50613141 /* ReferralCrowdloanViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferralCrowdloanViewController.swift; sourceTree = ""; }; 7DEDFCF115BB1015C928F3B2 /* ParitySignerWelcomeViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerWelcomeViewLayout.swift; sourceTree = ""; }; @@ -7647,6 +7664,7 @@ AEFED3DAA18BCEF0BFA15728 /* SelectValidatorsStartInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartInteractor.swift; sourceTree = ""; }; AF01941105BCD02536538362 /* CrowdloanContributionConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmProtocols.swift; sourceTree = ""; }; AF34514F17CE47AF0C5A66F6 /* TransferConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferConfirmWireframe.swift; sourceTree = ""; }; + AF5DEDDC53639DFCF524D794 /* SwapConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapConfirmViewFactory.swift; sourceTree = ""; }; AF7E7AAE54BF5E6A35BDD29B /* GovernanceDelegateInfoInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateInfoInteractor.swift; sourceTree = ""; }; AFC1FCEA168E40235A1D3EA6 /* GovernanceDelegateSearchViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateSearchViewController.swift; sourceTree = ""; }; B03974ECD8AEF39FDCA277D7 /* LedgerNetworkSelectionViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerNetworkSelectionViewFactory.swift; sourceTree = ""; }; @@ -7676,6 +7694,7 @@ B73F89021BEE1F4576128305 /* StakingSetupAmountProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSetupAmountProtocols.swift; sourceTree = ""; }; B765BDAA27726E2586953368 /* OnChainTransferSetupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OnChainTransferSetupInteractor.swift; sourceTree = ""; }; B7CB6BF970620958C9DDD037 /* ParaStkStakeConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkStakeConfirmWireframe.swift; sourceTree = ""; }; + B7DDDF162206B4BFF6D5935A /* SwapConfirmTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapConfirmTests.swift; sourceTree = ""; }; B8A6C6207095F63972E14618 /* DAppPhishingProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppPhishingProtocols.swift; sourceTree = ""; }; B8B0A8174A9FFB8422A70D83 /* ParitySignerWelcomeWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerWelcomeWireframe.swift; sourceTree = ""; }; B8B263D5668F1C91E2CF61D9 /* GovernanceRevokeDelegationConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceRevokeDelegationConfirmWireframe.swift; sourceTree = ""; }; @@ -7882,6 +7901,7 @@ F28EDDF9277242505FDDECA1 /* CustomValidatorListProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CustomValidatorListProtocols.swift; sourceTree = ""; }; F2B438707EA6C81C48EAB4CE /* ParaStkYieldBoostStopViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStopViewController.swift; sourceTree = ""; }; F2B676982F60C55530BDD569 /* AccountManagementPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountManagementPresenter.swift; sourceTree = ""; }; + F2FB715B933FB8E34A553A80 /* SwapConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapConfirmViewController.swift; sourceTree = ""; }; F31A3D4E3894582CB49013F0 /* ParaStkYieldBoostStartViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStartViewLayout.swift; sourceTree = ""; }; F32F1F0F6F985195CD19EDDB /* GovernanceEditDelegationTracksProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceEditDelegationTracksProtocols.swift; sourceTree = ""; }; F3526D1FB18C2615BE28E15C /* GovernanceUnavailableTracksViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnavailableTracksViewLayout.swift; sourceTree = ""; }; @@ -9510,6 +9530,14 @@ path = DelegationReferendumVoters; sourceTree = ""; }; + 73DE684949441C60D62A687A /* SwapConfirm */ = { + isa = PBXGroup; + children = ( + B7DDDF162206B4BFF6D5935A /* SwapConfirmTests.swift */, + ); + path = SwapConfirm; + sourceTree = ""; + }; 770F57892A8A48F7005FD7C1 /* Model */ = { isa = PBXGroup; children = ( @@ -9737,6 +9765,7 @@ isa = PBXGroup; children = ( 29BD7DA0076BA8BC3411221A /* Setup */, + 7E5E800395DC908962C169CF /* Confirm */, ); path = Swaps; sourceTree = ""; @@ -9976,6 +10005,22 @@ path = ParaStkRebond; sourceTree = ""; }; + 7E5E800395DC908962C169CF /* Confirm */ = { + isa = PBXGroup; + children = ( + 796859464B823B60746C5DE5 /* SwapConfirmProtocols.swift */, + 7CD1FBC9C063951E2520265D /* SwapConfirmWireframe.swift */, + 48E6BE303472080271AAC917 /* SwapConfirmPresenter.swift */, + 2DEED7526468089FE8A8989C /* SwapConfirmInteractor.swift */, + F2FB715B933FB8E34A553A80 /* SwapConfirmViewController.swift */, + 382B8763E4FDA7C4E2EF06A8 /* SwapConfirmViewLayout.swift */, + AF5DEDDC53639DFCF524D794 /* SwapConfirmViewFactory.swift */, + 7719019C2AE6996600D9C918 /* SwapPairView.swift */, + 7719019E2AE6C9DC00D9C918 /* SwapElementView.swift */, + ); + path = Confirm; + sourceTree = ""; + }; 7F067CADB4CD05233424BD6D /* SessionDetails */ = { isa = PBXGroup; children = ( @@ -14495,6 +14540,7 @@ 84B7C705289BFA79001A3566 /* AccountManagement */, 84B7C708289BFA79001A3566 /* WalletList */, 84B7C70A289BFA79001A3566 /* ControllerAccount */, + 73DE684949441C60D62A687A /* SwapConfirm */, ); path = Modules; sourceTree = ""; @@ -20072,6 +20118,7 @@ 844CB57426FA01DD00396E13 /* SubstrateRepositoryFactory.swift in Sources */, 849013DF24A927E2008F705E /* SettingsExtension.swift in Sources */, 888797B829F269130078633F /* BiometrySettings.swift in Sources */, + 7719019D2AE6996600D9C918 /* SwapPairView.swift in Sources */, 842898CC265A8EC3002D5D65 /* RemoteImageViewModel.swift in Sources */, 84DA3B1424C6D7C700B5E27F /* RuntimeDispatchInfo.swift in Sources */, 840D92A7278EE8690007B979 /* DAppSignBytesConfirmInteractor.swift in Sources */, @@ -21891,6 +21938,7 @@ 840B3D632899BBDD00DA1DA9 /* ParitySignerScanPresenter.swift in Sources */, 84FD91B029B08F7A007851D3 /* BaseTableSearchViewController.swift in Sources */, 1BEADE77C6236CB3BF719A47 /* CrowdloanContributionSetupViewFactory.swift in Sources */, + 7719019F2AE6C9DC00D9C918 /* SwapElementView.swift in Sources */, 845353BD2886EB1A006C871A /* ButtonLargeControl.swift in Sources */, 8466781827EC9CDA007935D3 /* PersistTransferDetails.swift in Sources */, 84800B02273301C800E1E4DD /* RemoteChainModel.swift in Sources */, @@ -23035,6 +23083,13 @@ 8786222ADF4643BE7A6FBBEB /* SwapSetupViewController.swift in Sources */, 350B8A18C9C91DF07D2E53C5 /* SwapSetupViewLayout.swift in Sources */, 66531C7E2E0E99C89A89A35A /* SwapSetupViewFactory.swift in Sources */, + EB280B977471EA442D91395E /* SwapConfirmProtocols.swift in Sources */, + 232B205831DF28B772515DF2 /* SwapConfirmWireframe.swift in Sources */, + A0FB39F2C6192F57B2FAFC37 /* SwapConfirmPresenter.swift in Sources */, + 8960EC452EA6E7E9105F4762 /* SwapConfirmInteractor.swift in Sources */, + EA7BA01C6745ABE0E1AF7E1D /* SwapConfirmViewController.swift in Sources */, + C28A0BFC483095E132705C83 /* SwapConfirmViewLayout.swift in Sources */, + C61286217D7D54EC70E3F1AA /* SwapConfirmViewFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -23195,6 +23250,7 @@ 84B7C748289BFA79001A3566 /* WalletListTests.swift in Sources */, 84B7C720289BFA79001A3566 /* ReferralCrowdloanTests.swift in Sources */, F4897BB126AED13D0075F291 /* EraCountdownOperationFactoryStub.swift in Sources */, + 262583559B47705A3021EA67 /* SwapConfirmTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/novawallet/Assets.xcassets/iconForward.imageset/Contents.json b/novawallet/Assets.xcassets/iconForward.imageset/Contents.json new file mode 100644 index 0000000000..5a580ab3d4 --- /dev/null +++ b/novawallet/Assets.xcassets/iconForward.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "arrow-forward.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconForward.imageset/arrow-forward.pdf b/novawallet/Assets.xcassets/iconForward.imageset/arrow-forward.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a2d45bb2722c7c639f3d2a682ea8efca33755ef3 GIT binary patch literal 1356 zcmZWpO>f&U487}D@KT^XR2C&!A3#xHP10i6hIQ#~#SWgMrWq1@v)!TFuOH>uveOAc zqCAV_drzd#?w7Zh$Sq?C0_yKS7{JvPWUJTi?x8EXaQuh;Pg#KAN(x$VeSQ3}P;Fuu zlPB>XL$&RmK@qs7gRJeEP#jpc48M0}Sl`{i;$i-;Y}qfCtq`^?Lmn$o4KZ#m(=I)e z<1@)~w9Y9g4^w11mNso!F~d<>j~=}|8O5!XPms&WF zFo&#EN=TSO4Hr^4ofD5#0|n2$lvICDtL+gp0NwQ)zCcKyob=OvsE_!lFT=^sy86?3q2WihW_Rqlb1KdpoxAaW;%K zspE;}Y+~M3Ro%7l_>CrrR`&JJ&j8tdu?++8DQtGdP5lawlf0w}=}P+r({9YP4sHF? zl%a(_lMnvs6te*hOy3~exX?WkAF+Z zER#9O6a}`a;3Rxebj7~jo`$|Q;fV>b?5H(Wn)|q*N)Qd@8^{S_aeB&0 zYKRjEV-voGEEzGJ>Y8G=4-Jd&yZ1}7 SwapConfirmViewProtocol? { + let interactor = SwapConfirmInteractor() + let wireframe = SwapConfirmWireframe() + + let presenter = SwapConfirmPresenter(interactor: interactor, wireframe: wireframe) + + let view = SwapConfirmViewController(presenter: presenter) + + presenter.view = view + interactor.presenter = presenter + + return view + } +} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift new file mode 100644 index 0000000000..0204dd464a --- /dev/null +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift @@ -0,0 +1,43 @@ +import UIKit +import SoraUI + +final class SwapConfirmViewLayout: ScrollableContainerLayoutView { + let rateCell: SwapRateView = .create { + $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() + $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote + $0.titleView.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() + $0.addBottomSeparator() + } + + let networkFeeCell = SwapNetworkFeeView(frame: .zero) + + let actionButton: TriangularedButton = .create { + $0.applyDefaultStyle() + } + + override func setupStyle() { + backgroundColor = R.color.colorSecondaryScreenBackground() + } + + override func setupLayout() { + super.setupLayout() + + stackView.layoutMargins = UIEdgeInsets(top: 12, left: 16, bottom: 0, right: 16) + + addSubview(actionButton) + actionButton.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) + make.bottom.equalTo(safeAreaLayoutGuide).inset(UIConstants.actionBottomInset) + make.height.equalTo(UIConstants.actionHeight) + } + } + + func setup(locale: Locale) { + rateCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupDetailsRate( + preferredLanguages: locale.rLanguages) + networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetwork( + preferredLanguages: locale.rLanguages) + rateCell.titleButton.invalidateLayout() + networkFeeCell.titleButton.invalidateLayout() + } +} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift new file mode 100644 index 0000000000..2384b3a875 --- /dev/null +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift @@ -0,0 +1,3 @@ +import Foundation + +final class SwapConfirmWireframe: SwapConfirmWireframeProtocol {} diff --git a/novawallet/Modules/Swaps/Confirm/SwapElementView.swift b/novawallet/Modules/Swaps/Confirm/SwapElementView.swift new file mode 100644 index 0000000000..36ac88a389 --- /dev/null +++ b/novawallet/Modules/Swaps/Confirm/SwapElementView.swift @@ -0,0 +1,77 @@ +import UIKit +import SoraUI + +final class SwapElementView: UIView { + var contentInsets: UIEdgeInsets = .zero { + didSet { + contentView.snp.updateConstraints { + $0.edges.equalToSuperview().inset(contentInsets) + } + } + } + + let backgroundView: RoundedView = .create { + $0.apply(style: .roundedLightCell) + } + + let imageView: AssetIconView = .create { + $0.contentInsets = .zero + $0.backgroundView.cornerRadius = 24 + } + + let valueLabel: UILabel = .init( + style: .semiboldBodyPrimary, + textAlignment: .center, + numberOfLines: 1 + ) + + let priceLabel: UILabel = .init( + style: .footnoteSecondary, + textAlignment: .center, + numberOfLines: 1 + ) + + let hubIconNameView: IconDetailsView = .create { + $0.spacing = 8 + $0.iconWidth = 16 + $0.mode = .iconDetails + $0.detailsLabel.apply(style: .footnoteSecondary) + } + + override var intrinsicContentSize: CGSize { + .init(width: UIView.noIntrinsicMetric, height: 168) + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .clear + + setupLayout() + } + + lazy var contentView = UIView.vStack([ + imageView, + valueLabel, + priceLabel, + hubIconNameView + ]) + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + addSubview(backgroundView) + addSubview(contentView) + + backgroundView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + contentView.snp.makeConstraints { + $0.edges.equalToSuperview().inset(contentInsets) + } + } +} diff --git a/novawallet/Modules/Swaps/Confirm/SwapPairView.swift b/novawallet/Modules/Swaps/Confirm/SwapPairView.swift new file mode 100644 index 0000000000..861a82777b --- /dev/null +++ b/novawallet/Modules/Swaps/Confirm/SwapPairView.swift @@ -0,0 +1,43 @@ +import UIKit +import SoraUI + +final class SwapPairView: UIView { + let leftAssetView = SwapElementView() + let rigthAssetView = SwapElementView() + + let arrowView: UIImageView = .create { + $0.backgroundColor = R.color.colorSecondaryScreenBackground() + $0.layer.cornerRadius = 24 + $0.image = R.image.iconForward() + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .clear + + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + let stackView = UIView.hStack(distribution: .fillEqually, [ + leftAssetView, + rigthAssetView + ]) + addSubview(stackView) + addSubview(arrowView) + + stackView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + arrowView.snp.makeConstraints { + $0.center.equalTo(stackView.snp.center) + } + } +} diff --git a/novawalletTests/Modules/SwapConfirm/SwapConfirmTests.swift b/novawalletTests/Modules/SwapConfirm/SwapConfirmTests.swift new file mode 100644 index 0000000000..d6bd265b37 --- /dev/null +++ b/novawalletTests/Modules/SwapConfirm/SwapConfirmTests.swift @@ -0,0 +1,16 @@ +import XCTest + +class SwapConfirmTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + XCTFail("Did you forget to add tests?") + } +} From 833c7d008d965f8d8e5281e6cb689d119a784140 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 24 Oct 2023 08:57:33 +0300 Subject: [PATCH 060/204] fixes after merge --- Podfile.lock | 2 +- novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 9500b91c99..7581bee044 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -305,4 +305,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f37e3724d47617fb7ce7ed5e0a583491617b5899 -COCOAPODS: 1.12.1 +COCOAPODS: 1.13.0 diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index b638f1d388..15674ac11e 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -490,7 +490,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } } - func showSettings() { + func showSettings() { guard let payChainAsset = payChainAsset else { return } @@ -506,7 +506,6 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { self?.estimateFee() } } - } extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { From fadef3d1ef274464ff61f03fb5133efc56eea7ef Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 24 Oct 2023 10:42:29 +0300 Subject: [PATCH 061/204] add cells --- .../Confirm/SwapConfirmViewController.swift | 26 ++++++++- .../Confirm/SwapConfirmViewFactory.swift | 4 +- .../Swaps/Confirm/SwapConfirmViewLayout.swift | 56 ++++++++++++++++++- novawallet/en.lproj/Localizable.strings | 1 + novawallet/ru.lproj/Localizable.strings | 1 + 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift index 70db14443f..20a7708112 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift @@ -1,13 +1,17 @@ import UIKit +import SoraFoundation -final class SwapConfirmViewController: UIViewController { +final class SwapConfirmViewController: UIViewController, ViewHolder { typealias RootViewType = SwapConfirmViewLayout let presenter: SwapConfirmPresenterProtocol - init(presenter: SwapConfirmPresenterProtocol) { + init(presenter: SwapConfirmPresenterProtocol, + localizationManager: LocalizationManagerProtocol) { self.presenter = presenter super.init(nibName: nil, bundle: nil) + + self.localizationManager = localizationManager } @available(*, unavailable) @@ -22,8 +26,26 @@ final class SwapConfirmViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + setupLocalization() + setupHandlers() presenter.setup() } + + private func setupLocalization() { + rootView.setup(locale: selectedLocale) + } + + private func setupHandlers() { + + } } extension SwapConfirmViewController: SwapConfirmViewProtocol {} + +extension SwapConfirmViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index c3b7750567..c8e0cb3ac4 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -1,4 +1,5 @@ import Foundation +import SoraFoundation struct SwapConfirmViewFactory { static func createView() -> SwapConfirmViewProtocol? { @@ -7,7 +8,8 @@ struct SwapConfirmViewFactory { let presenter = SwapConfirmPresenter(interactor: interactor, wireframe: wireframe) - let view = SwapConfirmViewController(presenter: presenter) + let view = SwapConfirmViewController(presenter: presenter, + localizationManager: LocalizationManager.shared) presenter.view = view interactor.presenter = presenter diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift index 0204dd464a..66ffdd35c5 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift @@ -2,15 +2,46 @@ import UIKit import SoraUI final class SwapConfirmViewLayout: ScrollableContainerLayoutView { + let pairsView = SwapPairView() + + let detailsTableView: StackTableView = .create { + $0.cellHeight = 44 + $0.hasSeparators = true + $0.contentInsets = UIEdgeInsets(top: 0, left: 16, bottom: 8, right: 16) + } + let rateCell: SwapRateView = .create { $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote $0.titleView.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() - $0.addBottomSeparator() + } + + let priceDifferenceCell: SwapRateView = .create { + $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() + $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote + $0.titleView.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() + } + + let slippageCell: SwapRateView = .create { + $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() + $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote + $0.titleView.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() } let networkFeeCell = SwapNetworkFeeView(frame: .zero) + let walletTableView: StackTableView = .create { + $0.cellHeight = 44 + $0.hasSeparators = true + $0.contentInsets = UIEdgeInsets(top: 0, left: 16, bottom: 8, right: 16) + } + + let walletCell = StackTableCell() + + let accountCell: StackInfoTableCell = .create { + $0.detailsLabel.lineBreakMode = .byTruncatingMiddle + } + let actionButton: TriangularedButton = .create { $0.applyDefaultStyle() } @@ -21,9 +52,19 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { override func setupLayout() { super.setupLayout() - stackView.layoutMargins = UIEdgeInsets(top: 12, left: 16, bottom: 0, right: 16) + addArrangedSubview(pairsView, spacingAfter: 8) + addArrangedSubview(detailsTableView, spacingAfter: 8) + detailsTableView.addArrangedSubview(rateCell) + detailsTableView.addArrangedSubview(priceDifferenceCell) + detailsTableView.addArrangedSubview(slippageCell) + detailsTableView.addArrangedSubview(networkFeeCell) + + addArrangedSubview(walletTableView) + walletTableView.addArrangedSubview(walletCell) + walletTableView.addArrangedSubview(accountCell) + addSubview(actionButton) actionButton.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) @@ -33,11 +74,22 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { } func setup(locale: Locale) { + slippageCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupSlippage( + preferredLanguages: locale.rLanguages + ) rateCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupDetailsRate( preferredLanguages: locale.rLanguages) networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetwork( preferredLanguages: locale.rLanguages) rateCell.titleButton.invalidateLayout() networkFeeCell.titleButton.invalidateLayout() + + walletCell.titleLabel.text = R.string.localizable.commonWallet( + preferredLanguages: locale.rLanguages) + accountCell.titleLabel.text = R.string.localizable.commonAccount( + preferredLanguages: locale.rLanguages) + + actionButton.imageWithTitleView?.title = R.string.localizable.commonConfirm( + preferredLanguages: locale.rLanguages) } } diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 062e018ebd..377dd5c68b 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1391,6 +1391,7 @@ "swaps.receive.token.selection.title" = "Token to receive"; "swaps.setup.settings.title" = "Swap settings"; "swaps.setup.slippage" = "Slippage"; +"swaps.setup.price.difference" = "Price difference"; "swaps.setup.slippage.error.amount.bounds" = "Enter a value between %@ and %@"; "swaps.setup.slippage.warning.low.amount" = "Transaction might be reverted because of low slippage tolerance."; "swaps.setup.slippage.warning.high.amount" = "Transaction might be frontrun because of high slippage."; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index e898b25684..59e2ed167b 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1391,6 +1391,7 @@ "swaps.receive.token.selection.title" = "Токен для получения"; "swaps.setup.settings.title" = "Настройки обмена"; "swaps.setup.slippage" = "Slippage"; +"swaps.setup.price.difference" = "Ценовая разница"; "swaps.setup.slippage.error.amount.bounds" = "Введите значение между %@ и %@"; "swaps.setup.slippage.warning.low.amount" = "Транзакция может быть отменена из-за низкого значения проскальзывания."; "swaps.setup.slippage.warning.high.amount" = "Транзакция может быть приостановлена из-за высокого значения проскальзывания."; From 2e927de9ccc922a3c28e87bc7aba65c2a89062a6 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 24 Oct 2023 10:44:18 +0300 Subject: [PATCH 062/204] fix russian localization --- novawallet/ru.lproj/Localizable.strings | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index ed97b7bdda..3de60aa295 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -831,7 +831,7 @@ "account.management.watch.only.hint" = "Это watch-only кошелек, Nova может показать вам баланс и другую информацию, но вы не можете выполнять какие-либо транзакции с этим кошельком."; "common.select.wallet" = "Выберите кошелек"; "welcome.import.subtitle" = "Nova совместима со всеми приложениями"; -"welcome.watch.only.subtitle" = "Отследить л��бой кошелек по его адресу"; +"welcome.watch.only.subtitle" = "Отследить любой кошелек по его адресу"; "common.ok.back" = "Хорошо, назад"; "no.key.title" = "Упс! Ключ отсутствует"; "no.key.message" = "Ваш кошелек доступен только для просмотра, то есть вы не можете выполнять с ним никаких операций"; @@ -1183,7 +1183,7 @@ "gov.remove.votes.ask.details" = "Вы ранее голосовали в референдумах в %@. Для того, чтобы сделать эти треки доступными для делегирования, вам необходимо удалить голоса."; "delegations.list.empty" = "Список делегаций появится здесь"; "gov.add.delegate.voting.error.title" = "Вы уже голосовали"; -"gov.add.delegate.voting.error.message" = "Пожалуйста, удалите голоса в выбра��ных треках для начала делегирования"; +"gov.add.delegate.voting.error.message" = "Пожалуйста, удалите голоса в выбранных треках для начала делегирования"; "gov.revoke.delegate.missing.error.title" = "Делегация уже отменена"; "gov.revoke.delegate.missing.error.message" = "Вы уже отменили делегацию в одном из треков"; "common.multi.tx.error.no.details.message" = "При отправке транзакций возникла ошибка (%@). Вы хотите повторить?"; From 5228445e9bfe1f5c7e133bab0ea118f90f73531a Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 24 Oct 2023 12:06:28 +0300 Subject: [PATCH 063/204] add rows --- .../Confirm/SwapConfirmViewController.swift | 16 ++++----- .../Confirm/SwapConfirmViewFactory.swift | 6 ++-- .../Swaps/Confirm/SwapConfirmViewLayout.swift | 34 +++++++++---------- .../Swaps/Setup/View/SwapRateView.swift | 19 +++++++++++ 4 files changed, 48 insertions(+), 27 deletions(-) diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift index 20a7708112..c0cb42ae8f 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift @@ -6,11 +6,13 @@ final class SwapConfirmViewController: UIViewController, ViewHolder { let presenter: SwapConfirmPresenterProtocol - init(presenter: SwapConfirmPresenterProtocol, - localizationManager: LocalizationManagerProtocol) { + init( + presenter: SwapConfirmPresenterProtocol, + localizationManager: LocalizationManagerProtocol + ) { self.presenter = presenter super.init(nibName: nil, bundle: nil) - + self.localizationManager = localizationManager } @@ -30,14 +32,12 @@ final class SwapConfirmViewController: UIViewController, ViewHolder { setupHandlers() presenter.setup() } - + private func setupLocalization() { rootView.setup(locale: selectedLocale) } - - private func setupHandlers() { - - } + + private func setupHandlers() {} } extension SwapConfirmViewController: SwapConfirmViewProtocol {} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index c8e0cb3ac4..d68daff8dd 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -8,8 +8,10 @@ struct SwapConfirmViewFactory { let presenter = SwapConfirmPresenter(interactor: interactor, wireframe: wireframe) - let view = SwapConfirmViewController(presenter: presenter, - localizationManager: LocalizationManager.shared) + let view = SwapConfirmViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) presenter.view = view interactor.presenter = presenter diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift index 66ffdd35c5..f849b6b6e2 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift @@ -3,45 +3,45 @@ import SoraUI final class SwapConfirmViewLayout: ScrollableContainerLayoutView { let pairsView = SwapPairView() - + let detailsTableView: StackTableView = .create { $0.cellHeight = 44 $0.hasSeparators = true $0.contentInsets = UIEdgeInsets(top: 0, left: 16, bottom: 8, right: 16) } - - let rateCell: SwapRateView = .create { + + let rateCell: SwapRateViewCell = .create { $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote - $0.titleView.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() + $0.titleButton.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() } - - let priceDifferenceCell: SwapRateView = .create { + + let priceDifferenceCell: SwapRateViewCell = .create { $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote - $0.titleView.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() + $0.titleButton.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() } - - let slippageCell: SwapRateView = .create { + + let slippageCell: SwapRateViewCell = .create { $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote - $0.titleView.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() + $0.titleButton.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() } - let networkFeeCell = SwapNetworkFeeView(frame: .zero) + let networkFeeCell = SwapNetworkFeeViewCell() let walletTableView: StackTableView = .create { $0.cellHeight = 44 $0.hasSeparators = true $0.contentInsets = UIEdgeInsets(top: 0, left: 16, bottom: 8, right: 16) } - + let walletCell = StackTableCell() let accountCell: StackInfoTableCell = .create { $0.detailsLabel.lineBreakMode = .byTruncatingMiddle } - + let actionButton: TriangularedButton = .create { $0.applyDefaultStyle() } @@ -60,11 +60,11 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { detailsTableView.addArrangedSubview(priceDifferenceCell) detailsTableView.addArrangedSubview(slippageCell) detailsTableView.addArrangedSubview(networkFeeCell) - + addArrangedSubview(walletTableView) walletTableView.addArrangedSubview(walletCell) walletTableView.addArrangedSubview(accountCell) - + addSubview(actionButton) actionButton.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) @@ -83,12 +83,12 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { preferredLanguages: locale.rLanguages) rateCell.titleButton.invalidateLayout() networkFeeCell.titleButton.invalidateLayout() - + walletCell.titleLabel.text = R.string.localizable.commonWallet( preferredLanguages: locale.rLanguages) accountCell.titleLabel.text = R.string.localizable.commonAccount( preferredLanguages: locale.rLanguages) - + actionButton.imageWithTitleView?.title = R.string.localizable.commonConfirm( preferredLanguages: locale.rLanguages) } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapRateView.swift b/novawallet/Modules/Swaps/Setup/View/SwapRateView.swift index a3afdf0430..9a73b57a98 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapRateView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapRateView.swift @@ -95,3 +95,22 @@ extension SwapRateView { isLoading = false } } + +final class SwapRateViewCell: RowView, StackTableViewCellProtocol { + var titleButton: RoundedButton { rowContentView.titleView } + var valueLabel: UILabel { rowContentView.valueView } + + func bind(loadableViewModel: LoadableViewModelState) { + rowContentView.bind(loadableViewModel: loadableViewModel) + } +} + +final class SwapNetworkFeeViewCell: RowView, StackTableViewCellProtocol { + var titleButton: RoundedButton { rowContentView.titleView } + var valueTopButton: RoundedButton { rowContentView.valueView.fView } + var valueBottomLabel: UILabel { rowContentView.valueView.sView } + + func bind(loadableViewModel: LoadableViewModelState) { + rowContentView.bind(loadableViewModel: loadableViewModel) + } +} From 86146cbbc990c91f80e6baa6b040657621ebfdcf Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 24 Oct 2023 12:48:42 +0300 Subject: [PATCH 064/204] buildfix --- .../Swaps/Validation/SwapDataValidatorFactory.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift index e103257e10..5a752085fc 100644 --- a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift +++ b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift @@ -68,26 +68,26 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { guard let self = self, let view = self.view else { return } - let availableToPayString = balanceViewModelFactoryFacade.amountFromValue( + let availableToPayString = self.balanceViewModelFactoryFacade.amountFromValue( targetAssetInfo: params.feeChainAsset.assetDisplayInfo, value: preparedValues.availableToPay ).value(for: locale) - let feeString = balanceViewModelFactoryFacade.amountFromValue( + let feeString = self.balanceViewModelFactoryFacade.amountFromValue( targetAssetInfo: params.feeChainAsset.assetDisplayInfo, value: preparedValues.feeDecimal ).value(for: locale) let errorParams: SwapMaxErrorParams if preparedValues.toBuyED != 0 { - let diffString = balanceViewModelFactoryFacade.amountFromValue( + let diffString = self.balanceViewModelFactoryFacade.amountFromValue( targetAssetInfo: params.feeChainAsset.assetDisplayInfo, value: preparedValues.diff ).value(for: locale) - let edDepositInFeeTokenString = balanceViewModelFactoryFacade.amountFromValue( + let edDepositInFeeTokenString = self.balanceViewModelFactoryFacade.amountFromValue( targetAssetInfo: params.feeChainAsset.assetDisplayInfo, value: preparedValues.edDepositInFeeTokenDecimal ).value(for: locale) - let edString = balanceViewModelFactoryFacade.amountFromValue( + let edString = self.balanceViewModelFactoryFacade.amountFromValue( targetAssetInfo: params.edChainAsset.assetDisplayInfo, value: preparedValues.edDecimal ).value(for: locale) From fdfd2b28817889eb825c64f26cd35cc2a419b9b5 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 25 Oct 2023 10:52:05 +0300 Subject: [PATCH 065/204] base interactor --- novawallet.xcodeproj/project.pbxproj | 40 ++- .../Swaps/Base/SwapBaseInteractor.swift | 246 +++++++++++++++++ .../Swaps/Base/SwapBaseProtocols.swift | 16 ++ .../View/SwapNetworkFeeView.swift | 0 .../Base/View/SwapNetworkFeeViewCell.swift | 11 + .../{Setup => Base}/View/SwapRateView.swift | 19 -- .../Swaps/Base/View/SwapRateViewCell.swift | 10 + .../Swaps/Confirm/SwapConfirmInteractor.swift | 49 +++- .../Swaps/Confirm/SwapConfirmPresenter.swift | 17 +- .../Swaps/Confirm/SwapConfirmProtocols.swift | 6 +- .../Confirm/SwapConfirmViewFactory.swift | 68 ++++- .../Swaps/Confirm/SwapConfirmViewLayout.swift | 3 + .../Modules/Swaps/Confirm/SwapPairView.swift | 10 +- .../Swaps/Setup/SwapSetupInteractor.swift | 247 +----------------- .../Swaps/Setup/SwapSetupPresenter.swift | 17 +- .../Swaps/Setup/SwapSetupProtocols.swift | 19 +- .../Swaps/Setup/SwapSetupViewFactory.swift | 2 +- .../Swaps/Setup/SwapSetupWireframe.swift | 22 ++ 18 files changed, 519 insertions(+), 283 deletions(-) create mode 100644 novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift create mode 100644 novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift rename novawallet/Modules/Swaps/{Setup => Base}/View/SwapNetworkFeeView.swift (100%) create mode 100644 novawallet/Modules/Swaps/Base/View/SwapNetworkFeeViewCell.swift rename novawallet/Modules/Swaps/{Setup => Base}/View/SwapRateView.swift (76%) create mode 100644 novawallet/Modules/Swaps/Base/View/SwapRateViewCell.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index e6703466bf..8accc0bc2a 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -684,9 +684,13 @@ 770F578B2A8A48FF005FD7C1 /* ButtonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770F578A2A8A48FF005FD7C1 /* ButtonViewModel.swift */; }; 77171CA82A98BBA10032B387 /* NominationPoolErrorPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77171CA72A98BBA10032B387 /* NominationPoolErrorPresentable.swift */; }; 77171CAA2A98BC420032B387 /* NominationPoolDataValidatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77171CA92A98BC420032B387 /* NominationPoolDataValidatorFactory.swift */; }; + 7719019B2AE670AE00D9C918 /* ShortTextInfoPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719019A2AE670AE00D9C918 /* ShortTextInfoPresentable.swift */; }; 7719019D2AE6996600D9C918 /* SwapPairView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719019C2AE6996600D9C918 /* SwapPairView.swift */; }; 7719019F2AE6C9DC00D9C918 /* SwapElementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719019E2AE6C9DC00D9C918 /* SwapElementView.swift */; }; - 7719019B2AE670AE00D9C918 /* ShortTextInfoPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719019A2AE670AE00D9C918 /* ShortTextInfoPresentable.swift */; }; + 771901A22AE7E34D00D9C918 /* SwapBaseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901A12AE7E34D00D9C918 /* SwapBaseInteractor.swift */; }; + 771901A42AE7E48800D9C918 /* SwapBaseProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901A32AE7E48800D9C918 /* SwapBaseProtocols.swift */; }; + 771901A62AE8FF7E00D9C918 /* SwapRateViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901A52AE8FF7E00D9C918 /* SwapRateViewCell.swift */; }; + 771901A82AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901A72AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift */; }; 77204EA42A1CD59100BBDE4A /* GenericBorderedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */; }; 77204EA62A1E0EAA00BBDE4A /* WalletConnectionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA52A1E0EAA00BBDE4A /* WalletConnectionsView.swift */; }; 7725062C2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725062B2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift */; }; @@ -4711,9 +4715,13 @@ 770F578A2A8A48FF005FD7C1 /* ButtonViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonViewModel.swift; sourceTree = ""; }; 77171CA72A98BBA10032B387 /* NominationPoolErrorPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolErrorPresentable.swift; sourceTree = ""; }; 77171CA92A98BC420032B387 /* NominationPoolDataValidatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolDataValidatorFactory.swift; sourceTree = ""; }; + 7719019A2AE670AE00D9C918 /* ShortTextInfoPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortTextInfoPresentable.swift; sourceTree = ""; }; 7719019C2AE6996600D9C918 /* SwapPairView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPairView.swift; sourceTree = ""; }; 7719019E2AE6C9DC00D9C918 /* SwapElementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapElementView.swift; sourceTree = ""; }; - 7719019A2AE670AE00D9C918 /* ShortTextInfoPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortTextInfoPresentable.swift; sourceTree = ""; }; + 771901A12AE7E34D00D9C918 /* SwapBaseInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapBaseInteractor.swift; sourceTree = ""; }; + 771901A32AE7E48800D9C918 /* SwapBaseProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapBaseProtocols.swift; sourceTree = ""; }; + 771901A52AE8FF7E00D9C918 /* SwapRateViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRateViewCell.swift; sourceTree = ""; }; + 771901A72AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeViewCell.swift; sourceTree = ""; }; 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericBorderedView.swift; sourceTree = ""; }; 77204EA52A1E0EAA00BBDE4A /* WalletConnectionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectionsView.swift; sourceTree = ""; }; 7725062B2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferendumSearchViewLayout.swift; sourceTree = ""; }; @@ -9579,6 +9587,27 @@ path = Model; sourceTree = ""; }; + 771901A02AE7E33A00D9C918 /* Base */ = { + isa = PBXGroup; + children = ( + 771901A92AE8FFDC00D9C918 /* View */, + 771901A12AE7E34D00D9C918 /* SwapBaseInteractor.swift */, + 771901A32AE7E48800D9C918 /* SwapBaseProtocols.swift */, + ); + path = Base; + sourceTree = ""; + }; + 771901A92AE8FFDC00D9C918 /* View */ = { + isa = PBXGroup; + children = ( + 771901A72AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift */, + 771901A52AE8FF7E00D9C918 /* SwapRateViewCell.swift */, + 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */, + 77ECB46F2ACEEE2D0015CE9F /* SwapNetworkFeeView.swift */, + ); + path = View; + sourceTree = ""; + }; 7726E232D196BDD627329E24 /* ParaStkStakeSetup */ = { isa = PBXGroup; children = ( @@ -9624,8 +9653,6 @@ 774091FF2ACC1BE400172516 /* SwapAmountInput.swift */, CDD43373A9159E0A592077BB /* SwapSetupViewLayout.swift */, 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */, - 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */, - 77ECB46F2ACEEE2D0015CE9F /* SwapNetworkFeeView.swift */, 77740BC12AD69E3400E8C06F /* SwapMaxButtonView.swift */, ); path = View; @@ -9805,6 +9832,7 @@ isa = PBXGroup; children = ( 288677D19FEB54E369E6B619 /* Slippage */, + 771901A02AE7E33A00D9C918 /* Base */, 29BD7DA0076BA8BC3411221A /* Setup */, 7E5E800395DC908962C169CF /* Confirm */, ); @@ -19620,6 +19648,7 @@ 849976D027B3AC0100B14A6C /* MetamaskEvent.swift in Sources */, 0CE629DD2AA9B6BF00E250BD /* RewardDestinationViewModel.swift in Sources */, 849976BC27B25A6600B14A6C /* DAppTransportModel.swift in Sources */, + 771901A42AE7E48800D9C918 /* SwapBaseProtocols.swift in Sources */, 84D2F1B32775A3220040C680 /* ExtrinsicJSONProcessor.swift in Sources */, 8428765624ADDE0200D91AD8 /* SettingsCellViewModel.swift in Sources */, 8428229C289BC9D300163031 /* AddAccount+ParitySignerAddressesWireframe.swift in Sources */, @@ -20041,6 +20070,7 @@ 841221A028F051EE00715C82 /* GovMetadataLocalStorageSubscriber.swift in Sources */, 84ABB3322A16150400B5E95A /* AssetReceiveInfo.swift in Sources */, 84100F3626A6069200A5054E /* IconTitleValueView.swift in Sources */, + 771901A82AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift in Sources */, 88B560BC28F80DCB00A5EB59 /* VoteRowView.swift in Sources */, 84FCCD98292E3610002D2D3D /* JSONRPCError+Evm.swift in Sources */, 842876AB24AE049B00D91AD8 /* SelectionItemViewProtocol.swift in Sources */, @@ -20049,6 +20079,7 @@ 84FD3DBB254104B600A234E3 /* WalletTransactionListUpdated.swift in Sources */, 842EBB2928908ADB00B952D8 /* WalletsListTableViewCell.swift in Sources */, 84C1706629961FAD00CBE531 /* GovernanceYourDelegationsInteractorError.swift in Sources */, + 771901A62AE8FF7E00D9C918 /* SwapRateViewCell.swift in Sources */, 8428765C24ADDE0200D91AD8 /* SettingsViewController.swift in Sources */, 84746B3028153E4C002642F4 /* GradientBannerModel.swift in Sources */, 8433B34A29B63661005E5D0F /* ExtrinsicSplitter.swift in Sources */, @@ -22443,6 +22474,7 @@ 54D334605E9A7C71A4873CFC /* ParaStkRedeemWireframe.swift in Sources */, 413CCB7C7B22831147B8E815 /* ParaStkRedeemPresenter.swift in Sources */, 35F9157CAA182493B2F0E1D3 /* ParaStkRedeemInteractor.swift in Sources */, + 771901A22AE7E34D00D9C918 /* SwapBaseInteractor.swift in Sources */, C729BF3E60E6825AEED11383 /* ParaStkRedeemViewController.swift in Sources */, 844C3E752A091B9800C4305F /* WalletsChooseViewModelFactory.swift in Sources */, E477B09B47A3021EF1CE66F0 /* ParaStkRedeemViewLayout.swift in Sources */, diff --git a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift new file mode 100644 index 0000000000..c1dde85d90 --- /dev/null +++ b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift @@ -0,0 +1,246 @@ +import UIKit +import RobinHood +import BigInt + +class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapBaseInteractorInputProtocol { + weak var basePresenter: SwapSetupInteractorOutputProtocol? + let assetConversionOperationFactory: AssetConversionOperationFactoryProtocol + let assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol + let runtimeService: RuntimeProviderProtocol + let feeProxy: ExtrinsicFeeProxyProtocol + let extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol + let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol + let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol + let currencyManager: CurrencyManagerProtocol + let selectedAccount: MetaAccountModel + + private let operationQueue: OperationQueue + private var quoteCall: CancellableCall? + private var runtimeOperationCall: CancellableCall? + private var extrinsicService: ExtrinsicServiceProtocol? + + private var priceProviders: [ChainAssetId: StreamableProvider] = [:] + private var assetBalanceProviders: [ChainAssetId: StreamableProvider] = [:] + + init( + assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, + assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol, + runtimeService: RuntimeProviderProtocol, + feeProxy: ExtrinsicFeeProxyProtocol, + extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + currencyManager: CurrencyManagerProtocol, + selectedAccount: MetaAccountModel, + operationQueue: OperationQueue + ) { + self.assetConversionOperationFactory = assetConversionOperationFactory + self.assetConversionExtrinsicService = assetConversionExtrinsicService + self.runtimeService = runtimeService + self.feeProxy = feeProxy + self.extrinsicServiceFactory = extrinsicServiceFactory + self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory + self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory + self.currencyManager = currencyManager + self.selectedAccount = selectedAccount + self.operationQueue = operationQueue + } + + func updateSubscriptions(activeChainAssets: Set) { + priceProviders = clear(providers: priceProviders, activeChainAssets: activeChainAssets) + assetBalanceProviders = clear(providers: assetBalanceProviders, activeChainAssets: activeChainAssets) + } + + func clear( + providers: [ChainAssetId: StreamableProvider], + activeChainAssets: Set + ) -> [ChainAssetId: StreamableProvider] { + providers.reduce(into: [ChainAssetId: StreamableProvider]()) { + if !activeChainAssets.contains($1.key) { + $1.value.removeObserver(self) + } else { + $0[$1.key] = $1.value + } + } + } + + func priceSubscription(chainAsset: ChainAsset) -> StreamableProvider? { + guard let priceId = chainAsset.asset.priceId else { + return nil + } + + return priceProviders[chainAsset.chainAssetId] ?? subscribeToPrice( + for: priceId, + currency: currencyManager.selectedCurrency + ) + } + + func assetBalanceSubscription(chainAsset: ChainAsset) -> StreamableProvider? { + guard let accountId = chainAccountResponse(for: chainAsset)?.accountId else { + return nil + } + let chainAssetId = chainAsset.chainAssetId + return assetBalanceProviders[chainAssetId] ?? subscribeToAssetBalanceProvider( + for: accountId, + chainId: chainAssetId.chainId, + assetId: chainAssetId.assetId + ) + } + + func quote(args: AssetConversion.QuoteArgs) { + clear(cancellable: "eCall) + + let wrapper = assetConversionOperationFactory.quote(for: args) + wrapper.targetOperation.completionBlock = { [weak self, args] in + DispatchQueue.main.async { + guard self?.quoteCall === wrapper else { + return + } + do { + let result = try wrapper.targetOperation.extractNoCancellableResultData() + + self?.basePresenter?.didReceive(quote: result, for: args) + } catch { + self?.basePresenter?.didReceive(error: .quote(error, args)) + } + } + } + + quoteCall = wrapper + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } + + func fee(args: AssetConversion.CallArgs) { + clear(cancellable: &runtimeOperationCall) + guard let extrinsicService = extrinsicService else { + return + } + + let runtimeCoderFactoryOperation = runtimeService.fetchCoderFactoryOperation() + + runtimeCoderFactoryOperation.completionBlock = { [weak self] in + guard let self = self else { + return + } + do { + let runtimeCoderFactory = try runtimeCoderFactoryOperation.extractNoCancellableResultData() + let builder = self.assetConversionExtrinsicService.fetchExtrinsicBuilderClosure( + for: args, + codingFactory: runtimeCoderFactory + ) + self.feeProxy.estimateFee( + using: extrinsicService, + reuseIdentifier: args.identifier, + setupBy: builder + ) + } catch { + DispatchQueue.main.async { + self.basePresenter?.didReceive(error: .fetchFeeFailed(error, args.identifier)) + } + } + } + + runtimeOperationCall = runtimeCoderFactoryOperation + operationQueue.addOperation(runtimeCoderFactoryOperation) + } + + func chainAccountResponse(for chainAsset: ChainAsset) -> ChainAccountResponse? { + let metaChainAccountResponse = selectedAccount.fetchMetaChainAccount(for: chainAsset.chain.accountRequest()) + return metaChainAccountResponse?.chainAccount + } + + func set(receiveChainAsset chainAsset: ChainAsset) { + priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) + } + + func set(payChainAsset chainAsset: ChainAsset) { + guard let chainAccount = chainAccountResponse(for: chainAsset) else { + extrinsicService = nil + basePresenter?.didReceive(payAccountId: nil) + return + } + priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) + assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) + + extrinsicService = extrinsicServiceFactory.createService( + account: chainAccount, + chain: chainAsset.chain + ) + basePresenter?.didReceive(payAccountId: chainAccount.accountId) + } + + func set(feeChainAsset chainAsset: ChainAsset) { + priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) + assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) + } + + // MARK: - SwapBaseInteractorInputProtocol + + func setup() { + feeProxy.delegate = self + } + + func calculateQuote(for args: AssetConversion.QuoteArgs) { + quote(args: args) + } + + func calculateFee( + args: AssetConversion.CallArgs + ) { + fee(args: args) + } + + func remakePriceSubscription(for chainAsset: ChainAsset) { + priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) + } +} + +extension SwapBaseInteractor: ExtrinsicFeeProxyDelegate { + func didReceiveFee(result: Result, for transactionId: TransactionFeeId) { + DispatchQueue.main.async { + switch result { + case let .success(dispatchInfo): + let fee = BigUInt(dispatchInfo.fee) + self.basePresenter?.didReceive(fee: fee, transactionId: transactionId) + case let .failure(error): + self.basePresenter?.didReceive(error: .fetchFeeFailed(error, transactionId)) + } + } + } +} + +extension SwapBaseInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { + func handlePrice(result: Result, priceId: AssetModel.PriceId) { + switch result { + case let .success(priceData): + basePresenter?.didReceive(price: priceData, priceId: priceId) + case let .failure(error): + basePresenter?.didReceive(error: .price(error, priceId)) + } + } +} + +extension SwapBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { + func handleAssetBalance( + result: Result, + accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) { + let chainAssetId = ChainAssetId(chainId: chainId, assetId: assetId) + switch result { + case let .success(balance): + let balance = balance ?? .createZero( + for: .init(chainId: chainId, assetId: assetId), + accountId: accountId + ) + basePresenter?.didReceive( + balance: balance, + for: chainAssetId, + accountId: accountId + ) + case let .failure(error): + basePresenter?.didReceive(error: .assetBalance(error, chainAssetId, accountId)) + } + } +} diff --git a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift new file mode 100644 index 0000000000..c392509ef0 --- /dev/null +++ b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift @@ -0,0 +1,16 @@ +import BigInt + +protocol SwapBaseInteractorInputProtocol: AnyObject { + func setup() + func calculateQuote(for args: AssetConversion.QuoteArgs) + func calculateFee(args: AssetConversion.CallArgs) + func remakePriceSubscription(for chainAsset: ChainAsset) +} + +protocol SwapBaseInteractorOutputProtocol: AnyObject { + func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) + func didReceive(fee: BigUInt?, transactionId: TransactionFeeId) + func didReceive(error: SwapSetupError) + func didReceive(price: PriceData?, priceId: AssetModel.PriceId) + func didReceive(payAccountId: AccountId?) +} diff --git a/novawallet/Modules/Swaps/Setup/View/SwapNetworkFeeView.swift b/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeView.swift similarity index 100% rename from novawallet/Modules/Swaps/Setup/View/SwapNetworkFeeView.swift rename to novawallet/Modules/Swaps/Base/View/SwapNetworkFeeView.swift diff --git a/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeViewCell.swift b/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeViewCell.swift new file mode 100644 index 0000000000..d41031812b --- /dev/null +++ b/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeViewCell.swift @@ -0,0 +1,11 @@ +import SoraUI + +final class SwapNetworkFeeViewCell: RowView, StackTableViewCellProtocol { + var titleButton: RoundedButton { rowContentView.titleView } + var valueTopButton: RoundedButton { rowContentView.valueView.fView } + var valueBottomLabel: UILabel { rowContentView.valueView.sView } + + func bind(loadableViewModel: LoadableViewModelState) { + rowContentView.bind(loadableViewModel: loadableViewModel) + } +} diff --git a/novawallet/Modules/Swaps/Setup/View/SwapRateView.swift b/novawallet/Modules/Swaps/Base/View/SwapRateView.swift similarity index 76% rename from novawallet/Modules/Swaps/Setup/View/SwapRateView.swift rename to novawallet/Modules/Swaps/Base/View/SwapRateView.swift index 9a73b57a98..a3afdf0430 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapRateView.swift +++ b/novawallet/Modules/Swaps/Base/View/SwapRateView.swift @@ -95,22 +95,3 @@ extension SwapRateView { isLoading = false } } - -final class SwapRateViewCell: RowView, StackTableViewCellProtocol { - var titleButton: RoundedButton { rowContentView.titleView } - var valueLabel: UILabel { rowContentView.valueView } - - func bind(loadableViewModel: LoadableViewModelState) { - rowContentView.bind(loadableViewModel: loadableViewModel) - } -} - -final class SwapNetworkFeeViewCell: RowView, StackTableViewCellProtocol { - var titleButton: RoundedButton { rowContentView.titleView } - var valueTopButton: RoundedButton { rowContentView.valueView.fView } - var valueBottomLabel: UILabel { rowContentView.valueView.sView } - - func bind(loadableViewModel: LoadableViewModelState) { - rowContentView.bind(loadableViewModel: loadableViewModel) - } -} diff --git a/novawallet/Modules/Swaps/Base/View/SwapRateViewCell.swift b/novawallet/Modules/Swaps/Base/View/SwapRateViewCell.swift new file mode 100644 index 0000000000..7cef873251 --- /dev/null +++ b/novawallet/Modules/Swaps/Base/View/SwapRateViewCell.swift @@ -0,0 +1,10 @@ +import SoraUI + +final class SwapRateViewCell: RowView, StackTableViewCellProtocol { + var titleButton: RoundedButton { rowContentView.titleView } + var valueLabel: UILabel { rowContentView.valueView } + + func bind(loadableViewModel: LoadableViewModelState) { + rowContentView.bind(loadableViewModel: loadableViewModel) + } +} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift index 498b4fd8a3..734b2187b0 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift @@ -1,7 +1,54 @@ import UIKit -final class SwapConfirmInteractor { +final class SwapConfirmInteractor: SwapBaseInteractor { weak var presenter: SwapConfirmInteractorOutputProtocol? + let payChainAsset: ChainAsset + let receiveChainAsset: ChainAsset + let feeChainAsset: ChainAsset + let slippage: BigRational + + init( + payChainAsset: ChainAsset, + receiveChainAsset: ChainAsset, + feeChainAsset: ChainAsset, + slippage: BigRational, + assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, + assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol, + runtimeService: RuntimeProviderProtocol, + feeProxy: ExtrinsicFeeProxyProtocol, + extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + currencyManager: CurrencyManagerProtocol, + selectedAccount: MetaAccountModel, + operationQueue: OperationQueue + ) { + self.payChainAsset = payChainAsset + self.receiveChainAsset = receiveChainAsset + self.feeChainAsset = feeChainAsset + self.slippage = slippage + + super.init( + assetConversionOperationFactory: assetConversionOperationFactory, + assetConversionExtrinsicService: assetConversionExtrinsicService, + runtimeService: runtimeService, + feeProxy: feeProxy, + extrinsicServiceFactory: extrinsicServiceFactory, + priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, + walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, + currencyManager: currencyManager, + selectedAccount: selectedAccount, + operationQueue: operationQueue + ) + } + + override func setup() { + super.setup() + + set(payChainAsset: payChainAsset) + set(receiveChainAsset: receiveChainAsset) + set(feeChainAsset: feeChainAsset) + } } extension SwapConfirmInteractor: SwapConfirmInteractorInputProtocol {} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 706b29a85f..971fd3cfd4 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -1,4 +1,5 @@ import Foundation +import BigInt final class SwapConfirmPresenter { weak var view: SwapConfirmViewProtocol? @@ -15,7 +16,19 @@ final class SwapConfirmPresenter { } extension SwapConfirmPresenter: SwapConfirmPresenterProtocol { - func setup() {} + func setup() { + interactor.setup() + } } -extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol {} +extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { + func didReceive(quote _: AssetConversion.Quote, for _: AssetConversion.QuoteArgs) {} + + func didReceive(fee _: BigUInt?, transactionId _: TransactionFeeId) {} + + func didReceive(error _: SwapSetupError) {} + + func didReceive(price _: PriceData?, priceId _: AssetModel.PriceId) {} + + func didReceive(payAccountId _: AccountId?) {} +} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift index b6c91994f1..22fabf993c 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift @@ -1,11 +1,11 @@ -protocol SwapConfirmViewProtocol: AnyObject {} +protocol SwapConfirmViewProtocol: ControllerBackedProtocol {} protocol SwapConfirmPresenterProtocol: AnyObject { func setup() } -protocol SwapConfirmInteractorInputProtocol: AnyObject {} +protocol SwapConfirmInteractorInputProtocol: SwapBaseInteractorInputProtocol {} -protocol SwapConfirmInteractorOutputProtocol: AnyObject {} +protocol SwapConfirmInteractorOutputProtocol: SwapBaseInteractorOutputProtocol {} protocol SwapConfirmWireframeProtocol: AnyObject {} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index d68daff8dd..33bd346ede 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -1,9 +1,22 @@ import Foundation import SoraFoundation +import RobinHood struct SwapConfirmViewFactory { - static func createView() -> SwapConfirmViewProtocol? { - let interactor = SwapConfirmInteractor() + static func createView( + payChainAsset: ChainAsset, + receiveChainAsset: ChainAsset, + feeChainAsset: ChainAsset, + slippage: BigRational + ) -> SwapConfirmViewProtocol? { + guard let interactor = createInteractor( + payChainAsset: payChainAsset, + receiveChainAsset: receiveChainAsset, + feeChainAsset: feeChainAsset, + slippage: slippage + ) else { + return nil + } let wireframe = SwapConfirmWireframe() let presenter = SwapConfirmPresenter(interactor: interactor, wireframe: wireframe) @@ -18,4 +31,55 @@ struct SwapConfirmViewFactory { return view } + + private static func createInteractor( + payChainAsset: ChainAsset, + receiveChainAsset: ChainAsset, + feeChainAsset: ChainAsset, + slippage: BigRational + ) -> SwapConfirmInteractor? { + let westmintChainId = KnowChainId.westmint + let chainRegistry = ChainRegistryFacade.sharedRegistry + + guard let connection = chainRegistry.getConnection(for: westmintChainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: westmintChainId), + let chainModel = chainRegistry.getChain(for: westmintChainId), + let currencyManager = CurrencyManager.shared, + let selectedAccount = SelectedWalletSettings.shared.value else { + return nil + } + + let operationQueue = OperationManagerFacade.sharedDefaultQueue + + let assetConversionOperationFactory = AssetHubSwapOperationFactory( + chain: chainModel, + runtimeService: runtimeService, + connection: connection, + operationQueue: operationQueue + ) + let extrinsicServiceFactory = ExtrinsicServiceFactory( + runtimeRegistry: runtimeService, + engine: connection, + operationManager: OperationManager(operationQueue: operationQueue) + ) + + let interactor = SwapConfirmInteractor( + payChainAsset: payChainAsset, + receiveChainAsset: receiveChainAsset, + feeChainAsset: feeChainAsset, + slippage: slippage, + assetConversionOperationFactory: assetConversionOperationFactory, + assetConversionExtrinsicService: AssetHubExtrinsicService(chain: chainModel), + runtimeService: runtimeService, + feeProxy: ExtrinsicFeeProxy(), + extrinsicServiceFactory: extrinsicServiceFactory, + priceLocalSubscriptionFactory: PriceProviderFactory.shared, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + currencyManager: currencyManager, + selectedAccount: selectedAccount, + operationQueue: operationQueue + ) + + return interactor + } } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift index f849b6b6e2..7e5624bf70 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift @@ -77,6 +77,9 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { slippageCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupSlippage( preferredLanguages: locale.rLanguages ) + priceDifferenceCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupPriceDifference( + preferredLanguages: locale.rLanguages + ) rateCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupDetailsRate( preferredLanguages: locale.rLanguages) networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetwork( diff --git a/novawallet/Modules/Swaps/Confirm/SwapPairView.swift b/novawallet/Modules/Swaps/Confirm/SwapPairView.swift index 861a82777b..0543df5639 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapPairView.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapPairView.swift @@ -5,10 +5,10 @@ final class SwapPairView: UIView { let leftAssetView = SwapElementView() let rigthAssetView = SwapElementView() - let arrowView: UIImageView = .create { - $0.backgroundColor = R.color.colorSecondaryScreenBackground() - $0.layer.cornerRadius = 24 - $0.image = R.image.iconForward() + let arrowView: RoundedButton = .create { + $0.imageWithTitleView?.iconImage = R.image.iconForward() + $0.backgroundView?.backgroundColor = R.color.colorSecondaryScreenBackground() + $0.roundedBackgroundView?.cornerRadius = 24 } override init(frame: CGRect) { @@ -25,7 +25,7 @@ final class SwapPairView: UIView { } private func setupLayout() { - let stackView = UIView.hStack(distribution: .fillEqually, [ + let stackView = UIView.hStack(distribution: .fillEqually, spacing: 8, [ leftAssetView, rigthAssetView ]) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 4de4b05a19..84c8ea9506 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -2,41 +2,28 @@ import UIKit import RobinHood import BigInt -final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning { - weak var presenter: SwapSetupInteractorOutputProtocol? - let assetConversionOperationFactory: AssetConversionOperationFactoryProtocol - let assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol - let runtimeService: RuntimeProviderProtocol - let feeProxy: ExtrinsicFeeProxyProtocol - let extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol - let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol - let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol - let currencyManager: CurrencyManagerProtocol - let selectedAccount: MetaAccountModel - - private let operationQueue: OperationQueue - private var quoteCall: CancellableCall? - private var runtimeOperationCall: CancellableCall? - private var extrinsicService: ExtrinsicServiceProtocol? +final class SwapSetupInteractor: SwapBaseInteractor { + weak var presenter: SwapSetupInteractorOutputProtocol? { + basePresenter as? SwapSetupInteractorOutputProtocol + } - private var priceProviders: [ChainAssetId: StreamableProvider] = [:] private var assetBalanceProviders: [ChainAssetId: StreamableProvider] = [:] private var receiveChainAsset: ChainAsset? { didSet { - updateSubscriptions() + updateSubscriptions(activeChainAssets: activeChainAssets) } } private var payChainAsset: ChainAsset? { didSet { - updateSubscriptions() + updateSubscriptions(activeChainAssets: activeChainAssets) } } private var feeChainAsset: ChainAsset? { didSet { - updateSubscriptions() + updateSubscriptions(activeChainAssets: activeChainAssets) } } @@ -49,235 +36,27 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning ].compactMap { $0 } ) } - - init( - assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, - assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol, - runtimeService: RuntimeProviderProtocol, - feeProxy: ExtrinsicFeeProxyProtocol, - extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, - priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, - walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, - currencyManager: CurrencyManagerProtocol, - selectedAccount: MetaAccountModel, - operationQueue: OperationQueue - ) { - self.assetConversionOperationFactory = assetConversionOperationFactory - self.assetConversionExtrinsicService = assetConversionExtrinsicService - self.runtimeService = runtimeService - self.feeProxy = feeProxy - self.extrinsicServiceFactory = extrinsicServiceFactory - self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory - self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory - self.currencyManager = currencyManager - self.selectedAccount = selectedAccount - self.operationQueue = operationQueue - } - - private func updateSubscriptions() { - priceProviders = clear(providers: priceProviders) - assetBalanceProviders = clear(providers: assetBalanceProviders) - } - - private func clear(providers: [ChainAssetId: StreamableProvider]) -> [ChainAssetId: StreamableProvider] { - providers.reduce(into: [ChainAssetId: StreamableProvider]()) { - if !activeChainAssets.contains($1.key) { - $1.value.removeObserver(self) - } else { - $0[$1.key] = $1.value - } - } - } - - private func priceSubscription(chainAsset: ChainAsset) -> StreamableProvider? { - guard let priceId = chainAsset.asset.priceId else { - return nil - } - - return priceProviders[chainAsset.chainAssetId] ?? subscribeToPrice( - for: priceId, - currency: currencyManager.selectedCurrency - ) - } - - private func assetBalanceSubscription(chainAsset: ChainAsset) -> StreamableProvider? { - guard let accountId = chainAccountResponse(for: chainAsset)?.accountId else { - return nil - } - let chainAssetId = chainAsset.chainAssetId - return assetBalanceProviders[chainAssetId] ?? subscribeToAssetBalanceProvider( - for: accountId, - chainId: chainAssetId.chainId, - assetId: chainAssetId.assetId - ) - } - - private func quote(args: AssetConversion.QuoteArgs) { - clear(cancellable: "eCall) - - let wrapper = assetConversionOperationFactory.quote(for: args) - wrapper.targetOperation.completionBlock = { [weak self, args] in - DispatchQueue.main.async { - guard self?.quoteCall === wrapper else { - return - } - do { - let result = try wrapper.targetOperation.extractNoCancellableResultData() - - self?.presenter?.didReceive(quote: result, for: args) - } catch { - self?.presenter?.didReceive(error: .quote(error, args)) - } - } - } - - quoteCall = wrapper - operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) - } - - private func fee(args: AssetConversion.CallArgs) { - clear(cancellable: &runtimeOperationCall) - guard let extrinsicService = extrinsicService else { - return - } - - let runtimeCoderFactoryOperation = runtimeService.fetchCoderFactoryOperation() - - runtimeCoderFactoryOperation.completionBlock = { [weak self] in - guard let self = self else { - return - } - do { - let runtimeCoderFactory = try runtimeCoderFactoryOperation.extractNoCancellableResultData() - let builder = self.assetConversionExtrinsicService.fetchExtrinsicBuilderClosure( - for: args, - codingFactory: runtimeCoderFactory - ) - self.feeProxy.estimateFee( - using: extrinsicService, - reuseIdentifier: args.identifier, - setupBy: builder - ) - } catch { - DispatchQueue.main.async { - self.presenter?.didReceive(error: .fetchFeeFailed(error, args.identifier)) - } - } - } - - runtimeOperationCall = runtimeCoderFactoryOperation - operationQueue.addOperation(runtimeCoderFactoryOperation) - } - - private func chainAccountResponse(for chainAsset: ChainAsset) -> ChainAccountResponse? { - let metaChainAccountResponse = selectedAccount.fetchMetaChainAccount(for: chainAsset.chain.accountRequest()) - return metaChainAccountResponse?.chainAccount - } } extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { - func setup() { - feeProxy.delegate = self - } - - func calculateQuote(for args: AssetConversion.QuoteArgs) { - quote(args: args) - } - func update(receiveChainAsset: ChainAsset?) { self.receiveChainAsset = receiveChainAsset - - if let chainAsset = receiveChainAsset { - priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) + receiveChainAsset.map { + set(receiveChainAsset: $0) } } func update(payChainAsset: ChainAsset?) { self.payChainAsset = payChainAsset - - guard let chainAsset = payChainAsset, - let chainAccount = chainAccountResponse(for: chainAsset) else { - extrinsicService = nil - presenter?.didReceive(payAccountId: nil) - return + payChainAsset.map { + set(payChainAsset: $0) } - priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) - assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) - - extrinsicService = extrinsicServiceFactory.createService( - account: chainAccount, - chain: chainAsset.chain - ) - presenter?.didReceive(payAccountId: chainAccount.accountId) } func update(feeChainAsset: ChainAsset?) { self.feeChainAsset = feeChainAsset - - guard let chainAsset = feeChainAsset else { - return - } - priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) - assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) - } - - func calculateFee( - args: AssetConversion.CallArgs - ) { - fee(args: args) - } - - func remakePriceSubscription(for chainAsset: ChainAsset) { - priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) - } -} - -extension SwapSetupInteractor: ExtrinsicFeeProxyDelegate { - func didReceiveFee(result: Result, for transactionId: TransactionFeeId) { - DispatchQueue.main.async { - switch result { - case let .success(dispatchInfo): - let fee = BigUInt(dispatchInfo.fee) - self.presenter?.didReceive(fee: fee, transactionId: transactionId) - case let .failure(error): - self.presenter?.didReceive(error: .fetchFeeFailed(error, transactionId)) - } - } - } -} - -extension SwapSetupInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { - func handlePrice(result: Result, priceId: AssetModel.PriceId) { - switch result { - case let .success(priceData): - presenter?.didReceive(price: priceData, priceId: priceId) - case let .failure(error): - presenter?.didReceive(error: .price(error, priceId)) - } - } -} - -extension SwapSetupInteractor: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { - func handleAssetBalance( - result: Result, - accountId: AccountId, - chainId: ChainModel.Id, - assetId: AssetModel.Id - ) { - let chainAssetId = ChainAssetId(chainId: chainId, assetId: assetId) - switch result { - case let .success(balance): - let balance = balance ?? .createZero( - for: .init(chainId: chainId, assetId: assetId), - accountId: accountId - ) - presenter?.didReceive( - balance: balance, - for: chainAssetId, - accountId: accountId - ) - case let .failure(error): - presenter?.didReceive(error: .assetBalance(error, chainAssetId, accountId)) + feeChainAsset.map { + set(feeChainAsset: $0) } } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 4d3f484ea5..bf5d2a2739 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -443,8 +443,21 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { ) } - // TODO: navigate to confirm screen - func proceed() {} + func proceed() { + guard let payChainAsset = payChainAsset, + let receiveChainAsset = receiveChainAsset, + let feeChainAsset = feeChainAsset, + let slippage = slippage else { + return + } + wireframe.showConfirmation( + from: view, + payChainAsset: payChainAsset, + receiveChainAsset: receiveChainAsset, + feeChainAsset: feeChainAsset, + slippage: slippage + ) + } func showSettings() { guard let payChainAsset = payChainAsset else { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 4ac3a68446..ef82fab141 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -32,22 +32,14 @@ protocol SwapSetupPresenterProtocol: AnyObject { func selectMaxPayAmount() } -protocol SwapSetupInteractorInputProtocol: AnyObject { +protocol SwapSetupInteractorInputProtocol: SwapBaseInteractorInputProtocol { func setup() func update(receiveChainAsset: ChainAsset?) func update(payChainAsset: ChainAsset?) func update(feeChainAsset: ChainAsset?) - func calculateQuote(for args: AssetConversion.QuoteArgs) - func calculateFee(args: AssetConversion.CallArgs) - func remakePriceSubscription(for chainAsset: ChainAsset) } -protocol SwapSetupInteractorOutputProtocol: AnyObject { - func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) - func didReceive(fee: BigUInt?, transactionId: TransactionFeeId) - func didReceive(error: SwapSetupError) - func didReceive(price: PriceData?, priceId: AssetModel.PriceId) - func didReceive(payAccountId: AccountId?) +protocol SwapSetupInteractorOutputProtocol: SwapBaseInteractorOutputProtocol { func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId: AccountId) } @@ -73,6 +65,13 @@ protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryabl title: LocalizableResource, details: LocalizableResource ) + func showConfirmation( + from view: ControllerBackedProtocol?, + payChainAsset: ChainAsset, + receiveChainAsset: ChainAsset, + feeChainAsset: ChainAsset, + slippage: BigRational + ) } enum SwapSetupError: Error { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index e20538832a..e03e3024b3 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -35,7 +35,7 @@ struct SwapSetupViewFactory { ) presenter.view = view - interactor.presenter = presenter + interactor.basePresenter = presenter return view } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index e0aceeaee8..26504d0505 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -68,4 +68,26 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { animated: true ) } + + func showConfirmation( + from view: ControllerBackedProtocol?, + payChainAsset: ChainAsset, + receiveChainAsset: ChainAsset, + feeChainAsset: ChainAsset, + slippage: BigRational + ) { + guard let confimView = SwapConfirmViewFactory.createView( + payChainAsset: payChainAsset, + receiveChainAsset: receiveChainAsset, + feeChainAsset: feeChainAsset, + slippage: slippage + ) else { + return + } + + view?.controller.navigationController?.pushViewController( + confimView.controller, + animated: true + ) + } } From 7ef97ff78ad41e454261b47b4c41555b7b5b5435 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 25 Oct 2023 11:01:33 +0300 Subject: [PATCH 066/204] remove tests --- novawallet.xcodeproj/project.pbxproj | 48 +++++++------------ .../SwapConfirm/SwapConfirmTests.swift | 16 ------- 2 files changed, 18 insertions(+), 46 deletions(-) delete mode 100644 novawalletTests/Modules/SwapConfirm/SwapConfirmTests.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index d9ceb3232b..9ee1de76d8 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -362,7 +362,6 @@ 255D7AEBA45EFA5324D92371 /* DAppListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED66939D92756F608FA11520 /* DAppListPresenter.swift */; }; 25993E2E536DE682E1DFC9AD /* ParaStkCollatorsSearchInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841C1EE99F5EA1713BA3F313 /* ParaStkCollatorsSearchInteractor.swift */; }; 25E4B008933E2EF7F2FAAA46 /* StakingMoreOptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2FE7131747C08259EB98AC /* StakingMoreOptionsViewController.swift */; }; - 262583559B47705A3021EA67 /* SwapConfirmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7DDDF162206B4BFF6D5935A /* SwapConfirmTests.swift */; }; 26533668754DB6C1DF2425AB /* TokenManageSingleViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19485BAFCB2056BDC135441 /* TokenManageSingleViewFactory.swift */; }; 265C6E00915F2F186551A67B /* NPoolsUnstakeSetupViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3246115B53EFE9461CD2F68B /* NPoolsUnstakeSetupViewFactory.swift */; }; 270C21973CB61F0BF3D2D1E3 /* CrowdloanListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02ACCC85B2CCF3D9392CA9B4 /* CrowdloanListProtocols.swift */; }; @@ -7731,7 +7730,6 @@ B73F89021BEE1F4576128305 /* StakingSetupAmountProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSetupAmountProtocols.swift; sourceTree = ""; }; B765BDAA27726E2586953368 /* OnChainTransferSetupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OnChainTransferSetupInteractor.swift; sourceTree = ""; }; B7CB6BF970620958C9DDD037 /* ParaStkStakeConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkStakeConfirmWireframe.swift; sourceTree = ""; }; - B7DDDF162206B4BFF6D5935A /* SwapConfirmTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapConfirmTests.swift; sourceTree = ""; }; B8A6C6207095F63972E14618 /* DAppPhishingProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppPhishingProtocols.swift; sourceTree = ""; }; B8B0A8174A9FFB8422A70D83 /* ParitySignerWelcomeWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerWelcomeWireframe.swift; sourceTree = ""; }; B8B263D5668F1C91E2CF61D9 /* GovernanceRevokeDelegationConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceRevokeDelegationConfirmWireframe.swift; sourceTree = ""; }; @@ -9583,21 +9581,32 @@ path = DelegationReferendumVoters; sourceTree = ""; }; - 73DE684949441C60D62A687A /* SwapConfirm */ = { + 770F57892A8A48F7005FD7C1 /* Model */ = { isa = PBXGroup; children = ( - B7DDDF162206B4BFF6D5935A /* SwapConfirmTests.swift */, + 77F033A72A86136D006BC67E /* StakingSelectPoolViewModelFactory.swift */, + 770F578A2A8A48FF005FD7C1 /* ButtonViewModel.swift */, ); - path = SwapConfirm; + path = Model; sourceTree = ""; }; - 770F57892A8A48F7005FD7C1 /* Model */ = { + 7719018A2AE0E62500D9C918 /* Validation */ = { isa = PBXGroup; children = ( - 77F033A72A86136D006BC67E /* StakingSelectPoolViewModelFactory.swift */, - 770F578A2A8A48FF005FD7C1 /* ButtonViewModel.swift */, + 7719018B2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift */, + 7719018D2AE0E71F00D9C918 /* SwapErrorPresentable.swift */, + 771901922AE2736E00D9C918 /* SwapFeeParams.swift */, + 771901942AE2739800D9C918 /* SwapMaxErrorParams.swift */, ); - path = Model; + path = Validation; + sourceTree = ""; + }; + 771901912AE2425400D9C918 /* Swaps */ = { + isa = PBXGroup; + children = ( + 7719018F2AE2424B00D9C918 /* SwapsValidationTests.swift */, + ); + path = Swaps; sourceTree = ""; }; 771901A02AE7E33A00D9C918 /* Base */ = { @@ -9621,25 +9630,6 @@ path = View; sourceTree = ""; }; - 7719018A2AE0E62500D9C918 /* Validation */ = { - isa = PBXGroup; - children = ( - 7719018B2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift */, - 7719018D2AE0E71F00D9C918 /* SwapErrorPresentable.swift */, - 771901922AE2736E00D9C918 /* SwapFeeParams.swift */, - 771901942AE2739800D9C918 /* SwapMaxErrorParams.swift */, - ); - path = Validation; - sourceTree = ""; - }; - 771901912AE2425400D9C918 /* Swaps */ = { - isa = PBXGroup; - children = ( - 7719018F2AE2424B00D9C918 /* SwapsValidationTests.swift */, - ); - path = Swaps; - sourceTree = ""; - }; 7726E232D196BDD627329E24 /* ParaStkStakeSetup */ = { isa = PBXGroup; children = ( @@ -14645,7 +14635,6 @@ 84B7C705289BFA79001A3566 /* AccountManagement */, 84B7C708289BFA79001A3566 /* WalletList */, 84B7C70A289BFA79001A3566 /* ControllerAccount */, - 73DE684949441C60D62A687A /* SwapConfirm */, ); path = Modules; sourceTree = ""; @@ -23374,7 +23363,6 @@ 84B7C748289BFA79001A3566 /* WalletListTests.swift in Sources */, 84B7C720289BFA79001A3566 /* ReferralCrowdloanTests.swift in Sources */, F4897BB126AED13D0075F291 /* EraCountdownOperationFactoryStub.swift in Sources */, - 262583559B47705A3021EA67 /* SwapConfirmTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/novawalletTests/Modules/SwapConfirm/SwapConfirmTests.swift b/novawalletTests/Modules/SwapConfirm/SwapConfirmTests.swift deleted file mode 100644 index d6bd265b37..0000000000 --- a/novawalletTests/Modules/SwapConfirm/SwapConfirmTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -import XCTest - -class SwapConfirmTests: XCTestCase { - - override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() { - XCTFail("Did you forget to add tests?") - } -} From e1e5cdae285344006d6a4a9df1a46781494b1c3d Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 25 Oct 2023 12:25:37 +0300 Subject: [PATCH 067/204] fixes --- novawallet/Modules/Swaps/Confirm/SwapElementView.swift | 6 +++++- novawallet/Modules/Swaps/Confirm/SwapPairView.swift | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/novawallet/Modules/Swaps/Confirm/SwapElementView.swift b/novawallet/Modules/Swaps/Confirm/SwapElementView.swift index 36ac88a389..f73baf3ad7 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapElementView.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapElementView.swift @@ -50,7 +50,7 @@ final class SwapElementView: UIView { setupLayout() } - lazy var contentView = UIView.vStack([ + lazy var contentView = UIView.vStack(distribution: .equalCentering, [ imageView, valueLabel, priceLabel, @@ -73,5 +73,9 @@ final class SwapElementView: UIView { contentView.snp.makeConstraints { $0.edges.equalToSuperview().inset(contentInsets) } + + imageView.snp.makeConstraints { + $0.height.width.equalTo(48) + } } } diff --git a/novawallet/Modules/Swaps/Confirm/SwapPairView.swift b/novawallet/Modules/Swaps/Confirm/SwapPairView.swift index 0543df5639..43ca8e59c9 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapPairView.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapPairView.swift @@ -7,8 +7,10 @@ final class SwapPairView: UIView { let arrowView: RoundedButton = .create { $0.imageWithTitleView?.iconImage = R.image.iconForward() - $0.backgroundView?.backgroundColor = R.color.colorSecondaryScreenBackground() + $0.roundedBackgroundView?.apply(style: .icon) + $0.roundedBackgroundView?.fillColor = R.color.colorSecondaryScreenBackground()! $0.roundedBackgroundView?.cornerRadius = 24 + $0.isUserInteractionEnabled = false } override init(frame: CGRect) { From 7711f0a128890a39063d3718ccebca7008eac5b6 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 25 Oct 2023 13:18:09 +0300 Subject: [PATCH 068/204] bugfixes --- .../Swaps/Slippage/SwapSlippagePresenter.swift | 14 ++++++++------ .../Swaps/Slippage/SwapSlippageProtocols.swift | 1 + .../Slippage/SwapSlippageViewController.swift | 8 +++++++- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index 726a886c84..005e081517 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -56,16 +56,16 @@ final class SwapSlippagePresenter { amount: amountInput, limit: 100, formatter: numberFormatter, - inputLocale: selectedLocale, precision: 1 ) view?.didReceiveInput(viewModel: inputViewModel) } - private func provideResetButtonState() { + private func provideButtonsState() { let amountChanged = amountInput != initialPercent() view?.didReceiveResetState(available: amountChanged) + view?.didReceiveButtonState(available: amountChanged) } private func provideErrors() { @@ -79,8 +79,10 @@ final class SwapSlippagePresenter { preferredLanguages: selectedLocale.rLanguages ) view?.didReceiveInput(error: error) + view?.didReceiveButtonState(available: false) } else { view?.didReceiveInput(error: nil) + view?.didReceiveButtonState(available: amountInput != initialPercent()) } } @@ -115,7 +117,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { } amountInput = initialPercent() - provideResetButtonState() + provideButtonsState() provideAmountViewModel() provideWarnings() view?.didReceivePreFilledPercents(viewModel: viewModel) @@ -124,14 +126,14 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { func select(percent: SlippagePercentViewModel) { amountInput = percent.value provideAmountViewModel() - provideResetButtonState() + provideButtonsState() provideErrors() provideWarnings() } func updateAmount(_ amount: Decimal?) { amountInput = amount - provideResetButtonState() + provideButtonsState() provideErrors() provideWarnings() } @@ -153,7 +155,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { func reset() { amountInput = initialPercent() provideAmountViewModel() - provideResetButtonState() + provideButtonsState() provideErrors() provideWarnings() } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift index 07c73056c1..c84ec8b9ef 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageProtocols.swift @@ -6,6 +6,7 @@ protocol SwapSlippageViewProtocol: ControllerBackedProtocol { func didReceiveInput(error: String?) func didReceiveInput(warning: String?) func didReceiveResetState(available: Bool) + func didReceiveButtonState(available: Bool) } protocol SwapSlippagePresenterProtocol: AnyObject { diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift index c67aa32536..b9115c1176 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift @@ -5,6 +5,7 @@ final class SwapSlippageViewController: UIViewController, ViewHolder { typealias RootViewType = SwapSlippageViewLayout let presenter: SwapSlippagePresenterProtocol + private var isApplyAvailable: Bool = false init( presenter: SwapSlippagePresenterProtocol, @@ -74,7 +75,7 @@ final class SwapSlippageViewController: UIViewController, ViewHolder { private func updateActionButton() { let inputValid = rootView.amountInput.inputViewModel?.isValid == true - rootView.actionButton.isEnabled = inputValid && rootView.errorLabel.isHidden + rootView.actionButton.isEnabled = isApplyAvailable && inputValid } @objc private func applyButtonAction() { @@ -114,6 +115,11 @@ extension SwapSlippageViewController: SwapSlippageViewProtocol { navigationItem.rightBarButtonItem?.isEnabled = available } + func didReceiveButtonState(available: Bool) { + isApplyAvailable = available + updateActionButton() + } + func didReceiveInput(error: String?) { rootView.set(error: error) updateActionButton() From a0f98947be8888e50e6c5ae78d63e63a3834c540 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 25 Oct 2023 13:22:57 +0300 Subject: [PATCH 069/204] cleanup --- .../Modules/Swaps/Slippage/SwapSlippagePresenter.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index 005e081517..5d92692615 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -62,7 +62,7 @@ final class SwapSlippagePresenter { view?.didReceiveInput(viewModel: inputViewModel) } - private func provideButtonsState() { + private func provideButtonStates() { let amountChanged = amountInput != initialPercent() view?.didReceiveResetState(available: amountChanged) view?.didReceiveButtonState(available: amountChanged) @@ -117,7 +117,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { } amountInput = initialPercent() - provideButtonsState() + provideButtonStates() provideAmountViewModel() provideWarnings() view?.didReceivePreFilledPercents(viewModel: viewModel) @@ -126,14 +126,14 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { func select(percent: SlippagePercentViewModel) { amountInput = percent.value provideAmountViewModel() - provideButtonsState() + provideButtonStates() provideErrors() provideWarnings() } func updateAmount(_ amount: Decimal?) { amountInput = amount - provideButtonsState() + provideButtonStates() provideErrors() provideWarnings() } @@ -155,7 +155,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { func reset() { amountInput = initialPercent() provideAmountViewModel() - provideButtonsState() + provideButtonStates() provideErrors() provideWarnings() } From 4ab89083ad6ffdaee1026de7eaa85e128b792858 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 25 Oct 2023 12:39:20 +0200 Subject: [PATCH 070/204] support custom extensions in the extrinsic service --- .../Substrate/ExtrinsicService.swift | 3 +- .../Substrate/ExtrinsicServiceFactory.swift | 51 +++++++++++++++++-- .../Substrate/Xcm/XcmTransferService.swift | 2 +- .../Extension/ExtrinsicExtension.swift | 8 ++- .../Swaps/Setup/SwapSetupInteractor.swift | 39 ++++++++++++-- 5 files changed, 90 insertions(+), 13 deletions(-) diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicService.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicService.swift index 0f06c109db..a53a3f2ffe 100644 --- a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicService.swift +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicService.swift @@ -105,6 +105,7 @@ final class ExtrinsicService { cryptoType: MultiassetCryptoType, walletType: MetaAccountModelType, runtimeRegistry: RuntimeCodingServiceProtocol, + extensions: [ExtrinsicExtension], engine: JSONRPCEngine, operationManager: OperationManagerProtocol ) { @@ -114,7 +115,7 @@ final class ExtrinsicService { cryptoType: cryptoType, signaturePayloadFormat: walletType.signaturePayloadFormat, runtimeRegistry: runtimeRegistry, - customExtensions: DefaultExtrinsicExtension.extensions, + customExtensions: extensions, engine: engine, operationManager: operationManager ) diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceFactory.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceFactory.swift index d6a17fc9d8..b59b704f97 100644 --- a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceFactory.swift +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceFactory.swift @@ -5,15 +5,53 @@ import SubstrateSdk protocol ExtrinsicServiceFactoryProtocol { func createService( account: ChainAccountResponse, - chain: ChainModel + chain: ChainModel, + extensions: [ExtrinsicExtension] ) -> ExtrinsicServiceProtocol func createOperationFactory( account: ChainAccountResponse, - chain: ChainModel + chain: ChainModel, + extensions: [ExtrinsicExtension] ) -> ExtrinsicOperationFactoryProtocol } +extension ExtrinsicServiceFactoryProtocol { + func createService( + account: ChainAccountResponse, + chain: ChainModel + ) -> ExtrinsicServiceProtocol { + createService( + account: account, + chain: chain, + extensions: DefaultExtrinsicExtension.extensions() + ) + } + + func createService( + account: ChainAccountResponse, + chain: ChainModel, + feeAssetId: UInt32 + ) -> ExtrinsicServiceProtocol { + createService( + account: account, + chain: chain, + extensions: DefaultExtrinsicExtension.extensions(payingFeeIn: feeAssetId) + ) + } + + func createOperationFactory( + account: ChainAccountResponse, + chain: ChainModel + ) -> ExtrinsicOperationFactoryProtocol { + createOperationFactory( + account: account, + chain: chain, + extensions: DefaultExtrinsicExtension.extensions() + ) + } +} + final class ExtrinsicServiceFactory { private let runtimeRegistry: RuntimeCodingServiceProtocol private let engine: JSONRPCEngine @@ -33,7 +71,8 @@ final class ExtrinsicServiceFactory { extension ExtrinsicServiceFactory: ExtrinsicServiceFactoryProtocol { func createService( account: ChainAccountResponse, - chain: ChainModel + chain: ChainModel, + extensions: [ExtrinsicExtension] ) -> ExtrinsicServiceProtocol { ExtrinsicService( accountId: account.accountId, @@ -41,6 +80,7 @@ extension ExtrinsicServiceFactory: ExtrinsicServiceFactoryProtocol { cryptoType: account.cryptoType, walletType: account.type, runtimeRegistry: runtimeRegistry, + extensions: extensions, engine: engine, operationManager: operationManager ) @@ -48,7 +88,8 @@ extension ExtrinsicServiceFactory: ExtrinsicServiceFactoryProtocol { func createOperationFactory( account: ChainAccountResponse, - chain: ChainModel + chain: ChainModel, + extensions: [ExtrinsicExtension] ) -> ExtrinsicOperationFactoryProtocol { ExtrinsicOperationFactory( accountId: account.accountId, @@ -56,7 +97,7 @@ extension ExtrinsicServiceFactory: ExtrinsicServiceFactoryProtocol { cryptoType: account.cryptoType, signaturePayloadFormat: account.type.signaturePayloadFormat, runtimeRegistry: runtimeRegistry, - customExtensions: DefaultExtrinsicExtension.extensions, + customExtensions: extensions, engine: engine, operationManager: operationManager ) diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTransferService.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTransferService.swift index 6e6a55a228..aeba25c568 100644 --- a/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTransferService.swift +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/Xcm/XcmTransferService.swift @@ -77,7 +77,7 @@ final class XcmTransferService { cryptoType: cryptoType, signaturePayloadFormat: signaturePayloadFormat, runtimeRegistry: runtimeProvider, - customExtensions: DefaultExtrinsicExtension.extensions, + customExtensions: DefaultExtrinsicExtension.extensions(), engine: connection, operationManager: OperationManager(operationQueue: operationQueue) ) diff --git a/novawallet/Common/Substrate/Extension/ExtrinsicExtension.swift b/novawallet/Common/Substrate/Extension/ExtrinsicExtension.swift index eef7c70cdc..8fb97e4ff1 100644 --- a/novawallet/Common/Substrate/Extension/ExtrinsicExtension.swift +++ b/novawallet/Common/Substrate/Extension/ExtrinsicExtension.swift @@ -2,12 +2,18 @@ import Foundation import SubstrateSdk enum DefaultExtrinsicExtension { - static var extensions: [ExtrinsicExtension] { + static func extensions() -> [ExtrinsicExtension] { [ ChargeAssetTxPayment() ] } + static func extensions(payingFeeIn assetId: UInt32) -> [ExtrinsicExtension] { + [ + ChargeAssetTxPayment(assetId: assetId) + ] + } + static func getCoders(for metadata: RuntimeMetadataProtocol) -> [ExtrinsicExtensionCoder] { let extensionName = ChargeAssetTxPayment.name diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 4de4b05a19..7d8088c2e7 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -74,6 +74,36 @@ final class SwapSetupInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning self.operationQueue = operationQueue } + private func updateExtrinsicService() { + guard let chainAsset = feeChainAsset, let chainAccount = chainAccountResponse(for: chainAsset) else { + extrinsicService = nil + return + } + + guard !chainAsset.isUtilityAsset else { + extrinsicService = extrinsicServiceFactory.createService( + account: chainAccount, + chain: chainAsset.chain + ) + return + } + + if + let assetType = chainAsset.asset.type, + case .statemine = AssetType(rawValue: assetType), + let typeExtras = chainAsset.asset.typeExtras, + let extras = try? typeExtras.map(to: StatemineAssetExtras.self), + let assetId = UInt32(extras.assetId) { + extrinsicService = extrinsicServiceFactory.createService( + account: chainAccount, + chain: chainAsset.chain, + feeAssetId: assetId + ) + } else { + extrinsicService = nil + } + } + private func updateSubscriptions() { priceProviders = clear(providers: priceProviders) assetBalanceProviders = clear(providers: assetBalanceProviders) @@ -197,26 +227,25 @@ extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { guard let chainAsset = payChainAsset, let chainAccount = chainAccountResponse(for: chainAsset) else { - extrinsicService = nil presenter?.didReceive(payAccountId: nil) return } + priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) - extrinsicService = extrinsicServiceFactory.createService( - account: chainAccount, - chain: chainAsset.chain - ) presenter?.didReceive(payAccountId: chainAccount.accountId) } func update(feeChainAsset: ChainAsset?) { self.feeChainAsset = feeChainAsset + updateExtrinsicService() + guard let chainAsset = feeChainAsset else { return } + priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) } From 5dd75e38c599db356258cc35d56961e057085b5f Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 25 Oct 2023 20:01:07 +0300 Subject: [PATCH 071/204] connect logic to ui --- novawallet.xcodeproj/project.pbxproj | 30 ++- .../Model/SwapConfirmViewModelFactory.swift | 168 ++++++++++++++++ .../Confirm/Model/SwapConfirmViewModels.swift | 16 ++ .../Swaps/Confirm/SwapConfirmInteractor.swift | 3 + .../Swaps/Confirm/SwapConfirmPresenter.swift | 186 +++++++++++++++++- .../Swaps/Confirm/SwapConfirmProtocols.swift | 10 +- .../Confirm/SwapConfirmViewController.swift | 48 ++++- .../Confirm/SwapConfirmViewFactory.swift | 42 +++- .../Swaps/Confirm/SwapElementView.swift | 81 -------- .../{ => View}/SwapConfirmViewLayout.swift | 0 .../Swaps/Confirm/View/SwapElementView.swift | 134 +++++++++++++ .../Confirm/{ => View}/SwapPairView.swift | 10 + .../Swaps/Setup/SwapSetupPresenter.swift | 8 +- .../Swaps/Setup/SwapSetupProtocols.swift | 4 +- .../Swaps/Setup/SwapSetupWireframe.swift | 8 +- 15 files changed, 648 insertions(+), 100 deletions(-) create mode 100644 novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift create mode 100644 novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModels.swift delete mode 100644 novawallet/Modules/Swaps/Confirm/SwapElementView.swift rename novawallet/Modules/Swaps/Confirm/{ => View}/SwapConfirmViewLayout.swift (100%) create mode 100644 novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift rename novawallet/Modules/Swaps/Confirm/{ => View}/SwapPairView.swift (81%) diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 9ee1de76d8..5d277195c3 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -696,6 +696,8 @@ 771901A42AE7E48800D9C918 /* SwapBaseProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901A32AE7E48800D9C918 /* SwapBaseProtocols.swift */; }; 771901A62AE8FF7E00D9C918 /* SwapRateViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901A52AE8FF7E00D9C918 /* SwapRateViewCell.swift */; }; 771901A82AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901A72AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift */; }; + 771901AB2AE9581400D9C918 /* SwapConfirmViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901AA2AE9581400D9C918 /* SwapConfirmViewModelFactory.swift */; }; + 771901B02AE97DA500D9C918 /* SwapConfirmViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901AF2AE97DA500D9C918 /* SwapConfirmViewModels.swift */; }; 77204EA42A1CD59100BBDE4A /* GenericBorderedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */; }; 77204EA62A1E0EAA00BBDE4A /* WalletConnectionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA52A1E0EAA00BBDE4A /* WalletConnectionsView.swift */; }; 7725062C2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725062B2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift */; }; @@ -4733,6 +4735,8 @@ 771901A32AE7E48800D9C918 /* SwapBaseProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapBaseProtocols.swift; sourceTree = ""; }; 771901A52AE8FF7E00D9C918 /* SwapRateViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRateViewCell.swift; sourceTree = ""; }; 771901A72AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeViewCell.swift; sourceTree = ""; }; + 771901AA2AE9581400D9C918 /* SwapConfirmViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConfirmViewModelFactory.swift; sourceTree = ""; }; + 771901AF2AE97DA500D9C918 /* SwapConfirmViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConfirmViewModels.swift; sourceTree = ""; }; 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericBorderedView.swift; sourceTree = ""; }; 77204EA52A1E0EAA00BBDE4A /* WalletConnectionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectionsView.swift; sourceTree = ""; }; 7725062B2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferendumSearchViewLayout.swift; sourceTree = ""; }; @@ -9630,6 +9634,25 @@ path = View; sourceTree = ""; }; + 771901AC2AE9730800D9C918 /* View */ = { + isa = PBXGroup; + children = ( + 382B8763E4FDA7C4E2EF06A8 /* SwapConfirmViewLayout.swift */, + 7719019C2AE6996600D9C918 /* SwapPairView.swift */, + 7719019E2AE6C9DC00D9C918 /* SwapElementView.swift */, + ); + path = View; + sourceTree = ""; + }; + 771901AE2AE9733200D9C918 /* Model */ = { + isa = PBXGroup; + children = ( + 771901AA2AE9581400D9C918 /* SwapConfirmViewModelFactory.swift */, + 771901AF2AE97DA500D9C918 /* SwapConfirmViewModels.swift */, + ); + path = Model; + sourceTree = ""; + }; 7726E232D196BDD627329E24 /* ParaStkStakeSetup */ = { isa = PBXGroup; children = ( @@ -10100,15 +10123,14 @@ 7E5E800395DC908962C169CF /* Confirm */ = { isa = PBXGroup; children = ( + 771901AE2AE9733200D9C918 /* Model */, + 771901AC2AE9730800D9C918 /* View */, 796859464B823B60746C5DE5 /* SwapConfirmProtocols.swift */, 7CD1FBC9C063951E2520265D /* SwapConfirmWireframe.swift */, 48E6BE303472080271AAC917 /* SwapConfirmPresenter.swift */, 2DEED7526468089FE8A8989C /* SwapConfirmInteractor.swift */, F2FB715B933FB8E34A553A80 /* SwapConfirmViewController.swift */, - 382B8763E4FDA7C4E2EF06A8 /* SwapConfirmViewLayout.swift */, AF5DEDDC53639DFCF524D794 /* SwapConfirmViewFactory.swift */, - 7719019C2AE6996600D9C918 /* SwapPairView.swift */, - 7719019E2AE6C9DC00D9C918 /* SwapElementView.swift */, ); path = Confirm; sourceTree = ""; @@ -22212,6 +22234,7 @@ 84350AD4284580F50031EF24 /* StakingTotalStakePresentable.swift in Sources */, 6B393DCF67CF97FDA580C69B /* DAppSearchPresenter.swift in Sources */, 84540172292F907B00213402 /* BlurBackgroundView.swift in Sources */, + 771901B02AE97DA500D9C918 /* SwapConfirmViewModels.swift in Sources */, DC682E96D056C069902B9C31 /* DAppSearchViewController.swift in Sources */, 286577D8FE44D1F9E7BBDCA9 /* DAppSearchViewLayout.swift in Sources */, 8825018E29D2E31F001BE6FB /* ModuleNameResolver.swift in Sources */, @@ -22400,6 +22423,7 @@ 2B682D343F75EBEB8A1E65BD /* DAppAuthSettingsViewLayout.swift in Sources */, 148748ACAE23B7D15144015B /* DAppAuthSettingsViewFactory.swift in Sources */, B1F86CA723BB4D69C5EF989D /* ParaStkStakeSetupProtocols.swift in Sources */, + 771901AB2AE9581400D9C918 /* SwapConfirmViewModelFactory.swift in Sources */, 623474C49445578F030291B0 /* ParaStkStakeSetupWireframe.swift in Sources */, 8490110B29E5A491005D688B /* URIScanViewFactory.swift in Sources */, 840B3D672899BFD200DA1DA9 /* QRScannerViewSettings.swift in Sources */, diff --git a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift new file mode 100644 index 0000000000..73386b8baa --- /dev/null +++ b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift @@ -0,0 +1,168 @@ +import Foundation +import SoraFoundation +import BigInt + +protocol SwapConfirmViewModelFactoryProtocol { + var locale: Locale { get set } + + func assetViewModel( + chainAsset: ChainAsset, + amount: BigUInt, + priceData: PriceData? + ) -> SwapAssetAmountViewModel + func rateViewModel(from params: RateParams) -> String + func priceDifferenceViewModel( + rateParams: RateParams, + priceIn: PriceData?, + priceOut: PriceData? + ) -> DifferenceViewModel? + func slippageViewModel(slippage: BigRational) -> String + func feeViewModel(fee: BigUInt, chainAsset: ChainAsset, priceData: PriceData?) -> SwapFeeViewModel + func walletViewModel(walletAddress: WalletDisplayAddress) -> WalletAccountViewModel? +} + +final class SwapConfirmViewModelFactory { + let percentForamatter: LocalizableResource + let balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol + let walletViewModelFactory = WalletAccountViewModelFactory() + let networkViewModelFactory: NetworkViewModelFactoryProtocol + private var localizedPercentForamatter: NumberFormatter + private var priceDifferenceWarningRange: (start: Decimal, end: Decimal) = (start: 10, end: 20) + + var locale: Locale { + didSet { + localizedPercentForamatter = percentForamatter.value(for: locale) + } + } + + init( + locale: Locale, + balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol, + networkViewModelFactory: NetworkViewModelFactoryProtocol, + percentForamatter: LocalizableResource + ) { + self.balanceViewModelFactoryFacade = balanceViewModelFactoryFacade + self.networkViewModelFactory = networkViewModelFactory + self.percentForamatter = percentForamatter + self.locale = locale + localizedPercentForamatter = percentForamatter.value(for: locale) + } +} + +extension SwapConfirmViewModelFactory: SwapConfirmViewModelFactoryProtocol { + func assetViewModel( + chainAsset: ChainAsset, + amount: BigUInt, + priceData: PriceData? + ) -> SwapAssetAmountViewModel { + let networkViewModel = networkViewModelFactory.createViewModel(from: chainAsset.chain) + let assetIcon: ImageViewModelProtocol = chainAsset.asset.icon.map { RemoteImageViewModel(url: $0) } ?? + StaticImageViewModel(image: R.image.iconDefaultToken()!) + let amountDecimal = Decimal.fromSubstrateAmount( + amount, + precision: chainAsset.assetDisplayInfo.assetPrecision + ) ?? 0 + let balanceViewModel = balanceViewModelFactoryFacade.balanceFromPrice( + targetAssetInfo: chainAsset.assetDisplayInfo, + amount: amountDecimal, + priceData: priceData + ).value(for: locale) + + return .init( + imageViewModel: assetIcon, + hub: networkViewModel, + balance: balanceViewModel + ) + } + + func rateViewModel(from params: RateParams) -> String { + guard + let amountOutDecimal = Decimal.fromSubstrateAmount( + params.amountOut, + precision: params.assetDisplayInfoOut.assetPrecision + ), + let amountInDecimal = Decimal.fromSubstrateAmount( + params.amountIn, + precision: params.assetDisplayInfoIn.assetPrecision + ), + amountInDecimal != 0 else { + return "" + } + + let difference = amountOutDecimal / amountInDecimal + + let amountIn = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: params.assetDisplayInfoIn, + value: 1 + ).value(for: locale) + let amountOut = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: params.assetDisplayInfoOut, + value: difference ?? 0 + ).value(for: locale) + + return "\(amountIn) = \(amountOut)" + } + + func priceDifferenceViewModel( + rateParams params: RateParams, + priceIn: PriceData?, + priceOut: PriceData? + ) -> DifferenceViewModel? { + guard + let amountOutDecimal = Decimal.fromSubstrateAmount( + params.amountOut, + precision: params.assetDisplayInfoOut.assetPrecision + ), + let amountInDecimal = Decimal.fromSubstrateAmount( + params.amountIn, + precision: params.assetDisplayInfoIn.assetPrecision + ) else { + return nil + } + guard let priceIn = priceIn?.decimalRate, + let priceOut = priceOut?.decimalRate else { + return nil + } + + let amountPriceIn = amountInDecimal * priceIn + let amountPriceOut = amountOutDecimal * priceOut + + guard amountPriceOut > 0 else { + return nil + } + + let diff = (amountPriceIn - amountPriceOut) / amountPriceOut * 100 + let diffString = localizedPercentForamatter.stringFromDecimal(diff) ?? "" + + switch abs(diff) { + case _ where abs(diff) > priceDifferenceWarningRange.end: + return .init(details: diffString, attention: .high) + case priceDifferenceWarningRange.start ..< priceDifferenceWarningRange.end: + return .init(details: diffString, attention: .medium) + default: + return .init(details: diffString, attention: .low) + } + } + + func slippageViewModel(slippage: BigRational) -> String { + slippage.decimalValue.map { localizedPercentForamatter.stringFromDecimal($0) ?? "" } ?? "" + } + + func feeViewModel(fee: BigUInt, chainAsset: ChainAsset, priceData: PriceData?) -> SwapFeeViewModel { + let amountDecimal = Decimal.fromSubstrateAmount( + fee, + precision: chainAsset.assetDisplayInfo.assetPrecision + ) ?? 0 + let balanceViewModel = balanceViewModelFactoryFacade.balanceFromPrice( + targetAssetInfo: chainAsset.assetDisplayInfo, + amount: amountDecimal, + priceData: priceData + ).value(for: locale) + + return .init(isEditable: false, balanceViewModel: balanceViewModel) + } + + func walletViewModel(walletAddress: WalletDisplayAddress) -> WalletAccountViewModel? { + try? walletViewModelFactory.createViewModel(from: walletAddress) + } +} diff --git a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModels.swift b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModels.swift new file mode 100644 index 0000000000..b5361defd8 --- /dev/null +++ b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModels.swift @@ -0,0 +1,16 @@ +struct SwapAssetAmountViewModel { + let imageViewModel: ImageViewModelProtocol? + let hub: NetworkViewModel + let balance: BalanceViewModelProtocol +} + +struct DifferenceViewModel { + let details: String + let attention: AttentionState +} + +enum AttentionState { + case high + case medium + case low +} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift index 734b2187b0..4608f9b2e2 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift @@ -6,12 +6,14 @@ final class SwapConfirmInteractor: SwapBaseInteractor { let receiveChainAsset: ChainAsset let feeChainAsset: ChainAsset let slippage: BigRational + let quote: AssetConversion.Quote init( payChainAsset: ChainAsset, receiveChainAsset: ChainAsset, feeChainAsset: ChainAsset, slippage: BigRational, + quote: AssetConversion.Quote, assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol, runtimeService: RuntimeProviderProtocol, @@ -27,6 +29,7 @@ final class SwapConfirmInteractor: SwapBaseInteractor { self.receiveChainAsset = receiveChainAsset self.feeChainAsset = feeChainAsset self.slippage = slippage + self.quote = quote super.init( assetConversionOperationFactory: assetConversionOperationFactory, diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 971fd3cfd4..554f9ddd49 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -1,34 +1,210 @@ import Foundation import BigInt +import SoraFoundation final class SwapConfirmPresenter { weak var view: SwapConfirmViewProtocol? let wireframe: SwapConfirmWireframeProtocol let interactor: SwapConfirmInteractorInputProtocol + let chainAssetIn: ChainAsset + let chainAssetOut: ChainAsset + let slippage: BigRational + let feeChainAsset: ChainAsset + + private var viewModelFactory: SwapConfirmViewModelFactoryProtocol + private var feePriceData: PriceData? + private var chainAssetInPriceData: PriceData? + private var chainAssetOutPriceData: PriceData? + private var quote: AssetConversion.Quote? + private var fee: BigUInt? + private var payAccountId: AccountId? + private var chainAccountResponse: MetaChainAccountResponse + private var quoteArgs: AssetConversion.QuoteArgs? init( interactor: SwapConfirmInteractorInputProtocol, - wireframe: SwapConfirmWireframeProtocol + wireframe: SwapConfirmWireframeProtocol, + viewModelFactory: SwapConfirmViewModelFactoryProtocol, + chainAssetIn: ChainAsset, + chainAssetOut: ChainAsset, + feeChainAsset: ChainAsset, + quote: AssetConversion.Quote, + quoteArgs: AssetConversion.QuoteArgs, + slippage: BigRational, + chainAccountResponse: MetaChainAccountResponse ) { self.interactor = interactor self.wireframe = wireframe + self.viewModelFactory = viewModelFactory + self.chainAssetIn = chainAssetIn + self.chainAssetOut = chainAssetOut + self.feeChainAsset = feeChainAsset + self.quote = quote + self.slippage = slippage + self.chainAccountResponse = chainAccountResponse + self.quoteArgs = quoteArgs + + localizationManager = localizationManager + } + + func provideAssetInViewModel() { + guard let quote = quote else { + return + } + let viewModel = viewModelFactory.assetViewModel( + chainAsset: chainAssetIn, + amount: quote.amountIn, + priceData: chainAssetInPriceData + ) + view?.didReceiveAssetIn(viewModel: viewModel) + } + + func provideAssetOutViewModel() { + guard let quote = quote else { + return + } + let viewModel = viewModelFactory.assetViewModel( + chainAsset: chainAssetOut, + amount: quote.amountOut, + priceData: chainAssetOutPriceData + ) + view?.didReceiveAssetOut(viewModel: viewModel) + } + + func provideRateViewModel() { + guard let quote = quote else { + view?.didReceiveRate(viewModel: .loading) + return + } + + let params = RateParams( + assetDisplayInfoIn: chainAssetIn.assetDisplayInfo, + assetDisplayInfoOut: chainAssetOut.assetDisplayInfo, + amountIn: quote.amountIn, + amountOut: quote.amountOut + ) + let viewModel = viewModelFactory.rateViewModel(from: params) + + view?.didReceiveRate(viewModel: .loaded(value: viewModel)) + } + + func providePriceDifferenceViewModel() { + guard let quote = quote else { + view?.didReceivePriceDifference(viewModel: .loading) + return + } + + let params = RateParams( + assetDisplayInfoIn: chainAssetIn.assetDisplayInfo, + assetDisplayInfoOut: chainAssetOut.assetDisplayInfo, + amountIn: quote.amountIn, + amountOut: quote.amountOut + ) + + if let viewModel = viewModelFactory.priceDifferenceViewModel( + rateParams: params, + priceIn: chainAssetInPriceData, + priceOut: chainAssetOutPriceData + ) { + view?.didReceivePriceDifference(viewModel: .loaded(value: viewModel)) + } else { + view?.didReceivePriceDifference(viewModel: nil) + } + } + + func provideSlippageViewModel() { + let viewModel = viewModelFactory.slippageViewModel(slippage: slippage) + view?.didReceiveSlippage(viewModel: viewModel) + } + + func provideFeeViewModel() { + guard let fee = fee else { + view?.didReceiveNetworkFee(viewModel: .loading) + return + } + let viewModel = viewModelFactory.feeViewModel( + fee: fee, + chainAsset: feeChainAsset, + priceData: feePriceData + ) + + view?.didReceiveNetworkFee(viewModel: .loaded(value: viewModel)) + } + + func provideWalletViewModel() { + guard let walletAddress = WalletDisplayAddress(response: chainAccountResponse) else { + view?.didReceiveWallet(viewModel: nil) + return + } + let viewModel = viewModelFactory.walletViewModel(walletAddress: walletAddress) + + view?.didReceiveWallet(viewModel: viewModel) + } + + func updateViews() { + provideAssetInViewModel() + provideAssetOutViewModel() + provideRateViewModel() + providePriceDifferenceViewModel() + provideSlippageViewModel() + provideFeeViewModel() + provideWalletViewModel() + } + + func estimateFee() { + guard let quoteArgs = quoteArgs else { + return + } + interactor.calculateQuote(for: quoteArgs) } } extension SwapConfirmPresenter: SwapConfirmPresenterProtocol { func setup() { interactor.setup() + estimateFee() + updateViews() } } extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { - func didReceive(quote _: AssetConversion.Quote, for _: AssetConversion.QuoteArgs) {} + func didReceive(quote: AssetConversion.Quote, for _: AssetConversion.QuoteArgs) { + self.quote = quote + provideAssetInViewModel() + provideAssetOutViewModel() + provideRateViewModel() + providePriceDifferenceViewModel() + } - func didReceive(fee _: BigUInt?, transactionId _: TransactionFeeId) {} + func didReceive(fee: BigUInt?, transactionId _: TransactionFeeId) { + self.fee = fee + provideFeeViewModel() + } func didReceive(error _: SwapSetupError) {} - func didReceive(price _: PriceData?, priceId _: AssetModel.PriceId) {} + func didReceive(price: PriceData?, priceId: AssetModel.PriceId) { + if priceId == chainAssetIn.asset.priceId { + chainAssetInPriceData = price + } + if priceId == chainAssetOut.asset.priceId { + chainAssetOutPriceData = price + } + if priceId == feeChainAsset.asset.priceId { + feePriceData = price + } + } + + func didReceive(payAccountId: AccountId?) { + self.payAccountId = payAccountId + } +} - func didReceive(payAccountId _: AccountId?) {} +extension SwapConfirmPresenter: Localizable { + func applyLocalization() { + if view?.isSetup == true { + viewModelFactory.locale = selectedLocale + updateViews() + } + } } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift index 22fabf993c..52c1f2f978 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift @@ -1,4 +1,12 @@ -protocol SwapConfirmViewProtocol: ControllerBackedProtocol {} +protocol SwapConfirmViewProtocol: ControllerBackedProtocol { + func didReceiveAssetIn(viewModel: SwapAssetAmountViewModel) + func didReceiveAssetOut(viewModel: SwapAssetAmountViewModel) + func didReceiveRate(viewModel: LoadableViewModelState) + func didReceivePriceDifference(viewModel: LoadableViewModelState?) + func didReceiveSlippage(viewModel: String) + func didReceiveNetworkFee(viewModel: LoadableViewModelState) + func didReceiveWallet(viewModel: WalletAccountViewModel?) +} protocol SwapConfirmPresenterProtocol: AnyObject { func setup() diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift index c0cb42ae8f..07dc4f763e 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift @@ -40,7 +40,53 @@ final class SwapConfirmViewController: UIViewController, ViewHolder { private func setupHandlers() {} } -extension SwapConfirmViewController: SwapConfirmViewProtocol {} +extension SwapConfirmViewController: SwapConfirmViewProtocol { + func didReceiveAssetIn(viewModel: SwapAssetAmountViewModel) { + rootView.pairsView.leftAssetView.bind(viewModel: viewModel) + } + + func didReceiveAssetOut(viewModel: SwapAssetAmountViewModel) { + rootView.pairsView.rigthAssetView.bind(viewModel: viewModel) + } + + func didReceiveRate(viewModel: LoadableViewModelState) { + rootView.rateCell.bind(loadableViewModel: viewModel) + } + + func didReceivePriceDifference(viewModel: LoadableViewModelState?) { + if let viewModel = viewModel { + rootView.priceDifferenceCell.isHidden = false + rootView.priceDifferenceCell.bind(differenceViewModel: viewModel) + } else { + rootView.priceDifferenceCell.isHidden = true + } + } + + func didReceiveSlippage(viewModel: String) { + rootView.slippageCell.bind(loadableViewModel: .loaded(value: viewModel)) + } + + func didReceiveNetworkFee(viewModel: LoadableViewModelState) { + rootView.networkFeeCell.bind(loadableViewModel: viewModel) + } + + func didReceiveWallet(viewModel: WalletAccountViewModel?) { + guard let viewModel = viewModel else { + rootView.walletTableView.isHidden = true + return + } + rootView.walletTableView.isHidden = false + rootView.walletCell.bind(viewModel: .init( + details: viewModel.walletName ?? "", + imageViewModel: viewModel.walletIcon + )) + + rootView.accountCell.bind(viewModel: .init( + details: viewModel.address, + imageViewModel: viewModel.addressIcon + )) + } +} extension SwapConfirmViewController: Localizable { func applyLocalization() { diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index 33bd346ede..e2582ba280 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -7,19 +7,51 @@ struct SwapConfirmViewFactory { payChainAsset: ChainAsset, receiveChainAsset: ChainAsset, feeChainAsset: ChainAsset, - slippage: BigRational + slippage: BigRational, + quote: AssetConversion.Quote, + quoteArgs: AssetConversion.QuoteArgs ) -> SwapConfirmViewProtocol? { + let accountRequest = payChainAsset.chain.accountRequest() + + guard let currencyManager = CurrencyManager.shared, + let selectedAccount = SelectedWalletSettings.shared.value, + let chainAccountResponse = selectedAccount.fetchMetaChainAccount(for: accountRequest) else { + return nil + } guard let interactor = createInteractor( payChainAsset: payChainAsset, receiveChainAsset: receiveChainAsset, feeChainAsset: feeChainAsset, - slippage: slippage + slippage: slippage, + quote: quote ) else { return nil } let wireframe = SwapConfirmWireframe() - let presenter = SwapConfirmPresenter(interactor: interactor, wireframe: wireframe) + let balanceViewModelFactoryFacade = BalanceViewModelFactoryFacade( + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager) + ) + + let viewModelFactory = SwapConfirmViewModelFactory( + locale: LocalizationManager.shared.selectedLocale, + balanceViewModelFactoryFacade: balanceViewModelFactoryFacade, + networkViewModelFactory: NetworkViewModelFactory(), + percentForamatter: NumberFormatter.percentSingle.localizableResource() + ) + + let presenter = SwapConfirmPresenter( + interactor: interactor, + wireframe: wireframe, + viewModelFactory: viewModelFactory, + chainAssetIn: payChainAsset, + chainAssetOut: receiveChainAsset, + feeChainAsset: feeChainAsset, + quote: quote, + quoteArgs: quoteArgs, + slippage: slippage, + chainAccountResponse: chainAccountResponse + ) let view = SwapConfirmViewController( presenter: presenter, @@ -36,7 +68,8 @@ struct SwapConfirmViewFactory { payChainAsset: ChainAsset, receiveChainAsset: ChainAsset, feeChainAsset: ChainAsset, - slippage: BigRational + slippage: BigRational, + quote: AssetConversion.Quote ) -> SwapConfirmInteractor? { let westmintChainId = KnowChainId.westmint let chainRegistry = ChainRegistryFacade.sharedRegistry @@ -68,6 +101,7 @@ struct SwapConfirmViewFactory { receiveChainAsset: receiveChainAsset, feeChainAsset: feeChainAsset, slippage: slippage, + quote: quote, assetConversionOperationFactory: assetConversionOperationFactory, assetConversionExtrinsicService: AssetHubExtrinsicService(chain: chainModel), runtimeService: runtimeService, diff --git a/novawallet/Modules/Swaps/Confirm/SwapElementView.swift b/novawallet/Modules/Swaps/Confirm/SwapElementView.swift deleted file mode 100644 index f73baf3ad7..0000000000 --- a/novawallet/Modules/Swaps/Confirm/SwapElementView.swift +++ /dev/null @@ -1,81 +0,0 @@ -import UIKit -import SoraUI - -final class SwapElementView: UIView { - var contentInsets: UIEdgeInsets = .zero { - didSet { - contentView.snp.updateConstraints { - $0.edges.equalToSuperview().inset(contentInsets) - } - } - } - - let backgroundView: RoundedView = .create { - $0.apply(style: .roundedLightCell) - } - - let imageView: AssetIconView = .create { - $0.contentInsets = .zero - $0.backgroundView.cornerRadius = 24 - } - - let valueLabel: UILabel = .init( - style: .semiboldBodyPrimary, - textAlignment: .center, - numberOfLines: 1 - ) - - let priceLabel: UILabel = .init( - style: .footnoteSecondary, - textAlignment: .center, - numberOfLines: 1 - ) - - let hubIconNameView: IconDetailsView = .create { - $0.spacing = 8 - $0.iconWidth = 16 - $0.mode = .iconDetails - $0.detailsLabel.apply(style: .footnoteSecondary) - } - - override var intrinsicContentSize: CGSize { - .init(width: UIView.noIntrinsicMetric, height: 168) - } - - override init(frame: CGRect) { - super.init(frame: frame) - - backgroundColor = .clear - - setupLayout() - } - - lazy var contentView = UIView.vStack(distribution: .equalCentering, [ - imageView, - valueLabel, - priceLabel, - hubIconNameView - ]) - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupLayout() { - addSubview(backgroundView) - addSubview(contentView) - - backgroundView.snp.makeConstraints { - $0.edges.equalToSuperview() - } - - contentView.snp.makeConstraints { - $0.edges.equalToSuperview().inset(contentInsets) - } - - imageView.snp.makeConstraints { - $0.height.width.equalTo(48) - } - } -} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift b/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift similarity index 100% rename from novawallet/Modules/Swaps/Confirm/SwapConfirmViewLayout.swift rename to novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift diff --git a/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift b/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift new file mode 100644 index 0000000000..34d67ffe29 --- /dev/null +++ b/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift @@ -0,0 +1,134 @@ +import UIKit +import SoraUI + +final class SwapElementView: UIView { + var contentInsets: UIEdgeInsets = .zero { + didSet { + contentView.snp.updateConstraints { + $0.edges.equalToSuperview().inset(contentInsets) + } + } + } + + static let assetIconRadius: CGFloat = 24 + + let backgroundView: RoundedView = .create { + $0.apply(style: .roundedLightCell) + } + + let assetIconView: AssetIconView = .create { + $0.contentInsets = .zero + $0.backgroundView.cornerRadius = SwapElementView.assetIconRadius + } + + let valueLabel: UILabel = .init( + style: .semiboldBodyPrimary, + textAlignment: .center, + numberOfLines: 1 + ) + + let priceLabel: UILabel = .init( + style: .footnoteSecondary, + textAlignment: .center, + numberOfLines: 1 + ) + + let hubIconNameView: IconDetailsView = .create { + $0.spacing = 8 + $0.iconWidth = 16 + $0.mode = .iconDetails + $0.detailsLabel.apply(style: .footnoteSecondary) + } + + private var hubImageViewModel: ImageViewModelProtocol? + + override var intrinsicContentSize: CGSize { + .init(width: UIView.noIntrinsicMetric, height: 168) + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .clear + + setupLayout() + } + + lazy var contentView = UIView.vStack(distribution: .equalCentering, [ + assetIconView, + valueLabel, + priceLabel, + hubIconNameView + ]) + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + addSubview(backgroundView) + addSubview(contentView) + + backgroundView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + contentView.snp.makeConstraints { + $0.edges.equalToSuperview().inset(contentInsets) + } + + assetIconView.snp.makeConstraints { + $0.height.width.equalTo(48) + } + } +} + +extension SwapElementView { + func bind(viewModel: SwapAssetAmountViewModel) { + let width = 2 * Self.assetIconRadius - assetIconView.contentInsets.left - assetIconView.contentInsets.right + let height = 2 * Self.assetIconRadius - assetIconView.contentInsets.top - assetIconView.contentInsets.bottom + let size = CGSize(width: width, height: height) + assetIconView.bind(viewModel: viewModel.imageViewModel, size: size) + + viewModel.hub.icon?.cancel(on: hubIconNameView.imageView) + hubImageViewModel = viewModel.hub.icon + viewModel.hub.icon?.loadImage( + on: hubIconNameView.imageView, + targetSize: .init( + width: hubIconNameView.iconWidth, + height: hubIconNameView.iconWidth + ), + animated: true + ) + hubIconNameView.detailsLabel.text = viewModel.hub.name + valueLabel.text = viewModel.balance.amount + priceLabel.text = viewModel.balance.price + } +} + +extension SwapRateViewCell { + func bind(attention: AttentionState) { + switch attention { + case .high: + titleButton.imageWithTitleView?.titleColor = R.color.colorTextNegative() + case .medium: + titleButton.imageWithTitleView?.titleColor = R.color.colorTextWarning() + case .low: + titleButton.imageWithTitleView?.titleColor = R.color.colorTextPrimary() + } + } + + func bind(differenceViewModel: LoadableViewModelState) { + switch differenceViewModel { + case .loading: + bind(loadableViewModel: .loading) + case let .cached(value): + bind(attention: value.attention) + bind(loadableViewModel: .cached(value: value.details)) + case let .loaded(value): + bind(attention: value.attention) + bind(loadableViewModel: .loaded(value: value.details)) + } + } +} diff --git a/novawallet/Modules/Swaps/Confirm/SwapPairView.swift b/novawallet/Modules/Swaps/Confirm/View/SwapPairView.swift similarity index 81% rename from novawallet/Modules/Swaps/Confirm/SwapPairView.swift rename to novawallet/Modules/Swaps/Confirm/View/SwapPairView.swift index 43ca8e59c9..1f032d8e71 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapPairView.swift +++ b/novawallet/Modules/Swaps/Confirm/View/SwapPairView.swift @@ -42,4 +42,14 @@ final class SwapPairView: UIView { $0.center.equalTo(stackView.snp.center) } } + + override var intrinsicContentSize: CGSize { + CGSize( + width: UIView.noIntrinsicMetric, + height: max( + leftAssetView.intrinsicContentSize.height, + rigthAssetView.intrinsicContentSize.height + ) + ) + } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 126454c0fb..79d0f8314f 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -490,7 +490,9 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { DataValidationRunner(validators: validators).runValidation { [weak self] in guard let receiveChainAsset = self?.receiveChainAsset, - let slippage = self?.slippage else { + let slippage = self?.slippage, + let quote = self?.quote, + let quoteArgs = self?.quoteArgs else { return } @@ -499,7 +501,9 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { payChainAsset: payChainAsset, receiveChainAsset: receiveChainAsset, feeChainAsset: feeChainAsset, - slippage: slippage + slippage: slippage, + quote: quote, + quoteArgs: quoteArgs ) } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 30416e8d7a..57ccf08963 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -71,7 +71,9 @@ protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryabl payChainAsset: ChainAsset, receiveChainAsset: ChainAsset, feeChainAsset: ChainAsset, - slippage: BigRational + slippage: BigRational, + quote: AssetConversion.Quote, + quoteArgs: AssetConversion.QuoteArgs ) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index 26504d0505..3ef481cbed 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -74,13 +74,17 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { payChainAsset: ChainAsset, receiveChainAsset: ChainAsset, feeChainAsset: ChainAsset, - slippage: BigRational + slippage: BigRational, + quote: AssetConversion.Quote, + quoteArgs: AssetConversion.QuoteArgs ) { guard let confimView = SwapConfirmViewFactory.createView( payChainAsset: payChainAsset, receiveChainAsset: receiveChainAsset, feeChainAsset: feeChainAsset, - slippage: slippage + slippage: slippage, + quote: quote, + quoteArgs: quoteArgs ) else { return } From af9e6422773faeab10bdb3bcca7750572bb341cd Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 26 Oct 2023 09:11:19 +0300 Subject: [PATCH 072/204] fix UI --- .../Swaps/Base/SwapBaseInteractor.swift | 2 +- .../Swaps/Base/SwapBaseProtocols.swift | 1 + .../Swaps/Confirm/SwapConfirmInteractor.swift | 5 +- .../Swaps/Confirm/SwapConfirmPresenter.swift | 27 +++++++++-- .../Confirm/SwapConfirmViewFactory.swift | 2 +- .../Swaps/Confirm/View/SwapElementView.swift | 46 +++++++++++++------ .../Swaps/Setup/SwapSetupProtocols.swift | 4 +- 7 files changed, 63 insertions(+), 24 deletions(-) diff --git a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift index c1dde85d90..0e21d91a32 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift @@ -3,7 +3,7 @@ import RobinHood import BigInt class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapBaseInteractorInputProtocol { - weak var basePresenter: SwapSetupInteractorOutputProtocol? + weak var basePresenter: SwapBaseInteractorOutputProtocol? let assetConversionOperationFactory: AssetConversionOperationFactoryProtocol let assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol let runtimeService: RuntimeProviderProtocol diff --git a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift index c392509ef0..ee3b80a4c3 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift @@ -13,4 +13,5 @@ protocol SwapBaseInteractorOutputProtocol: AnyObject { func didReceive(error: SwapSetupError) func didReceive(price: PriceData?, priceId: AssetModel.PriceId) func didReceive(payAccountId: AccountId?) + func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId: AccountId) } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift index 4608f9b2e2..981960441a 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift @@ -1,7 +1,10 @@ import UIKit final class SwapConfirmInteractor: SwapBaseInteractor { - weak var presenter: SwapConfirmInteractorOutputProtocol? + var presenter: SwapConfirmInteractorOutputProtocol? { + basePresenter as? SwapConfirmInteractorOutputProtocol + } + let payChainAsset: ChainAsset let receiveChainAsset: ChainAsset let feeChainAsset: ChainAsset diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 554f9ddd49..d9eb4b32ce 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -20,6 +20,7 @@ final class SwapConfirmPresenter { private var payAccountId: AccountId? private var chainAccountResponse: MetaChainAccountResponse private var quoteArgs: AssetConversion.QuoteArgs? + private var balances: [ChainAssetId: AssetBalance?] = [:] init( interactor: SwapConfirmInteractorInputProtocol, @@ -152,10 +153,22 @@ final class SwapConfirmPresenter { } func estimateFee() { - guard let quoteArgs = quoteArgs else { + guard let quote = quote, let quoteArgs = quoteArgs, let accountId = payAccountId else { return } - interactor.calculateQuote(for: quoteArgs) + fee = nil + provideFeeViewModel() + + interactor.calculateFee(args: .init( + assetIn: quote.assetIn, + amountIn: quote.amountIn, + assetOut: quote.assetOut, + amountOut: quote.amountOut, + receiver: accountId, + direction: quoteArgs.direction, + slippage: slippage + ) + ) } } @@ -168,12 +181,15 @@ extension SwapConfirmPresenter: SwapConfirmPresenterProtocol { } extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { - func didReceive(quote: AssetConversion.Quote, for _: AssetConversion.QuoteArgs) { + func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) { self.quote = quote + self.quoteArgs = quoteArgs + provideAssetInViewModel() provideAssetOutViewModel() provideRateViewModel() providePriceDifferenceViewModel() + estimateFee() } func didReceive(fee: BigUInt?, transactionId _: TransactionFeeId) { @@ -197,6 +213,11 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { func didReceive(payAccountId: AccountId?) { self.payAccountId = payAccountId + estimateFee() + } + + func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId: AccountId) { + balances[chainAsset] = balance } } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index e2582ba280..b378561e20 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -59,7 +59,7 @@ struct SwapConfirmViewFactory { ) presenter.view = view - interactor.presenter = presenter + interactor.basePresenter = presenter return view } diff --git a/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift b/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift index 34d67ffe29..1cc1104c0f 100644 --- a/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift +++ b/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift @@ -1,8 +1,9 @@ import UIKit import SoraUI +import SnapKit final class SwapElementView: UIView { - var contentInsets: UIEdgeInsets = .zero { + var contentInsets: UIEdgeInsets = .init(top: 16, left: 12, bottom: 20, right: 12) { didSet { contentView.snp.updateConstraints { $0.edges.equalToSuperview().inset(contentInsets) @@ -42,10 +43,6 @@ final class SwapElementView: UIView { private var hubImageViewModel: ImageViewModelProtocol? - override var intrinsicContentSize: CGSize { - .init(width: UIView.noIntrinsicMetric, height: 168) - } - override init(frame: CGRect) { super.init(frame: frame) @@ -54,12 +51,7 @@ final class SwapElementView: UIView { setupLayout() } - lazy var contentView = UIView.vStack(distribution: .equalCentering, [ - assetIconView, - valueLabel, - priceLabel, - hubIconNameView - ]) + lazy var contentView = UIView() @available(*, unavailable) required init?(coder _: NSCoder) { @@ -70,6 +62,34 @@ final class SwapElementView: UIView { addSubview(backgroundView) addSubview(contentView) + contentView.addSubview(assetIconView) + contentView.addSubview(valueLabel) + contentView.addSubview(priceLabel) + contentView.addSubview(hubIconNameView) + + assetIconView.snp.makeConstraints { + $0.width.height.equalTo(48) + $0.top.centerX.equalToSuperview() + } + + valueLabel.snp.makeConstraints { + $0.top.equalTo(assetIconView.snp.bottom).offset(8) + $0.leading.trailing.equalToSuperview() + } + + priceLabel.snp.makeConstraints { + $0.top.equalTo(valueLabel.snp.bottom).offset(2) + $0.leading.trailing.equalToSuperview() + } + + hubIconNameView.snp.makeConstraints { + $0.top.equalTo(priceLabel.snp.bottom).offset(16) + $0.leading.greaterThanOrEqualToSuperview() + $0.trailing.lessThanOrEqualToSuperview() + $0.centerX.equalToSuperview().priority(.high) + $0.bottom.equalToSuperview() + } + backgroundView.snp.makeConstraints { $0.edges.equalToSuperview() } @@ -77,10 +97,6 @@ final class SwapElementView: UIView { contentView.snp.makeConstraints { $0.edges.equalToSuperview().inset(contentInsets) } - - assetIconView.snp.makeConstraints { - $0.height.width.equalTo(48) - } } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 57ccf08963..6024658b64 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -39,9 +39,7 @@ protocol SwapSetupInteractorInputProtocol: SwapBaseInteractorInputProtocol { func update(feeChainAsset: ChainAsset?) } -protocol SwapSetupInteractorOutputProtocol: SwapBaseInteractorOutputProtocol { - func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId: AccountId) -} +protocol SwapSetupInteractorOutputProtocol: SwapBaseInteractorOutputProtocol {} protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, ErrorPresentable, SwapErrorPresentable, ShortTextInfoPresentable { From 4e8a38e58c655bdfea4082bbc70d8b24487a8bfc Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 26 Oct 2023 10:18:30 +0300 Subject: [PATCH 073/204] renaming, cleanup --- novawallet.xcodeproj/project.pbxproj | 20 ++++--- .../View/StackTable/StackInfoTableCell.swift | 10 +++- .../ShortTextInfoPresentableExtensions.swift | 53 +++++++++++++++++++ ...{SwapRateView.swift => SwapInfoView.swift} | 6 +-- ...eViewCell.swift => SwapInfoViewCell.swift} | 2 +- .../Swaps/Confirm/SwapConfirmPresenter.swift | 52 +++++++++++++++++- .../Swaps/Confirm/SwapConfirmProtocols.swift | 10 +++- .../Confirm/SwapConfirmViewController.swift | 32 ++++++++++- .../Confirm/View/SwapConfirmViewLayout.swift | 18 +++++-- .../Swaps/Confirm/View/SwapElementView.swift | 2 +- .../Swaps/Setup/SwapSetupPresenter.swift | 32 +---------- .../Swaps/Setup/View/SwapDetailsView.swift | 2 +- .../Setup/View/SwapSetupViewLayout.swift | 2 +- .../Slippage/SwapSlippagePresenter.swift | 12 +---- novawallet/en.lproj/Localizable.strings | 1 + novawallet/ru.lproj/Localizable.strings | 1 + 16 files changed, 191 insertions(+), 64 deletions(-) create mode 100644 novawallet/Modules/Swaps/Base/ShortTextInfoPresentableExtensions.swift rename novawallet/Modules/Swaps/Base/View/{SwapRateView.swift => SwapInfoView.swift} (95%) rename novawallet/Modules/Swaps/Base/View/{SwapRateViewCell.swift => SwapInfoViewCell.swift} (82%) diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 5d277195c3..7c7db14ffc 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -694,10 +694,11 @@ 7719019F2AE6C9DC00D9C918 /* SwapElementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719019E2AE6C9DC00D9C918 /* SwapElementView.swift */; }; 771901A22AE7E34D00D9C918 /* SwapBaseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901A12AE7E34D00D9C918 /* SwapBaseInteractor.swift */; }; 771901A42AE7E48800D9C918 /* SwapBaseProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901A32AE7E48800D9C918 /* SwapBaseProtocols.swift */; }; - 771901A62AE8FF7E00D9C918 /* SwapRateViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901A52AE8FF7E00D9C918 /* SwapRateViewCell.swift */; }; + 771901A62AE8FF7E00D9C918 /* SwapInfoViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901A52AE8FF7E00D9C918 /* SwapInfoViewCell.swift */; }; 771901A82AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901A72AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift */; }; 771901AB2AE9581400D9C918 /* SwapConfirmViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901AA2AE9581400D9C918 /* SwapConfirmViewModelFactory.swift */; }; 771901B02AE97DA500D9C918 /* SwapConfirmViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901AF2AE97DA500D9C918 /* SwapConfirmViewModels.swift */; }; + 771901B22AEA3E5100D9C918 /* ShortTextInfoPresentableExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901B12AEA3E5100D9C918 /* ShortTextInfoPresentableExtensions.swift */; }; 77204EA42A1CD59100BBDE4A /* GenericBorderedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */; }; 77204EA62A1E0EAA00BBDE4A /* WalletConnectionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA52A1E0EAA00BBDE4A /* WalletConnectionsView.swift */; }; 7725062C2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725062B2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift */; }; @@ -725,7 +726,7 @@ 775F19532A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775F19522A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift */; }; 77740BBC2AD4A7B800E8C06F /* CollapsableContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBB2AD4A7B800E8C06F /* CollapsableContainerView.swift */; }; 77740BBE2AD4A7F500E8C06F /* SwapDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */; }; - 77740BC02AD4A80D00E8C06F /* SwapRateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */; }; + 77740BC02AD4A80D00E8C06F /* SwapInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BBF2AD4A80D00E8C06F /* SwapInfoView.swift */; }; 77740BC22AD69E3400E8C06F /* SwapMaxButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BC12AD69E3400E8C06F /* SwapMaxButtonView.swift */; }; 77740BC42AD8145500E8C06F /* PercentInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BC32AD8145500E8C06F /* PercentInputView.swift */; }; 77740BC62AD849D100E8C06F /* SlippagePercentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77740BC52AD849D100E8C06F /* SlippagePercentViewModel.swift */; }; @@ -4733,10 +4734,11 @@ 7719019E2AE6C9DC00D9C918 /* SwapElementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapElementView.swift; sourceTree = ""; }; 771901A12AE7E34D00D9C918 /* SwapBaseInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapBaseInteractor.swift; sourceTree = ""; }; 771901A32AE7E48800D9C918 /* SwapBaseProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapBaseProtocols.swift; sourceTree = ""; }; - 771901A52AE8FF7E00D9C918 /* SwapRateViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRateViewCell.swift; sourceTree = ""; }; + 771901A52AE8FF7E00D9C918 /* SwapInfoViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapInfoViewCell.swift; sourceTree = ""; }; 771901A72AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeViewCell.swift; sourceTree = ""; }; 771901AA2AE9581400D9C918 /* SwapConfirmViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConfirmViewModelFactory.swift; sourceTree = ""; }; 771901AF2AE97DA500D9C918 /* SwapConfirmViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConfirmViewModels.swift; sourceTree = ""; }; + 771901B12AEA3E5100D9C918 /* ShortTextInfoPresentableExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortTextInfoPresentableExtensions.swift; sourceTree = ""; }; 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericBorderedView.swift; sourceTree = ""; }; 77204EA52A1E0EAA00BBDE4A /* WalletConnectionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectionsView.swift; sourceTree = ""; }; 7725062B2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferendumSearchViewLayout.swift; sourceTree = ""; }; @@ -4765,7 +4767,7 @@ 775F19522A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoParachainPresenter.swift; sourceTree = ""; }; 77740BBB2AD4A7B800E8C06F /* CollapsableContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsableContainerView.swift; sourceTree = ""; }; 77740BBD2AD4A7F500E8C06F /* SwapDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapDetailsView.swift; sourceTree = ""; }; - 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRateView.swift; sourceTree = ""; }; + 77740BBF2AD4A80D00E8C06F /* SwapInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapInfoView.swift; sourceTree = ""; }; 77740BC12AD69E3400E8C06F /* SwapMaxButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapMaxButtonView.swift; sourceTree = ""; }; 77740BC32AD8145500E8C06F /* PercentInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PercentInputView.swift; sourceTree = ""; }; 77740BC52AD849D100E8C06F /* SlippagePercentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlippagePercentViewModel.swift; sourceTree = ""; }; @@ -9619,6 +9621,7 @@ 771901A92AE8FFDC00D9C918 /* View */, 771901A12AE7E34D00D9C918 /* SwapBaseInteractor.swift */, 771901A32AE7E48800D9C918 /* SwapBaseProtocols.swift */, + 771901B12AEA3E5100D9C918 /* ShortTextInfoPresentableExtensions.swift */, ); path = Base; sourceTree = ""; @@ -9627,8 +9630,8 @@ isa = PBXGroup; children = ( 771901A72AE8FF9F00D9C918 /* SwapNetworkFeeViewCell.swift */, - 771901A52AE8FF7E00D9C918 /* SwapRateViewCell.swift */, - 77740BBF2AD4A80D00E8C06F /* SwapRateView.swift */, + 771901A52AE8FF7E00D9C918 /* SwapInfoViewCell.swift */, + 77740BBF2AD4A80D00E8C06F /* SwapInfoView.swift */, 77ECB46F2ACEEE2D0015CE9F /* SwapNetworkFeeView.swift */, ); path = View; @@ -20124,7 +20127,7 @@ 84FD3DBB254104B600A234E3 /* WalletTransactionListUpdated.swift in Sources */, 842EBB2928908ADB00B952D8 /* WalletsListTableViewCell.swift in Sources */, 84C1706629961FAD00CBE531 /* GovernanceYourDelegationsInteractorError.swift in Sources */, - 771901A62AE8FF7E00D9C918 /* SwapRateViewCell.swift in Sources */, + 771901A62AE8FF7E00D9C918 /* SwapInfoViewCell.swift in Sources */, 8428765C24ADDE0200D91AD8 /* SettingsViewController.swift in Sources */, 84746B3028153E4C002642F4 /* GradientBannerModel.swift in Sources */, 8433B34A29B63661005E5D0F /* ExtrinsicSplitter.swift in Sources */, @@ -21603,6 +21606,7 @@ 1BFC90E1D8646F7429FFD5E6 /* ExportMnemonicProtocols.swift in Sources */, 848B59BA28BCB3CA0009543C /* LedgerBaseAccountConfirmationWireframe.swift in Sources */, 3133215566E418F40844A60E /* ExportMnemonicWireframe.swift in Sources */, + 771901B22AEA3E5100D9C918 /* ShortTextInfoPresentableExtensions.swift in Sources */, 8446F5F02817279600B7A86C /* HintListView.swift in Sources */, 84BFE89728C2314300140F1F /* ParaStkYieldBoostOperationFactory.swift in Sources */, 84A70DA129CCEDC800C648AD /* GovV2SubsquareOperationFactory.swift in Sources */, @@ -22773,7 +22777,7 @@ C644308270C29AC6F90CFEA6 /* ReferendumDetailsWireframe.swift in Sources */, 7D2906130F25492872637EFC /* ReferendumDetailsPresenter.swift in Sources */, 5E3B1E6B9E94848B186FD4D1 /* ReferendumDetailsInteractor.swift in Sources */, - 77740BC02AD4A80D00E8C06F /* SwapRateView.swift in Sources */, + 77740BC02AD4A80D00E8C06F /* SwapInfoView.swift in Sources */, 488E4467895040EA85FDCC79 /* ReferendumDetailsViewController.swift in Sources */, 845B811D28F44A700040CE84 /* ReferendumActionLocal.swift in Sources */, 77E255692A16148000B644C3 /* StakingRewardsFilter.swift in Sources */, diff --git a/novawallet/Common/View/StackTable/StackInfoTableCell.swift b/novawallet/Common/View/StackTable/StackInfoTableCell.swift index 5f13b3f8f4..600a3aab4e 100644 --- a/novawallet/Common/View/StackTable/StackInfoTableCell.swift +++ b/novawallet/Common/View/StackTable/StackInfoTableCell.swift @@ -15,6 +15,12 @@ class StackInfoTableCell: RowView, SkeletonableView { +final class SwapInfoView: GenericTitleValueView, SkeletonableView { var titleButton: RoundedButton { titleView } var valueLabel: UILabel { valueView } var skeletonView: SkrullableView? @@ -46,7 +46,7 @@ final class SwapRateView: GenericTitleValueView, Skeleto } } -extension SwapRateView { +extension SwapInfoView { func bind(loadableViewModel: LoadableViewModelState) { switch loadableViewModel { case let .cached(value), let .loaded(value): @@ -60,7 +60,7 @@ extension SwapRateView { } } -extension SwapRateView { +extension SwapInfoView { func createSkeletons(for spaceSize: CGSize) -> [Skeletonable] { let size = CGSize(width: 68, height: 8) let offset = CGPoint( diff --git a/novawallet/Modules/Swaps/Base/View/SwapRateViewCell.swift b/novawallet/Modules/Swaps/Base/View/SwapInfoViewCell.swift similarity index 82% rename from novawallet/Modules/Swaps/Base/View/SwapRateViewCell.swift rename to novawallet/Modules/Swaps/Base/View/SwapInfoViewCell.swift index 7cef873251..9fe270ce4e 100644 --- a/novawallet/Modules/Swaps/Base/View/SwapRateViewCell.swift +++ b/novawallet/Modules/Swaps/Base/View/SwapInfoViewCell.swift @@ -1,6 +1,6 @@ import SoraUI -final class SwapRateViewCell: RowView, StackTableViewCellProtocol { +final class SwapInfoViewCell: RowView, StackTableViewCellProtocol { var titleButton: RoundedButton { rowContentView.titleView } var valueLabel: UILabel { rowContentView.valueView } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index d9eb4b32ce..110b66eb0a 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -178,6 +178,56 @@ extension SwapConfirmPresenter: SwapConfirmPresenterProtocol { estimateFee() updateViews() } + + func showRateInfo() { + wireframe.showRateInfo(from: view) + } + + func showPriceDifferenceInfo() { + let title = LocalizableResource { + R.string.localizable.swapsSetupPriceDifference( + preferredLanguages: $0.rLanguages + ) + } + let details = LocalizableResource { + R.string.localizable.swapsSetupPriceDifferenceDescription( + preferredLanguages: $0.rLanguages + ) + } + wireframe.showInfo( + from: view, + title: title, + details: details + ) + } + + func showSlippageInfo() { + wireframe.showSlippageInfo(from: view) + } + + func showNetworkFeeInfo() { + wireframe.showFeeInfo(from: view) + } + + func showAddressOptions() { + guard let view = view else { + return + } + guard let address = chainAccountResponse.chainAccount.toAddress() else { + return + } + + wireframe.presentAccountOptions( + from: view, + address: address, + chain: chainAssetIn.chain, + locale: selectedLocale + ) + } + + func confirm() { + // TODO: Validations + submit + } } extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { @@ -216,7 +266,7 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { estimateFee() } - func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId: AccountId) { + func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId _: AccountId) { balances[chainAsset] = balance } } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift index 52c1f2f978..2b2a348379 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift @@ -6,14 +6,22 @@ protocol SwapConfirmViewProtocol: ControllerBackedProtocol { func didReceiveSlippage(viewModel: String) func didReceiveNetworkFee(viewModel: LoadableViewModelState) func didReceiveWallet(viewModel: WalletAccountViewModel?) + func didReceiveWarning(viewModel: String?) } protocol SwapConfirmPresenterProtocol: AnyObject { func setup() + func showRateInfo() + func showPriceDifferenceInfo() + func showSlippageInfo() + func showNetworkFeeInfo() + func showAddressOptions() + func confirm() } protocol SwapConfirmInteractorInputProtocol: SwapBaseInteractorInputProtocol {} protocol SwapConfirmInteractorOutputProtocol: SwapBaseInteractorOutputProtocol {} -protocol SwapConfirmWireframeProtocol: AnyObject {} +protocol SwapConfirmWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, AddressOptionsPresentable, + ErrorPresentable, SwapErrorPresentable, ShortTextInfoPresentable {} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift index 07dc4f763e..64c29d396d 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift @@ -37,7 +37,33 @@ final class SwapConfirmViewController: UIViewController, ViewHolder { rootView.setup(locale: selectedLocale) } - private func setupHandlers() {} + private func setupHandlers() { + rootView.rateCell.addTarget(self, action: #selector(rateAction), for: .touchUpInside) + rootView.priceDifferenceCell.addTarget(self, action: #selector(priceDifferenceAction), for: .touchUpInside) + rootView.slippageCell.addTarget(self, action: #selector(slippageAction), for: .touchUpInside) + rootView.networkFeeCell.addTarget(self, action: #selector(networkFeeAction), for: .touchUpInside) + rootView.accountCell.addTarget(self, action: #selector(addressAction), for: .touchUpInside) + } + + @objc private func rateAction() { + presenter.showRateInfo() + } + + @objc private func priceDifferenceAction() { + presenter.showPriceDifferenceInfo() + } + + @objc private func slippageAction() { + presenter.showSlippageInfo() + } + + @objc private func networkFeeAction() { + presenter.showNetworkFeeInfo() + } + + @objc private func addressAction() { + presenter.showAddressOptions() + } } extension SwapConfirmViewController: SwapConfirmViewProtocol { @@ -86,6 +112,10 @@ extension SwapConfirmViewController: SwapConfirmViewProtocol { imageViewModel: viewModel.addressIcon )) } + + func didReceiveWarning(viewModel: String?) { + rootView.set(warning: viewModel) + } } extension SwapConfirmViewController: Localizable { diff --git a/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift b/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift index 7e5624bf70..7d7f11eff0 100644 --- a/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift +++ b/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift @@ -10,19 +10,19 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { $0.contentInsets = UIEdgeInsets(top: 0, left: 16, bottom: 8, right: 16) } - let rateCell: SwapRateViewCell = .create { + let rateCell: SwapInfoViewCell = .create { $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote $0.titleButton.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() } - let priceDifferenceCell: SwapRateViewCell = .create { + let priceDifferenceCell: SwapInfoViewCell = .create { $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote $0.titleButton.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() } - let slippageCell: SwapRateViewCell = .create { + let slippageCell: SwapInfoViewCell = .create { $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote $0.titleButton.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() @@ -40,8 +40,11 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { let accountCell: StackInfoTableCell = .create { $0.detailsLabel.lineBreakMode = .byTruncatingMiddle + $0.infoIcon = R.image.iconInfoFilledAccent() } + private var warningView: InlineAlertView? + let actionButton: TriangularedButton = .create { $0.applyDefaultStyle() } @@ -95,4 +98,13 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { actionButton.imageWithTitleView?.title = R.string.localizable.commonConfirm( preferredLanguages: locale.rLanguages) } + + func set(warning: String?) { + applyWarning( + on: &warningView, + after: walletTableView, + text: warning, + spacing: 8 + ) + } } diff --git a/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift b/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift index 1cc1104c0f..a475ee8eb6 100644 --- a/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift +++ b/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift @@ -123,7 +123,7 @@ extension SwapElementView { } } -extension SwapRateViewCell { +extension SwapInfoViewCell { func bind(attention: AttentionState) { switch attention { case .high: diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 79d0f8314f..1da8960ed1 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -441,39 +441,11 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { func showFeeActions() {} func showFeeInfo() { - let title = LocalizableResource { - R.string.localizable.commonNetwork( - preferredLanguages: $0.rLanguages - ) - } - let details = LocalizableResource { - R.string.localizable.swapsNetworkFeeDescription( - preferredLanguages: $0.rLanguages - ) - } - wireframe.showInfo( - from: view, - title: title, - details: details - ) + wireframe.showFeeInfo(from: view) } func showRateInfo() { - let title = LocalizableResource { - R.string.localizable.swapsSetupDetailsRate( - preferredLanguages: $0.rLanguages - ) - } - let details = LocalizableResource { - R.string.localizable.swapsRateDescription( - preferredLanguages: $0.rLanguages - ) - } - wireframe.showInfo( - from: view, - title: title, - details: details - ) + wireframe.showRateInfo(from: view) } func proceed() { diff --git a/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift b/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift index d557fe67e7..022f77812d 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift @@ -1,7 +1,7 @@ import UIKit final class SwapDetailsView: CollapsableContainerView { - let rateCell: SwapRateView = .create { + let rateCell: SwapInfoView = .create { $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote $0.titleView.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index 91632c2387..084dc75683 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -27,7 +27,7 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { $0.setExpanded(false, animated: false) } - var rateCell: SwapRateView { + var rateCell: SwapInfoView { detailsView.rateCell } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index 726a886c84..443143ae43 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -137,17 +137,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { } func showSlippageInfo() { - let title = LocalizableResource { - R.string.localizable.swapsSetupSlippage(preferredLanguages: $0.rLanguages) - } - let details = LocalizableResource { - R.string.localizable.swapsSetupSlippageDescription(preferredLanguages: $0.rLanguages) - } - wireframe.showInfo( - from: view, - title: title, - details: details - ) + wireframe.showSlippageInfo(from: view) } func reset() { diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 0d25d2a275..a45181de89 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1406,3 +1406,4 @@ "common.alert.external.link.disclaimer.message" = "To continue the purchase you will be redirected from Nova Wallet app to %@"; "polkadot.staking.promotion.title" = "Boost your DOT 🚀"; "polkadot.staking.promotion.message" = "Received your DOT back from crowdloans? Start staking your DOT today to get the maximum possible rewards!"; +"swaps.setup.price.difference.description" = "Price difference refers to the difference in price between two different assets. When making a swap in crypto, the price difference is usually the difference between the price of the asset you are swapping for and the price of the asset you are swapping with."; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 23bdf47671..2ddc5ddd94 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1406,3 +1406,4 @@ "common.alert.external.link.disclaimer.message" = "Для продолжения покупки вы будете перенаправлены из приложения Nova Wallet на сайт %@"; "polkadot.staking.promotion.title" = "Максимизируйте\nнаграды от DOT 🚀"; "polkadot.staking.promotion.message" = "Получили свои DOT из краудлоунов? Начните стейкать DOT уже сегодня, чтобы получить максимальные вознаграждения!"; +"swaps.setup.price.difference.description" = "Разница в цене относится к разнице в цене между двумя различными активами. При совершении обмена в криптовалюте разница в цене обычно представляет собой разницу между ценой актива, на который вы меняете, и ценой актива, на который вы меняетесь."; From bf2c05e5e99ea0d0a77a974557bd3427db4e9f07 Mon Sep 17 00:00:00 2001 From: ERussel Date: Thu, 26 Oct 2023 14:39:27 +0200 Subject: [PATCH 074/204] bump version to v16 --- novawallet/Common/Configs/ApplicationConfigs.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novawallet/Common/Configs/ApplicationConfigs.swift b/novawallet/Common/Configs/ApplicationConfigs.swift index 3e3b6bdb71..123d475664 100644 --- a/novawallet/Common/Configs/ApplicationConfigs.swift +++ b/novawallet/Common/Configs/ApplicationConfigs.swift @@ -129,9 +129,9 @@ extension ApplicationConfig: ApplicationConfigProtocol { var chainListURL: URL { #if F_RELEASE - URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/chains/v15/chains.json")! + URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/chains/v16/chains.json")! #else - URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/chains/v15/chains_dev.json")! + URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/chains/v16/chains_dev.json")! #endif } From 8f4087d6c953035edb38a0f1b8ca95bbb620deba Mon Sep 17 00:00:00 2001 From: ERussel Date: Thu, 26 Oct 2023 15:02:15 +0200 Subject: [PATCH 075/204] fix slippage availability button --- .../Modules/Swaps/Slippage/SwapSlippageViewController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift index b9115c1176..516931d499 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewController.swift @@ -75,7 +75,9 @@ final class SwapSlippageViewController: UIViewController, ViewHolder { private func updateActionButton() { let inputValid = rootView.amountInput.inputViewModel?.isValid == true - rootView.actionButton.isEnabled = isApplyAvailable && inputValid + + let isEnabled = isApplyAvailable && inputValid + rootView.actionButton.set(enabled: isEnabled, changeStyle: true) } @objc private func applyButtonAction() { From ab6ac724b4137f98cf76628154ce23833fbd9a7e Mon Sep 17 00:00:00 2001 From: ERussel Date: Thu, 26 Oct 2023 15:08:35 +0200 Subject: [PATCH 076/204] fix default slippage --- novawallet.xcodeproj/project.pbxproj | 4 ++++ .../Model/AssetConversionConstants.swift | 5 +++++ .../Modules/Swaps/Setup/SwapSetupPresenter.swift | 2 +- .../Modules/Swaps/Slippage/SwapSlippagePresenter.swift | 10 ++++++---- 4 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 novawallet/Modules/AssetConversion/Model/AssetConversionConstants.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 9ee1de76d8..ca2ce4eea6 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -247,6 +247,7 @@ 0CC2E56A2A6E6EBB004092E7 /* LocalStorageProviderObserving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2E5692A6E6EBB004092E7 /* LocalStorageProviderObserving.swift */; }; 0CC4CCF42A67C9C400F63041 /* Multistaking+NominationPools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC4CCF32A67C9C400F63041 /* Multistaking+NominationPools.swift */; }; 0CC6C8D82AAB401200AD8D9B /* CustomValidatorsFullList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC6C8D72AAB401200AD8D9B /* CustomValidatorsFullList.swift */; }; + 0CC74A3C2AEA9B06005280F5 /* AssetConversionConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC74A3B2AEA9B06005280F5 /* AssetConversionConstants.swift */; }; 0CCA245B2AC6917400AEF23D /* XcmV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA245A2AC6917400AEF23D /* XcmV3.swift */; }; 0CCA245D2AC6918800AEF23D /* XcmV3Junction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA245C2AC6918800AEF23D /* XcmV3Junction.swift */; }; 0CCA245F2AC6974200AEF23D /* XcmV3Multilocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA245E2AC6974200AEF23D /* XcmV3Multilocation.swift */; }; @@ -4288,6 +4289,7 @@ 0CC41A9A168AF0F1FBE5F799 /* Pods_novawalletAll_novawallet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_novawalletAll_novawallet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0CC4CCF32A67C9C400F63041 /* Multistaking+NominationPools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Multistaking+NominationPools.swift"; sourceTree = ""; }; 0CC6C8D72AAB401200AD8D9B /* CustomValidatorsFullList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomValidatorsFullList.swift; sourceTree = ""; }; + 0CC74A3B2AEA9B06005280F5 /* AssetConversionConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionConstants.swift; sourceTree = ""; }; 0CCA245A2AC6917400AEF23D /* XcmV3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmV3.swift; sourceTree = ""; }; 0CCA245C2AC6918800AEF23D /* XcmV3Junction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmV3Junction.swift; sourceTree = ""; }; 0CCA245E2AC6974200AEF23D /* XcmV3Multilocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmV3Multilocation.swift; sourceTree = ""; }; @@ -8289,6 +8291,7 @@ isa = PBXGroup; children = ( 0C0CB37E2AC540B200EAC516 /* AssetConversion.swift */, + 0CC74A3B2AEA9B06005280F5 /* AssetConversionConstants.swift */, ); path = Model; sourceTree = ""; @@ -21574,6 +21577,7 @@ 84D6D7FA27A7EC810094FC33 /* AssetListAssetCell.swift in Sources */, 61E0DC83C1D60D677274D7CE /* AccountExportPasswordViewFactory.swift in Sources */, 84EBFCF4285E90F80006327E /* XcmWeightMessages.swift in Sources */, + 0CC74A3C2AEA9B06005280F5 /* AssetConversionConstants.swift in Sources */, 843F9ABC29DDAF8F004F1737 /* JSONRPCTimeout.swift in Sources */, 841E5540282D524800C8438F /* ParastakingLocalStorageSubscriber.swift in Sources */, 84350AD228457CC30031EF24 /* BaseValidatorInfoViewModelFactory.swift in Sources */, diff --git a/novawallet/Modules/AssetConversion/Model/AssetConversionConstants.swift b/novawallet/Modules/AssetConversion/Model/AssetConversionConstants.swift new file mode 100644 index 0000000000..db5647a14b --- /dev/null +++ b/novawallet/Modules/AssetConversion/Model/AssetConversionConstants.swift @@ -0,0 +1,5 @@ +import Foundation + +enum AssetConversionConstants { + static let defaultSlippage: Decimal = 0.5 +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 126454c0fb..739ff16b01 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -376,7 +376,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideButtonState() provideSettingsState() // TODO: get from settings - slippage = .fraction(from: 0.5)?.fromPercents() + slippage = .fraction(from: AssetConversionConstants.defaultSlippage)?.fromPercents() interactor.setup() } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index 5d92692615..f84a2cdc3c 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -63,9 +63,11 @@ final class SwapSlippagePresenter { } private func provideButtonStates() { - let amountChanged = amountInput != initialPercent() - view?.didReceiveResetState(available: amountChanged) - view?.didReceiveButtonState(available: amountChanged) + let canReset = amountInput != AssetConversionConstants.defaultSlippage + view?.didReceiveResetState(available: canReset) + + let canApply = amountInput != initialPercent() + view?.didReceiveButtonState(available: canApply) } private func provideErrors() { @@ -153,7 +155,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { } func reset() { - amountInput = initialPercent() + amountInput = AssetConversionConstants.defaultSlippage provideAmountViewModel() provideButtonStates() provideErrors() From ce9dc57d871d66171802246fabaab280142ba1e8 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 27 Oct 2023 11:04:11 +0300 Subject: [PATCH 077/204] refactoring --- novawallet.xcodeproj/project.pbxproj | 4 + .../View/AssetListTotalBalanceCell.swift | 4 +- .../Swaps/Base/SwapBaseInteractor.swift | 16 +- .../Swaps/Base/SwapBaseProtocols.swift | 2 +- .../Confirm/Model/SwapConfirmInitState.swift | 8 + .../Swaps/Confirm/SwapConfirmInteractor.swift | 87 ++++++-- .../Swaps/Confirm/SwapConfirmPresenter.swift | 200 +++++++++++++----- .../Swaps/Confirm/SwapConfirmProtocols.swift | 19 +- .../Confirm/SwapConfirmViewController.swift | 8 + .../Confirm/SwapConfirmViewFactory.swift | 71 +++---- .../Swaps/Confirm/SwapConfirmWireframe.swift | 13 +- .../Swaps/Setup/SwapSetupInteractor.swift | 2 - .../Setup/SwapSetupPresenter+Validating.swift | 2 +- .../Swaps/Setup/SwapSetupPresenter.swift | 16 +- .../Swaps/Setup/SwapSetupProtocols.swift | 7 +- .../Swaps/Setup/SwapSetupViewController.swift | 2 +- .../Swaps/Setup/SwapSetupWireframe.swift | 14 +- novawallet/en.lproj/Localizable.strings | 2 +- novawallet/ru.lproj/Localizable.strings | 2 +- 19 files changed, 327 insertions(+), 152 deletions(-) create mode 100644 novawallet/Modules/Swaps/Confirm/Model/SwapConfirmInitState.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 7c7db14ffc..36d48b2f72 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -813,6 +813,7 @@ 77E0DC9E2A6940C400D03724 /* Calendar+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E0DC9D2A6940C400D03724 /* Calendar+Helpers.swift */; }; 77E255672A16145500B644C3 /* StakingRewardsFilterMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E255662A16145500B644C3 /* StakingRewardsFilterMapper.swift */; }; 77E255692A16148000B644C3 /* StakingRewardsFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E255682A16148000B644C3 /* StakingRewardsFilter.swift */; }; + 77E304A92AEB9F76006FD6F0 /* SwapConfirmInitState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304A82AEB9F76006FD6F0 /* SwapConfirmInitState.swift */; }; 77EA2A232A333C1500B0670B /* french_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A182A333C1500B0670B /* french_output.json */; }; 77EA2A242A333C1500B0670B /* arrays_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A192A333C1500B0670B /* arrays_output.json */; }; 77EA2A252A333C1500B0670B /* weird_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A1A2A333C1500B0670B /* weird_output.json */; }; @@ -4855,6 +4856,7 @@ 77E255652A16059A00B644C3 /* MultiassetUserDataModel9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel9.xcdatamodel; sourceTree = ""; }; 77E255662A16145500B644C3 /* StakingRewardsFilterMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardsFilterMapper.swift; sourceTree = ""; }; 77E255682A16148000B644C3 /* StakingRewardsFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardsFilter.swift; sourceTree = ""; }; + 77E304A82AEB9F76006FD6F0 /* SwapConfirmInitState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConfirmInitState.swift; sourceTree = ""; }; 77EA2A182A333C1500B0670B /* french_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = french_output.json; sourceTree = ""; }; 77EA2A192A333C1500B0670B /* arrays_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = arrays_output.json; sourceTree = ""; }; 77EA2A1A2A333C1500B0670B /* weird_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = weird_output.json; sourceTree = ""; }; @@ -9652,6 +9654,7 @@ children = ( 771901AA2AE9581400D9C918 /* SwapConfirmViewModelFactory.swift */, 771901AF2AE97DA500D9C918 /* SwapConfirmViewModels.swift */, + 77E304A82AEB9F76006FD6F0 /* SwapConfirmInitState.swift */, ); path = Model; sourceTree = ""; @@ -20998,6 +21001,7 @@ 0C17BD9B2A43025E004AF9E7 /* Pagination.swift in Sources */, 88D02FE82942EB1A00E26390 /* AssetDetailsModel.swift in Sources */, 77F033A22A84E00F006BC67E /* StakingPoolView.swift in Sources */, + 77E304A92AEB9F76006FD6F0 /* SwapConfirmInitState.swift in Sources */, 88421055289BBA8D00306F2C /* CurrencyViewLayout.swift in Sources */, 845C407D2702812E00BFA50B /* StakingAccountUpdatingService.swift in Sources */, 8430D6C92801A2B500FFB6AE /* WebSocketProtocols.swift in Sources */, diff --git a/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift b/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift index 93546d03ab..bc2d61b632 100644 --- a/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift +++ b/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift @@ -52,7 +52,7 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { icon: R.image.iconReceive() ) lazy var swapButton = createActionButton( - title: R.string.localizable.walletAssetsSwap( + title: R.string.localizable.commonSwap( preferredLanguages: locale.rLanguages ), icon: R.image.iconActionChange() @@ -213,7 +213,7 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { buyButton.imageWithTitleView?.title = R.string.localizable.walletAssetBuy( preferredLanguages: locale.rLanguages ) - swapButton.imageWithTitleView?.title = R.string.localizable.walletAssetsSwap( + swapButton.imageWithTitleView?.title = R.string.localizable.commonSwap( preferredLanguages: locale.rLanguages ) } diff --git a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift index 0e21d91a32..c66bed9295 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift @@ -17,10 +17,10 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB private let operationQueue: OperationQueue private var quoteCall: CancellableCall? private var runtimeOperationCall: CancellableCall? - private var extrinsicService: ExtrinsicServiceProtocol? + private(set) var extrinsicService: ExtrinsicServiceProtocol? - private var priceProviders: [ChainAssetId: StreamableProvider] = [:] - private var assetBalanceProviders: [ChainAssetId: StreamableProvider] = [:] + private(set) var priceProviders: [ChainAssetId: StreamableProvider] = [:] + private(set) var assetBalanceProviders: [ChainAssetId: StreamableProvider] = [:] init( assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, @@ -101,7 +101,7 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB self?.basePresenter?.didReceive(quote: result, for: args) } catch { - self?.basePresenter?.didReceive(error: .quote(error, args)) + self?.basePresenter?.didReceive(baseError: .quote(error, args)) } } } @@ -135,7 +135,7 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB ) } catch { DispatchQueue.main.async { - self.basePresenter?.didReceive(error: .fetchFeeFailed(error, args.identifier)) + self.basePresenter?.didReceive(baseError: .fetchFeeFailed(error, args.identifier)) } } } @@ -203,7 +203,7 @@ extension SwapBaseInteractor: ExtrinsicFeeProxyDelegate { let fee = BigUInt(dispatchInfo.fee) self.basePresenter?.didReceive(fee: fee, transactionId: transactionId) case let .failure(error): - self.basePresenter?.didReceive(error: .fetchFeeFailed(error, transactionId)) + self.basePresenter?.didReceive(baseError: .fetchFeeFailed(error, transactionId)) } } } @@ -215,7 +215,7 @@ extension SwapBaseInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptio case let .success(priceData): basePresenter?.didReceive(price: priceData, priceId: priceId) case let .failure(error): - basePresenter?.didReceive(error: .price(error, priceId)) + basePresenter?.didReceive(baseError: .price(error, priceId)) } } } @@ -240,7 +240,7 @@ extension SwapBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscript accountId: accountId ) case let .failure(error): - basePresenter?.didReceive(error: .assetBalance(error, chainAssetId, accountId)) + basePresenter?.didReceive(baseError: .assetBalance(error, chainAssetId, accountId)) } } } diff --git a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift index ee3b80a4c3..284dc52da6 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift @@ -10,7 +10,7 @@ protocol SwapBaseInteractorInputProtocol: AnyObject { protocol SwapBaseInteractorOutputProtocol: AnyObject { func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) func didReceive(fee: BigUInt?, transactionId: TransactionFeeId) - func didReceive(error: SwapSetupError) + func didReceive(baseError: SwapSetupError) func didReceive(price: PriceData?, priceId: AssetModel.PriceId) func didReceive(payAccountId: AccountId?) func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId: AccountId) diff --git a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmInitState.swift b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmInitState.swift new file mode 100644 index 0000000000..ec4d9d2251 --- /dev/null +++ b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmInitState.swift @@ -0,0 +1,8 @@ +struct SwapConfirmInitState { + let chainAssetIn: ChainAsset + let chainAssetOut: ChainAsset + let feeChainAsset: ChainAsset + let slippage: BigRational + let quote: AssetConversion.Quote + let quoteArgs: AssetConversion.QuoteArgs +} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift index 981960441a..1fcd4e609a 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift @@ -1,22 +1,18 @@ import UIKit +import RobinHood final class SwapConfirmInteractor: SwapBaseInteractor { var presenter: SwapConfirmInteractorOutputProtocol? { basePresenter as? SwapConfirmInteractorOutputProtocol } - let payChainAsset: ChainAsset - let receiveChainAsset: ChainAsset - let feeChainAsset: ChainAsset - let slippage: BigRational - let quote: AssetConversion.Quote + let initState: SwapConfirmInitState + let signer: SigningWrapperProtocol + let operationQueue: OperationQueue + private var submitOperationCall: CancellableCall? init( - payChainAsset: ChainAsset, - receiveChainAsset: ChainAsset, - feeChainAsset: ChainAsset, - slippage: BigRational, - quote: AssetConversion.Quote, + initState: SwapConfirmInitState, assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol, runtimeService: RuntimeProviderProtocol, @@ -26,13 +22,12 @@ final class SwapConfirmInteractor: SwapBaseInteractor { walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, currencyManager: CurrencyManagerProtocol, selectedAccount: MetaAccountModel, - operationQueue: OperationQueue + operationQueue: OperationQueue, + signer: SigningWrapperProtocol ) { - self.payChainAsset = payChainAsset - self.receiveChainAsset = receiveChainAsset - self.feeChainAsset = feeChainAsset - self.slippage = slippage - self.quote = quote + self.initState = initState + self.signer = signer + self.operationQueue = operationQueue super.init( assetConversionOperationFactory: assetConversionOperationFactory, @@ -51,10 +46,62 @@ final class SwapConfirmInteractor: SwapBaseInteractor { override func setup() { super.setup() - set(payChainAsset: payChainAsset) - set(receiveChainAsset: receiveChainAsset) - set(feeChainAsset: feeChainAsset) + set(payChainAsset: initState.chainAssetIn) + set(receiveChainAsset: initState.chainAssetOut) + set(feeChainAsset: initState.feeChainAsset) + } + + func submitExtrinsic(args: AssetConversion.CallArgs) { + clear(cancellable: &submitOperationCall) + guard let extrinsicService = extrinsicService else { + return + } + + let runtimeCoderFactoryOperation = runtimeService.fetchCoderFactoryOperation() + + runtimeCoderFactoryOperation.completionBlock = { [weak self] in + guard let self = self else { + return + } + do { + let runtimeCoderFactory = try runtimeCoderFactoryOperation.extractNoCancellableResultData() + let builder = self.assetConversionExtrinsicService.fetchExtrinsicBuilderClosure( + for: args, + codingFactory: runtimeCoderFactory + ) + self.submitClosure(extrinsicService: extrinsicService, builder: builder) + } catch { + DispatchQueue.main.async { + self.presenter?.didReceive(error: .submit(error)) + } + } + } + + submitOperationCall = runtimeCoderFactoryOperation + operationQueue.addOperation(runtimeCoderFactoryOperation) + } + + private func submitClosure( + extrinsicService: ExtrinsicServiceProtocol, + builder: @escaping ExtrinsicBuilderClosure + ) { + extrinsicService.submit( + builder, + signer: signer, + runningIn: .main + ) { [weak self] result in + switch result { + case let .success(hash): + self?.presenter?.didReceiveConfirmation(hash: hash) + case let .failure(error): + self?.presenter?.didReceive(error: .submit(error)) + } + } } } -extension SwapConfirmInteractor: SwapConfirmInteractorInputProtocol {} +extension SwapConfirmInteractor: SwapConfirmInteractorInputProtocol { + func submit(args: AssetConversion.CallArgs) { + submitExtrinsic(args: args) + } +} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 110b66eb0a..67962e0008 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -6,10 +6,8 @@ final class SwapConfirmPresenter { weak var view: SwapConfirmViewProtocol? let wireframe: SwapConfirmWireframeProtocol let interactor: SwapConfirmInteractorInputProtocol - let chainAssetIn: ChainAsset - let chainAssetOut: ChainAsset - let slippage: BigRational - let feeChainAsset: ChainAsset + let dataValidatingFactory: SwapDataValidatorFactoryProtocol + let initState: SwapConfirmInitState private var viewModelFactory: SwapConfirmViewModelFactoryProtocol private var feePriceData: PriceData? @@ -19,68 +17,60 @@ final class SwapConfirmPresenter { private var fee: BigUInt? private var payAccountId: AccountId? private var chainAccountResponse: MetaChainAccountResponse - private var quoteArgs: AssetConversion.QuoteArgs? private var balances: [ChainAssetId: AssetBalance?] = [:] init( interactor: SwapConfirmInteractorInputProtocol, wireframe: SwapConfirmWireframeProtocol, viewModelFactory: SwapConfirmViewModelFactoryProtocol, - chainAssetIn: ChainAsset, - chainAssetOut: ChainAsset, - feeChainAsset: ChainAsset, - quote: AssetConversion.Quote, - quoteArgs: AssetConversion.QuoteArgs, - slippage: BigRational, - chainAccountResponse: MetaChainAccountResponse + chainAccountResponse: MetaChainAccountResponse, + localizationManager: LocalizationManagerProtocol, + dataValidatingFactory: SwapDataValidatorFactoryProtocol, + initState: SwapConfirmInitState ) { self.interactor = interactor self.wireframe = wireframe self.viewModelFactory = viewModelFactory - self.chainAssetIn = chainAssetIn - self.chainAssetOut = chainAssetOut - self.feeChainAsset = feeChainAsset - self.quote = quote - self.slippage = slippage + self.initState = initState + quote = initState.quote self.chainAccountResponse = chainAccountResponse - self.quoteArgs = quoteArgs - - localizationManager = localizationManager + self.dataValidatingFactory = dataValidatingFactory + self.localizationManager = localizationManager } - func provideAssetInViewModel() { + private func provideAssetInViewModel() { guard let quote = quote else { return } let viewModel = viewModelFactory.assetViewModel( - chainAsset: chainAssetIn, + chainAsset: initState.chainAssetIn, amount: quote.amountIn, priceData: chainAssetInPriceData ) view?.didReceiveAssetIn(viewModel: viewModel) } - func provideAssetOutViewModel() { + private func provideAssetOutViewModel() { guard let quote = quote else { return } let viewModel = viewModelFactory.assetViewModel( - chainAsset: chainAssetOut, + chainAsset: initState.chainAssetOut, amount: quote.amountOut, priceData: chainAssetOutPriceData ) view?.didReceiveAssetOut(viewModel: viewModel) } - func provideRateViewModel() { + private func provideRateViewModel() { guard let quote = quote else { view?.didReceiveRate(viewModel: .loading) return } let params = RateParams( - assetDisplayInfoIn: chainAssetIn.assetDisplayInfo, - assetDisplayInfoOut: chainAssetOut.assetDisplayInfo, + assetDisplayInfoIn: initState.chainAssetIn.assetDisplayInfo, + assetDisplayInfoOut: initState.chainAssetOut.assetDisplayInfo, amountIn: quote.amountIn, amountOut: quote.amountOut ) @@ -89,15 +79,15 @@ final class SwapConfirmPresenter { view?.didReceiveRate(viewModel: .loaded(value: viewModel)) } - func providePriceDifferenceViewModel() { + private func providePriceDifferenceViewModel() { guard let quote = quote else { view?.didReceivePriceDifference(viewModel: .loading) return } let params = RateParams( - assetDisplayInfoIn: chainAssetIn.assetDisplayInfo, - assetDisplayInfoOut: chainAssetOut.assetDisplayInfo, + assetDisplayInfoIn: initState.chainAssetIn.assetDisplayInfo, + assetDisplayInfoOut: initState.chainAssetOut.assetDisplayInfo, amountIn: quote.amountIn, amountOut: quote.amountOut ) @@ -113,26 +103,26 @@ final class SwapConfirmPresenter { } } - func provideSlippageViewModel() { - let viewModel = viewModelFactory.slippageViewModel(slippage: slippage) + private func provideSlippageViewModel() { + let viewModel = viewModelFactory.slippageViewModel(slippage: initState.slippage) view?.didReceiveSlippage(viewModel: viewModel) } - func provideFeeViewModel() { + private func provideFeeViewModel() { guard let fee = fee else { view?.didReceiveNetworkFee(viewModel: .loading) return } let viewModel = viewModelFactory.feeViewModel( fee: fee, - chainAsset: feeChainAsset, + chainAsset: initState.feeChainAsset, priceData: feePriceData ) view?.didReceiveNetworkFee(viewModel: .loaded(value: viewModel)) } - func provideWalletViewModel() { + private func provideWalletViewModel() { guard let walletAddress = WalletDisplayAddress(response: chainAccountResponse) else { view?.didReceiveWallet(viewModel: nil) return @@ -142,7 +132,7 @@ final class SwapConfirmPresenter { view?.didReceiveWallet(viewModel: viewModel) } - func updateViews() { + private func updateViews() { provideAssetInViewModel() provideAssetOutViewModel() provideRateViewModel() @@ -152,10 +142,12 @@ final class SwapConfirmPresenter { provideWalletViewModel() } - func estimateFee() { - guard let quote = quote, let quoteArgs = quoteArgs, let accountId = payAccountId else { + private func estimateFee() { + guard let quote = quote, + let accountId = payAccountId else { return } + fee = nil provideFeeViewModel() @@ -165,10 +157,72 @@ final class SwapConfirmPresenter { assetOut: quote.assetOut, amountOut: quote.amountOut, receiver: accountId, - direction: quoteArgs.direction, - slippage: slippage + direction: initState.quoteArgs.direction, + slippage: initState.slippage + ) ) + } + + func validators( + spendingAmount: Decimal? + ) -> [DataValidating] { + let feeDecimal = fee.map { Decimal.fromSubstrateAmount( + $0, + precision: Int16(initState.feeChainAsset.asset.precision) + ) } ?? nil + + let payAssetBalance = balances[initState.chainAssetIn.chainAssetId] + + let validators: [DataValidating] = [ + dataValidatingFactory.has(fee: feeDecimal, locale: selectedLocale) { [weak self] in + self?.estimateFee() + }, + dataValidatingFactory.canSpendAmountInPlank( + balance: payAssetBalance??.transferable, + spendingAmount: spendingAmount, + asset: initState.chainAssetIn.assetDisplayInfo, + locale: selectedLocale + ), + dataValidatingFactory.canPayFeeSpendingAmountInPlank( + balance: payAssetBalance??.transferable, + fee: initState.chainAssetIn.chainAssetId == initState.feeChainAsset.chainAssetId ? fee : nil, + spendingAmount: spendingAmount, + asset: initState.feeChainAsset.assetDisplayInfo, + locale: selectedLocale + ), + dataValidatingFactory.has( + quote: quote, + payChainAssetId: initState.chainAssetIn.chainAssetId, + receiveChainAssetId: initState.chainAssetOut.chainAssetId, + locale: selectedLocale, + onError: { [weak self] in + guard let self = self else { + return + } + self.interactor.calculateQuote(for: self.initState.quoteArgs) + } + ) + ] + + return validators + } + + private func submit() { + guard let quote = quote, + let accountId = payAccountId else { + return + } + let args = AssetConversion.CallArgs( + assetIn: quote.assetIn, + amountIn: quote.amountIn, + assetOut: quote.assetOut, + amountOut: quote.amountOut, + receiver: accountId, + direction: initState.quoteArgs.direction, + slippage: initState.slippage ) + + interactor.submit(args: args) } } @@ -220,20 +274,25 @@ extension SwapConfirmPresenter: SwapConfirmPresenterProtocol { wireframe.presentAccountOptions( from: view, address: address, - chain: chainAssetIn.chain, + chain: initState.chainAssetIn.chain, locale: selectedLocale ) } func confirm() { - // TODO: Validations + submit + let spendingAmount = quote?.amountIn.decimal(precision: initState.chainAssetIn.asset.precision) + + let validators = validators(spendingAmount: spendingAmount) + + DataValidationRunner(validators: validators).runValidation { [weak self] in + self?.submit() + } } } extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { - func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) { + func didReceive(quote: AssetConversion.Quote, for _: AssetConversion.QuoteArgs) { self.quote = quote - self.quoteArgs = quoteArgs provideAssetInViewModel() provideAssetOutViewModel() @@ -247,17 +306,20 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { provideFeeViewModel() } - func didReceive(error _: SwapSetupError) {} - func didReceive(price: PriceData?, priceId: AssetModel.PriceId) { - if priceId == chainAssetIn.asset.priceId { + if priceId == initState.chainAssetIn.asset.priceId { chainAssetInPriceData = price + provideAssetInViewModel() + providePriceDifferenceViewModel() } - if priceId == chainAssetOut.asset.priceId { + if priceId == initState.chainAssetOut.asset.priceId { chainAssetOutPriceData = price + provideAssetOutViewModel() + providePriceDifferenceViewModel() } - if priceId == feeChainAsset.asset.priceId { + if priceId == initState.feeChainAsset.asset.priceId { feePriceData = price + provideFeeViewModel() } } @@ -269,6 +331,46 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId _: AccountId) { balances[chainAsset] = balance } + + func didReceive(baseError: SwapSetupError) { + switch baseError { + case let .quote: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self, initState] in + self?.interactor.calculateQuote(for: initState.quoteArgs) + } + case let .fetchFeeFailed: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.estimateFee() + } + case let .price(_, priceId): + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + guard let self = self else { + return + } + [self.initState.chainAssetIn, self.initState.chainAssetOut, self.initState.feeChainAsset] + .compactMap { $0 } + .filter { $0.asset.priceId == priceId } + .forEach(self.interactor.remakePriceSubscription) + } + case let .assetBalance: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.setup() + } + } + } + + func didReceive(error: SwapConfirmError) { + switch error { + case let .submit(error): + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.submit() + } + } + } + + func didReceiveConfirmation(hash _: String) { + wireframe.complete(on: view, locale: selectedLocale) + } } extension SwapConfirmPresenter: Localizable { diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift index 2b2a348379..77a90c9adf 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift @@ -1,3 +1,5 @@ +import Foundation + protocol SwapConfirmViewProtocol: ControllerBackedProtocol { func didReceiveAssetIn(viewModel: SwapAssetAmountViewModel) func didReceiveAssetOut(viewModel: SwapAssetAmountViewModel) @@ -19,9 +21,20 @@ protocol SwapConfirmPresenterProtocol: AnyObject { func confirm() } -protocol SwapConfirmInteractorInputProtocol: SwapBaseInteractorInputProtocol {} +protocol SwapConfirmInteractorInputProtocol: SwapBaseInteractorInputProtocol { + func submit(args: AssetConversion.CallArgs) +} -protocol SwapConfirmInteractorOutputProtocol: SwapBaseInteractorOutputProtocol {} +protocol SwapConfirmInteractorOutputProtocol: SwapBaseInteractorOutputProtocol { + func didReceiveConfirmation(hash: String) + func didReceive(error: SwapConfirmError) +} protocol SwapConfirmWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, AddressOptionsPresentable, - ErrorPresentable, SwapErrorPresentable, ShortTextInfoPresentable {} + ErrorPresentable, SwapErrorPresentable, ShortTextInfoPresentable, ModalAlertPresenting { + func complete(on view: ControllerBackedProtocol?, locale: Locale) +} + +enum SwapConfirmError { + case submit(Error) +} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift index 64c29d396d..e120230f6d 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift @@ -35,6 +35,9 @@ final class SwapConfirmViewController: UIViewController, ViewHolder { private func setupLocalization() { rootView.setup(locale: selectedLocale) + title = R.string.localizable.commonSwap( + preferredLanguages: selectedLocale.rLanguages + ) } private func setupHandlers() { @@ -43,6 +46,7 @@ final class SwapConfirmViewController: UIViewController, ViewHolder { rootView.slippageCell.addTarget(self, action: #selector(slippageAction), for: .touchUpInside) rootView.networkFeeCell.addTarget(self, action: #selector(networkFeeAction), for: .touchUpInside) rootView.accountCell.addTarget(self, action: #selector(addressAction), for: .touchUpInside) + rootView.actionButton.addTarget(self, action: #selector(confirmAction), for: .touchUpInside) } @objc private func rateAction() { @@ -64,6 +68,10 @@ final class SwapConfirmViewController: UIViewController, ViewHolder { @objc private func addressAction() { presenter.showAddressOptions() } + + @objc private func confirmAction() { + presenter.confirm() + } } extension SwapConfirmViewController: SwapConfirmViewProtocol { diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index b378561e20..5506ceb8aa 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -4,26 +4,18 @@ import RobinHood struct SwapConfirmViewFactory { static func createView( - payChainAsset: ChainAsset, - receiveChainAsset: ChainAsset, - feeChainAsset: ChainAsset, - slippage: BigRational, - quote: AssetConversion.Quote, - quoteArgs: AssetConversion.QuoteArgs + initState: SwapConfirmInitState ) -> SwapConfirmViewProtocol? { - let accountRequest = payChainAsset.chain.accountRequest() + let accountRequest = initState.chainAssetIn.chain.accountRequest() guard let currencyManager = CurrencyManager.shared, - let selectedAccount = SelectedWalletSettings.shared.value, - let chainAccountResponse = selectedAccount.fetchMetaChainAccount(for: accountRequest) else { + let wallet = SelectedWalletSettings.shared.value, + let chainAccountResponse = wallet.fetchMetaChainAccount(for: accountRequest) else { return nil } guard let interactor = createInteractor( - payChainAsset: payChainAsset, - receiveChainAsset: receiveChainAsset, - feeChainAsset: feeChainAsset, - slippage: slippage, - quote: quote + wallet: wallet, + initState: initState ) else { return nil } @@ -40,17 +32,19 @@ struct SwapConfirmViewFactory { percentForamatter: NumberFormatter.percentSingle.localizableResource() ) + let dataValidatingFactory = SwapDataValidatorFactory( + presentable: wireframe, + balanceViewModelFactoryFacade: balanceViewModelFactoryFacade + ) + let presenter = SwapConfirmPresenter( interactor: interactor, wireframe: wireframe, viewModelFactory: viewModelFactory, - chainAssetIn: payChainAsset, - chainAssetOut: receiveChainAsset, - feeChainAsset: feeChainAsset, - quote: quote, - quoteArgs: quoteArgs, - slippage: slippage, - chainAccountResponse: chainAccountResponse + chainAccountResponse: chainAccountResponse, + localizationManager: LocalizationManager.shared, + dataValidatingFactory: dataValidatingFactory, + initState: initState ) let view = SwapConfirmViewController( @@ -59,26 +53,25 @@ struct SwapConfirmViewFactory { ) presenter.view = view + dataValidatingFactory.view = view interactor.basePresenter = presenter return view } private static func createInteractor( - payChainAsset: ChainAsset, - receiveChainAsset: ChainAsset, - feeChainAsset: ChainAsset, - slippage: BigRational, - quote: AssetConversion.Quote + wallet: MetaAccountModel, + initState: SwapConfirmInitState ) -> SwapConfirmInteractor? { - let westmintChainId = KnowChainId.westmint + let chainId = initState.chainAssetIn.chain.chainId let chainRegistry = ChainRegistryFacade.sharedRegistry + let accountRequest = initState.chainAssetIn.chain.accountRequest() - guard let connection = chainRegistry.getConnection(for: westmintChainId), - let runtimeService = chainRegistry.getRuntimeProvider(for: westmintChainId), - let chainModel = chainRegistry.getChain(for: westmintChainId), + guard let connection = chainRegistry.getConnection(for: chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chainId), + let chainModel = chainRegistry.getChain(for: chainId), let currencyManager = CurrencyManager.shared, - let selectedAccount = SelectedWalletSettings.shared.value else { + let chainAccountResponse = wallet.fetchMetaChainAccount(for: accountRequest) else { return nil } @@ -96,12 +89,13 @@ struct SwapConfirmViewFactory { operationManager: OperationManager(operationQueue: operationQueue) ) + let signingWrapper = SigningWrapperFactory().createSigningWrapper( + for: chainAccountResponse.metaId, + accountResponse: chainAccountResponse.chainAccount + ) + let interactor = SwapConfirmInteractor( - payChainAsset: payChainAsset, - receiveChainAsset: receiveChainAsset, - feeChainAsset: feeChainAsset, - slippage: slippage, - quote: quote, + initState: initState, assetConversionOperationFactory: assetConversionOperationFactory, assetConversionExtrinsicService: AssetHubExtrinsicService(chain: chainModel), runtimeService: runtimeService, @@ -110,8 +104,9 @@ struct SwapConfirmViewFactory { priceLocalSubscriptionFactory: PriceProviderFactory.shared, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, currencyManager: currencyManager, - selectedAccount: selectedAccount, - operationQueue: operationQueue + selectedAccount: wallet, + operationQueue: operationQueue, + signer: signingWrapper ) return interactor diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift index 2384b3a875..34a9fb5af5 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift @@ -1,3 +1,14 @@ import Foundation -final class SwapConfirmWireframe: SwapConfirmWireframeProtocol {} +final class SwapConfirmWireframe: SwapConfirmWireframeProtocol { + func complete(on view: ControllerBackedProtocol?, locale: Locale) { + let title = R.string.localizable + .commonTransactionSubmitted(preferredLanguages: locale.rLanguages) + + let presenter = view?.controller.navigationController?.presentingViewController + + presenter?.dismiss(animated: true) { + self.presentSuccessNotification(title, from: presenter, completion: nil) + } + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 84c8ea9506..8bd865e483 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -7,8 +7,6 @@ final class SwapSetupInteractor: SwapBaseInteractor { basePresenter as? SwapSetupInteractorOutputProtocol } - private var assetBalanceProviders: [ChainAssetId: StreamableProvider] = [:] - private var receiveChainAsset: ChainAsset? { didSet { updateSubscriptions(activeChainAssets: activeChainAssets) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift index fa0345d5f3..91f9bf5705 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift @@ -12,7 +12,7 @@ extension SwapSetupPresenter { precision: Int16(feeChainAsset.asset.precision) ) } ?? nil - var validators: [DataValidating] = [ + let validators: [DataValidating] = [ dataValidatingFactory.has(fee: feeDecimal, locale: selectedLocale) { [weak self] in self?.estimateFee() }, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 1da8960ed1..061e256508 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -468,15 +468,19 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { return } - self?.wireframe.showConfirmation( - from: self?.view, - payChainAsset: payChainAsset, - receiveChainAsset: receiveChainAsset, + let confirmInitState = SwapConfirmInitState( + chainAssetIn: payChainAsset, + chainAssetOut: receiveChainAsset, feeChainAsset: feeChainAsset, slippage: slippage, quote: quote, quoteArgs: quoteArgs ) + + self?.wireframe.showConfirmation( + from: self?.view, + initState: confirmInitState + ) } } @@ -499,8 +503,8 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { - func didReceive(error: SwapSetupError) { - switch error { + func didReceive(baseError: SwapSetupError) { + switch baseError { case let .quote(_, args): guard args == quoteArgs else { return diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 6024658b64..442c38ee03 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -66,12 +66,7 @@ protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryabl ) func showConfirmation( from view: ControllerBackedProtocol?, - payChainAsset: ChainAsset, - receiveChainAsset: ChainAsset, - feeChainAsset: ChainAsset, - slippage: BigRational, - quote: AssetConversion.Quote, - quoteArgs: AssetConversion.QuoteArgs + initState: SwapConfirmInitState ) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index e2a26583a5..296d3f129e 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -87,7 +87,7 @@ final class SwapSetupViewController: UIViewController, ViewHolder { } private func setupLocalization() { - title = R.string.localizable.walletAssetsSwap(preferredLanguages: selectedLocale.rLanguages) + title = R.string.localizable.commonSwap(preferredLanguages: selectedLocale.rLanguages) rootView.setup(locale: selectedLocale) setupAccessoryView() } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index 3ef481cbed..9ae705cdfc 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -71,20 +71,10 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { func showConfirmation( from view: ControllerBackedProtocol?, - payChainAsset: ChainAsset, - receiveChainAsset: ChainAsset, - feeChainAsset: ChainAsset, - slippage: BigRational, - quote: AssetConversion.Quote, - quoteArgs: AssetConversion.QuoteArgs + initState: SwapConfirmInitState ) { guard let confimView = SwapConfirmViewFactory.createView( - payChainAsset: payChainAsset, - receiveChainAsset: receiveChainAsset, - feeChainAsset: feeChainAsset, - slippage: slippage, - quote: quote, - quoteArgs: quoteArgs + initState: initState ) else { return } diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index a45181de89..45feb5f05c 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1374,7 +1374,7 @@ "wallet.list.empty.action.title" = "Buy tokens"; "asset.operation.send.empty.state.message" = "You don’t have tokens to send.\nBuy or Receive tokens to your\naccount."; "governance.referendums.status.deciding" = "Deciding"; -"wallet.assets.swap" = "Swap"; +"common.swap" = "Swap"; "swaps.setup.asset.select.subtitle" = "Select a token"; "swaps.setup.asset.pay.title" = "Pay"; "swaps.setup.asset.receive.title" = "Receive"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 2ddc5ddd94..207864f11f 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1374,7 +1374,7 @@ "wallet.list.empty.action.title" = "Купить токены"; "asset.operation.send.empty.state.message" = "У вас нет токенов для отправки.\nКупите или получите токены\nна свой аккаунт."; "governance.referendums.status.deciding" = "Решение"; -"wallet.assets.swap" = "Обмен"; +"common.swap" = "Обмен"; "swaps.setup.asset.select.subtitle" = "Выберите токeн"; "swaps.setup.asset.pay.title" = "Оплата в"; "swaps.setup.asset.receive.title" = "Получение"; From c8f4e21a2c77067199de2c81377d717313bf3a7a Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 27 Oct 2023 12:19:36 +0300 Subject: [PATCH 078/204] add warning --- novawallet.xcodeproj/project.pbxproj | 4 ++ .../Modules/Swaps/Base/SlippageBounds.swift | 48 +++++++++++++++++++ .../Swaps/Confirm/SwapConfirmPresenter.swift | 17 +++++-- .../Swaps/Confirm/SwapConfirmProtocols.swift | 4 +- .../Confirm/SwapConfirmViewController.swift | 10 +++- .../Confirm/View/SwapConfirmViewLayout.swift | 17 +++---- .../Slippage/SwapSlippagePresenter.swift | 41 ++++------------ 7 files changed, 94 insertions(+), 47 deletions(-) create mode 100644 novawallet/Modules/Swaps/Base/SlippageBounds.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 36d48b2f72..f881a86dfe 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -814,6 +814,7 @@ 77E255672A16145500B644C3 /* StakingRewardsFilterMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E255662A16145500B644C3 /* StakingRewardsFilterMapper.swift */; }; 77E255692A16148000B644C3 /* StakingRewardsFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E255682A16148000B644C3 /* StakingRewardsFilter.swift */; }; 77E304A92AEB9F76006FD6F0 /* SwapConfirmInitState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304A82AEB9F76006FD6F0 /* SwapConfirmInitState.swift */; }; + 77E304AB2AEBB214006FD6F0 /* SlippageBounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304AA2AEBB214006FD6F0 /* SlippageBounds.swift */; }; 77EA2A232A333C1500B0670B /* french_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A182A333C1500B0670B /* french_output.json */; }; 77EA2A242A333C1500B0670B /* arrays_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A192A333C1500B0670B /* arrays_output.json */; }; 77EA2A252A333C1500B0670B /* weird_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A1A2A333C1500B0670B /* weird_output.json */; }; @@ -4857,6 +4858,7 @@ 77E255662A16145500B644C3 /* StakingRewardsFilterMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardsFilterMapper.swift; sourceTree = ""; }; 77E255682A16148000B644C3 /* StakingRewardsFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardsFilter.swift; sourceTree = ""; }; 77E304A82AEB9F76006FD6F0 /* SwapConfirmInitState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConfirmInitState.swift; sourceTree = ""; }; + 77E304AA2AEBB214006FD6F0 /* SlippageBounds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlippageBounds.swift; sourceTree = ""; }; 77EA2A182A333C1500B0670B /* french_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = french_output.json; sourceTree = ""; }; 77EA2A192A333C1500B0670B /* arrays_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = arrays_output.json; sourceTree = ""; }; 77EA2A1A2A333C1500B0670B /* weird_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = weird_output.json; sourceTree = ""; }; @@ -9624,6 +9626,7 @@ 771901A12AE7E34D00D9C918 /* SwapBaseInteractor.swift */, 771901A32AE7E48800D9C918 /* SwapBaseProtocols.swift */, 771901B12AEA3E5100D9C918 /* ShortTextInfoPresentableExtensions.swift */, + 77E304AA2AEBB214006FD6F0 /* SlippageBounds.swift */, ); path = Base; sourceTree = ""; @@ -23076,6 +23079,7 @@ CF2F3A0F0999D6D054CD33D2 /* ReferendumSearchProtocols.swift in Sources */, 0CE629D62AA9B5E200E250BD /* BalanceViewModel.swift in Sources */, 0C2F86982A728EE900593C01 /* NPoolsRewardEngineFactory.swift in Sources */, + 77E304AB2AEBB214006FD6F0 /* SlippageBounds.swift in Sources */, C5B07E59C0B00CAD1D0D2DFD /* ReferendumSearchWireframe.swift in Sources */, AB27EE4EE30A06D8E7B8EDB4 /* ReferendumSearchPresenter.swift in Sources */, 2F2ACE609F7423EDD0F06F30 /* ReferendumSearchViewController.swift in Sources */, diff --git a/novawallet/Modules/Swaps/Base/SlippageBounds.swift b/novawallet/Modules/Swaps/Base/SlippageBounds.swift new file mode 100644 index 0000000000..93b59b4af2 --- /dev/null +++ b/novawallet/Modules/Swaps/Base/SlippageBounds.swift @@ -0,0 +1,48 @@ +import Foundation + +struct SlippageBounds { + let restriction: BoundValue = .init(lower: 0.1, upper: 50) + let recommendation: BoundValue = .init(lower: 0.1, upper: 5) + + struct BoundValue { + let lower: Decimal + let upper: Decimal + } +} + +extension SlippageBounds { + func warning(for value: Decimal?, locale: Locale) -> String? { + guard let value = value, value > 0 else { + return nil + } + if value <= recommendation.lower { + let warning = R.string.localizable.swapsSetupSlippageWarningLowAmount( + preferredLanguages: locale.rLanguages + ) + return warning + } else if value >= recommendation.upper { + let warning = R.string.localizable.swapsSetupSlippageWarningHighAmount( + preferredLanguages: locale.rLanguages + ) + return warning + } else { + return nil + } + } + + func error(for value: Decimal?, stringAmountClosure: (Decimal) -> String, locale: Locale) -> String? { + if let value = value, + value < restriction.lower || value > restriction.upper { + let minAmountString = stringAmountClosure(restriction.lower) + let maxAmountString = stringAmountClosure(restriction.upper) + let error = R.string.localizable.swapsSetupSlippageErrorAmountBounds( + minAmountString, + maxAmountString, + preferredLanguages: locale.rLanguages + ) + return error + } else { + return nil + } + } +} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 67962e0008..e5b5e0dabf 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -8,6 +8,7 @@ final class SwapConfirmPresenter { let interactor: SwapConfirmInteractorInputProtocol let dataValidatingFactory: SwapDataValidatorFactoryProtocol let initState: SwapConfirmInitState + let slippageBounds = SlippageBounds() private var viewModelFactory: SwapConfirmViewModelFactoryProtocol private var feePriceData: PriceData? @@ -106,6 +107,11 @@ final class SwapConfirmPresenter { private func provideSlippageViewModel() { let viewModel = viewModelFactory.slippageViewModel(slippage: initState.slippage) view?.didReceiveSlippage(viewModel: viewModel) + let warning = slippageBounds.warning( + for: initState.slippage.toPercents().decimalValue, + locale: selectedLocale + ) + view?.didReceiveWarning(viewModel: warning) } private func provideFeeViewModel() { @@ -163,7 +169,7 @@ final class SwapConfirmPresenter { ) } - func validators( + private func validators( spendingAmount: Decimal? ) -> [DataValidating] { let feeDecimal = fee.map { Decimal.fromSubstrateAmount( @@ -280,6 +286,7 @@ extension SwapConfirmPresenter: SwapConfirmPresenterProtocol { } func confirm() { + view?.didReceiveStartLoading() let spendingAmount = quote?.amountIn.decimal(precision: initState.chainAssetIn.asset.precision) let validators = validators(spendingAmount: spendingAmount) @@ -360,15 +367,19 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { } func didReceive(error: SwapConfirmError) { + view?.didReceiveStopLoading() switch error { case let .submit(error): - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.submit() + if error.isWatchOnlySigning { + wireframe.presentDismissingNoSigningView(from: view) + } else { + _ = wireframe.present(error: error, from: view, locale: selectedLocale) } } } func didReceiveConfirmation(hash _: String) { + view?.didReceiveStopLoading() wireframe.complete(on: view, locale: selectedLocale) } } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift index 77a90c9adf..8945309686 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift @@ -9,6 +9,8 @@ protocol SwapConfirmViewProtocol: ControllerBackedProtocol { func didReceiveNetworkFee(viewModel: LoadableViewModelState) func didReceiveWallet(viewModel: WalletAccountViewModel?) func didReceiveWarning(viewModel: String?) + func didReceiveStartLoading() + func didReceiveStopLoading() } protocol SwapConfirmPresenterProtocol: AnyObject { @@ -31,7 +33,7 @@ protocol SwapConfirmInteractorOutputProtocol: SwapBaseInteractorOutputProtocol { } protocol SwapConfirmWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, AddressOptionsPresentable, - ErrorPresentable, SwapErrorPresentable, ShortTextInfoPresentable, ModalAlertPresenting { + ErrorPresentable, SwapErrorPresentable, ShortTextInfoPresentable, ModalAlertPresenting, MessageSheetPresentable { func complete(on view: ControllerBackedProtocol?, locale: Locale) } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift index e120230f6d..150478de24 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift @@ -46,7 +46,7 @@ final class SwapConfirmViewController: UIViewController, ViewHolder { rootView.slippageCell.addTarget(self, action: #selector(slippageAction), for: .touchUpInside) rootView.networkFeeCell.addTarget(self, action: #selector(networkFeeAction), for: .touchUpInside) rootView.accountCell.addTarget(self, action: #selector(addressAction), for: .touchUpInside) - rootView.actionButton.addTarget(self, action: #selector(confirmAction), for: .touchUpInside) + rootView.loadableActionView.actionButton.addTarget(self, action: #selector(confirmAction), for: .touchUpInside) } @objc private func rateAction() { @@ -124,6 +124,14 @@ extension SwapConfirmViewController: SwapConfirmViewProtocol { func didReceiveWarning(viewModel: String?) { rootView.set(warning: viewModel) } + + func didReceiveStartLoading() { + rootView.loadableActionView.startLoading() + } + + func didReceiveStopLoading() { + rootView.loadableActionView.stopLoading() + } } extension SwapConfirmViewController: Localizable { diff --git a/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift b/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift index 7d7f11eff0..d0cd473c61 100644 --- a/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift +++ b/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift @@ -45,9 +45,7 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { private var warningView: InlineAlertView? - let actionButton: TriangularedButton = .create { - $0.applyDefaultStyle() - } + let loadableActionView = LoadableActionView() override func setupStyle() { backgroundColor = R.color.colorSecondaryScreenBackground() @@ -64,12 +62,12 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { detailsTableView.addArrangedSubview(slippageCell) detailsTableView.addArrangedSubview(networkFeeCell) - addArrangedSubview(walletTableView) + addArrangedSubview(walletTableView, spacingAfter: 8) walletTableView.addArrangedSubview(walletCell) walletTableView.addArrangedSubview(accountCell) - addSubview(actionButton) - actionButton.snp.makeConstraints { make in + addSubview(loadableActionView) + loadableActionView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) make.bottom.equalTo(safeAreaLayoutGuide).inset(UIConstants.actionBottomInset) make.height.equalTo(UIConstants.actionHeight) @@ -95,16 +93,15 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { accountCell.titleLabel.text = R.string.localizable.commonAccount( preferredLanguages: locale.rLanguages) - actionButton.imageWithTitleView?.title = R.string.localizable.commonConfirm( + loadableActionView.actionButton.imageWithTitleView?.title = R.string.localizable.commonConfirm( preferredLanguages: locale.rLanguages) } func set(warning: String?) { applyWarning( on: &warningView, - after: walletTableView, - text: warning, - spacing: 8 + after: nil, + text: warning ) } } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index 443143ae43..6da5ad8624 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -11,8 +11,7 @@ final class SwapSlippagePresenter { let prefilledPercents: [Decimal] = [0.1, 1, 3] let initPercent: BigRational? let chainAsset: ChainAsset - let amountRestriction: (lower: Decimal, upper: Decimal) = (lower: 0.1, upper: 50) - let amountRecommendation: (lower: Decimal, upper: Decimal) = (lower: 0.1, upper: 5) + let bounds = SlippageBounds() private var percentFormatter: NumberFormatter private var numberFormatter: NumberFormatter @@ -69,39 +68,17 @@ final class SwapSlippagePresenter { } private func provideErrors() { - if let amountInput = amountInput, - amountInput < amountRestriction.lower || amountInput > amountRestriction.upper { - let minAmountString = title(for: amountRestriction.lower) - let maxAmountString = title(for: amountRestriction.upper) - let error = R.string.localizable.swapsSetupSlippageErrorAmountBounds( - minAmountString, - maxAmountString, - preferredLanguages: selectedLocale.rLanguages - ) - view?.didReceiveInput(error: error) - } else { - view?.didReceiveInput(error: nil) - } + let error = bounds.error( + for: amountInput, + stringAmountClosure: title, + locale: selectedLocale + ) + view?.didReceiveInput(error: error) } private func provideWarnings() { - guard let amountInput = amountInput, amountInput > 0 else { - view?.didReceiveInput(warning: nil) - return - } - if amountInput <= amountRecommendation.lower { - let warning = R.string.localizable.swapsSetupSlippageWarningLowAmount( - preferredLanguages: selectedLocale.rLanguages - ) - view?.didReceiveInput(warning: warning) - } else if amountInput >= amountRecommendation.upper { - let warning = R.string.localizable.swapsSetupSlippageWarningHighAmount( - preferredLanguages: selectedLocale.rLanguages - ) - view?.didReceiveInput(warning: warning) - } else { - view?.didReceiveInput(warning: nil) - } + let warning = bounds.warning(for: amountInput, locale: selectedLocale) + view?.didReceiveInput(warning: warning) } } From b4630d8438d1874a4fc385fe8a912655897bbf9a Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 27 Oct 2023 12:30:32 +0300 Subject: [PATCH 079/204] fixes after merging --- .../Swaps/Slippage/SwapSlippagePresenter.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index cd3feccb00..b58e8eb3fc 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -62,26 +62,26 @@ final class SwapSlippagePresenter { } private func provideButtonStates() { + let error = bounds.error( + for: amountInput, + stringAmountClosure: title, + locale: selectedLocale + ) let canReset = amountInput != AssetConversionConstants.defaultSlippage view?.didReceiveResetState(available: canReset) - let canApply = amountInput != initialPercent() + let canApply = amountInput != initialPercent() && error == nil view?.didReceiveButtonState(available: canApply) } private func provideErrors() { - let error = bounds.error( + let error = bounds.error( for: amountInput, stringAmountClosure: title, locale: selectedLocale ) view?.didReceiveInput(error: error) - if error != nil { - view?.didReceiveButtonState(available: false) - } else { - view?.didReceiveButtonState(available: amountInput != initialPercent()) - } - view?.didReceiveButtonState(available: amountInput != initialPercent()) + provideButtonStates() } private func provideWarnings() { From 4fe397412319c0aa073dc310749b62a622520d5e Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 27 Oct 2023 12:39:37 +0300 Subject: [PATCH 080/204] fix icon --- novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift b/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift index a475ee8eb6..1ca1eb8c30 100644 --- a/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift +++ b/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift @@ -18,7 +18,7 @@ final class SwapElementView: UIView { } let assetIconView: AssetIconView = .create { - $0.contentInsets = .zero + $0.contentInsets = UIEdgeInsets(top: 6, left: 6, bottom: 6, right: 6) $0.backgroundView.cornerRadius = SwapElementView.assetIconRadius } From a73d39e257643f5e15a5591e35aa453d67814e2a Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 27 Oct 2023 13:19:57 +0300 Subject: [PATCH 081/204] add alert for rate change --- .../Model/AssetConversion.swift | 2 +- .../Swaps/Confirm/SwapConfirmPresenter.swift | 70 +++++++++++++++++-- novawallet/en.lproj/Localizable.strings | 2 + novawallet/ru.lproj/Localizable.strings | 2 + 4 files changed, 68 insertions(+), 8 deletions(-) diff --git a/novawallet/Modules/AssetConversion/Model/AssetConversion.swift b/novawallet/Modules/AssetConversion/Model/AssetConversion.swift index 1be3d0c80f..aefbd794a0 100644 --- a/novawallet/Modules/AssetConversion/Model/AssetConversion.swift +++ b/novawallet/Modules/AssetConversion/Model/AssetConversion.swift @@ -14,7 +14,7 @@ enum AssetConversion { let direction: Direction } - struct Quote { + struct Quote: Equatable { let amountIn: BigUInt let assetIn: ChainAssetId let amountOut: BigUInt diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index e5b5e0dabf..c5785262fe 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -230,6 +230,58 @@ final class SwapConfirmPresenter { interactor.submit(args: args) } + + private func checkRateChanged( + oldValue: AssetConversion.Quote, + newValue: AssetConversion.Quote, + confirmClosure: @escaping () -> Void + ) { + guard oldValue != newValue else { + confirmClosure() + return + } + let oldRateParams = RateParams( + assetDisplayInfoIn: initState.chainAssetIn.assetDisplayInfo, + assetDisplayInfoOut: initState.chainAssetOut.assetDisplayInfo, + amountIn: oldValue.amountIn, + amountOut: oldValue.amountOut + ) + let newRateParams = RateParams( + assetDisplayInfoIn: initState.chainAssetIn.assetDisplayInfo, + assetDisplayInfoOut: initState.chainAssetOut.assetDisplayInfo, + amountIn: newValue.amountIn, + amountOut: newValue.amountOut + ) + + let oldRate = viewModelFactory.rateViewModel(from: oldRateParams) + let newRate = viewModelFactory.rateViewModel(from: newRateParams) + + let title = R.string.localizable.swapsSetupErrorRateWasUpdatedTitle( + preferredLanguages: selectedLocale.rLanguages + ) + let message = R.string.localizable.swapsSetupErrorRateWasUpdatedMessage( + oldRate, + newRate, + preferredLanguages: selectedLocale.rLanguages + ) + let confirmTitle = R.string.localizable.commonConfirm( + preferredLanguages: selectedLocale.rLanguages + ) + let cancelTitle = R.string.localizable.commonCancel( + preferredLanguages: selectedLocale.rLanguages + ) + let confirmAction = AlertPresentableAction(title: confirmTitle, handler: confirmClosure) + wireframe.present( + viewModel: .init( + title: title, + message: message, + actions: [confirmAction], + closeAction: cancelTitle + ), + style: .alert, + from: view + ) + } } extension SwapConfirmPresenter: SwapConfirmPresenterProtocol { @@ -299,13 +351,17 @@ extension SwapConfirmPresenter: SwapConfirmPresenterProtocol { extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { func didReceive(quote: AssetConversion.Quote, for _: AssetConversion.QuoteArgs) { - self.quote = quote - - provideAssetInViewModel() - provideAssetOutViewModel() - provideRateViewModel() - providePriceDifferenceViewModel() - estimateFee() + checkRateChanged( + oldValue: self.quote ?? initState.quote, + newValue: quote + ) { [weak self] in + self?.quote = quote + self?.provideAssetInViewModel() + self?.provideAssetOutViewModel() + self?.provideRateViewModel() + self?.providePriceDifferenceViewModel() + self?.estimateFee() + } } func didReceive(fee: BigUInt?, transactionId _: TransactionFeeId) { diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 45feb5f05c..afab5a7529 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1407,3 +1407,5 @@ "polkadot.staking.promotion.title" = "Boost your DOT 🚀"; "polkadot.staking.promotion.message" = "Received your DOT back from crowdloans? Start staking your DOT today to get the maximum possible rewards!"; "swaps.setup.price.difference.description" = "Price difference refers to the difference in price between two different assets. When making a swap in crypto, the price difference is usually the difference between the price of the asset you are swapping for and the price of the asset you are swapping with."; +"swaps.setup.error.rate.was.updated.title" = "Swap rate was updated"; +"swaps.setup.error.rate.was.updated.message" = "Old rate: %@.\nNew rate:%@"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 207864f11f..84b8cc12e2 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1407,3 +1407,5 @@ "polkadot.staking.promotion.title" = "Максимизируйте\nнаграды от DOT 🚀"; "polkadot.staking.promotion.message" = "Получили свои DOT из краудлоунов? Начните стейкать DOT уже сегодня, чтобы получить максимальные вознаграждения!"; "swaps.setup.price.difference.description" = "Разница в цене относится к разнице в цене между двумя различными активами. При совершении обмена в криптовалюте разница в цене обычно представляет собой разницу между ценой актива, на который вы меняете, и ценой актива, на который вы меняетесь."; +"swaps.setup.error.rate.was.updated.title" = "Обменный курс был обновлен"; +"swaps.setup.error.rate.was.updated.message" = "Было: %@.\nСтало:%@"; From fe816d2d1313eff01a6ebbe1284c2e002faad6f9 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 27 Oct 2023 17:25:21 +0300 Subject: [PATCH 082/204] bugfixes --- .../Confirm/Model/SwapConfirmViewModelFactory.swift | 10 +++++----- .../Modules/Swaps/Confirm/View/SwapElementView.swift | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift index 73386b8baa..857bd274e2 100644 --- a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift @@ -27,7 +27,7 @@ final class SwapConfirmViewModelFactory { let walletViewModelFactory = WalletAccountViewModelFactory() let networkViewModelFactory: NetworkViewModelFactoryProtocol private var localizedPercentForamatter: NumberFormatter - private var priceDifferenceWarningRange: (start: Decimal, end: Decimal) = (start: 10, end: 20) + private var priceDifferenceWarningRange: (start: Decimal, end: Decimal) = (start: 0.1, end: 0.2) var locale: Locale { didSet { @@ -127,15 +127,15 @@ extension SwapConfirmViewModelFactory: SwapConfirmViewModelFactoryProtocol { let amountPriceIn = amountInDecimal * priceIn let amountPriceOut = amountOutDecimal * priceOut - guard amountPriceOut > 0 else { + guard amountPriceOut != 0, amountPriceIn > amountPriceOut else { return nil } - let diff = (amountPriceIn - amountPriceOut) / amountPriceOut * 100 + var diff = abs(amountPriceIn - amountPriceOut) / amountPriceOut let diffString = localizedPercentForamatter.stringFromDecimal(diff) ?? "" - switch abs(diff) { - case _ where abs(diff) > priceDifferenceWarningRange.end: + switch diff { + case _ where diff > priceDifferenceWarningRange.end: return .init(details: diffString, attention: .high) case priceDifferenceWarningRange.start ..< priceDifferenceWarningRange.end: return .init(details: diffString, attention: .medium) diff --git a/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift b/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift index 1ca1eb8c30..5b85f361fb 100644 --- a/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift +++ b/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift @@ -119,7 +119,7 @@ extension SwapElementView { ) hubIconNameView.detailsLabel.text = viewModel.hub.name valueLabel.text = viewModel.balance.amount - priceLabel.text = viewModel.balance.price + priceLabel.text = viewModel.balance.price ?? " " } } @@ -127,11 +127,11 @@ extension SwapInfoViewCell { func bind(attention: AttentionState) { switch attention { case .high: - titleButton.imageWithTitleView?.titleColor = R.color.colorTextNegative() + valueLabel.textColor = R.color.colorTextNegative() case .medium: - titleButton.imageWithTitleView?.titleColor = R.color.colorTextWarning() + valueLabel.textColor = R.color.colorTextWarning() case .low: - titleButton.imageWithTitleView?.titleColor = R.color.colorTextPrimary() + valueLabel.textColor = R.color.colorTextPrimary() } } From 47c647859cdceb2c3d1223c1988f433e7d4dbacc Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 27 Oct 2023 17:35:33 +0300 Subject: [PATCH 083/204] cleanup --- .../Swaps/Base/View/SwapInfoViewCell.swift | 26 +++++++++++++++++++ .../Swaps/Confirm/View/SwapElementView.swift | 26 ------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/novawallet/Modules/Swaps/Base/View/SwapInfoViewCell.swift b/novawallet/Modules/Swaps/Base/View/SwapInfoViewCell.swift index 9fe270ce4e..22e0cf7c10 100644 --- a/novawallet/Modules/Swaps/Base/View/SwapInfoViewCell.swift +++ b/novawallet/Modules/Swaps/Base/View/SwapInfoViewCell.swift @@ -8,3 +8,29 @@ final class SwapInfoViewCell: RowView, StackTableViewCellProtocol rowContentView.bind(loadableViewModel: loadableViewModel) } } + +extension SwapInfoViewCell { + func bind(attention: AttentionState) { + switch attention { + case .high: + valueLabel.textColor = R.color.colorTextNegative() + case .medium: + valueLabel.textColor = R.color.colorTextWarning() + case .low: + valueLabel.textColor = R.color.colorTextPrimary() + } + } + + func bind(differenceViewModel: LoadableViewModelState) { + switch differenceViewModel { + case .loading: + bind(loadableViewModel: .loading) + case let .cached(value): + bind(attention: value.attention) + bind(loadableViewModel: .cached(value: value.details)) + case let .loaded(value): + bind(attention: value.attention) + bind(loadableViewModel: .loaded(value: value.details)) + } + } +} diff --git a/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift b/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift index 5b85f361fb..41d11541c9 100644 --- a/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift +++ b/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift @@ -122,29 +122,3 @@ extension SwapElementView { priceLabel.text = viewModel.balance.price ?? " " } } - -extension SwapInfoViewCell { - func bind(attention: AttentionState) { - switch attention { - case .high: - valueLabel.textColor = R.color.colorTextNegative() - case .medium: - valueLabel.textColor = R.color.colorTextWarning() - case .low: - valueLabel.textColor = R.color.colorTextPrimary() - } - } - - func bind(differenceViewModel: LoadableViewModelState) { - switch differenceViewModel { - case .loading: - bind(loadableViewModel: .loading) - case let .cached(value): - bind(attention: value.attention) - bind(loadableViewModel: .cached(value: value.details)) - case let .loaded(value): - bind(attention: value.attention) - bind(loadableViewModel: .loaded(value: value.details)) - } - } -} From 7f9f68a1ff93f123b7f903ea8cae9bcb10fa966f Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 27 Oct 2023 16:44:54 +0200 Subject: [PATCH 084/204] refactor swap screen --- novawallet.xcodeproj/project.pbxproj | 12 ++ .../ChainRegistry/LocalChain/ChainModel.swift | 8 ++ .../Service/AssetConversionFeeService.swift | 16 ++- .../Service/AssetHub/AssetHubFeeService.swift | 6 +- .../Base/Model/AssetHubFeeModelBuilder.swift | 61 +++++++++ .../Swaps/Base/SwapBaseInteractor.swift | 122 ++++++------------ .../Swaps/Base/SwapBaseProtocols.swift | 2 +- .../Swaps/Confirm/SwapConfirmInteractor.swift | 16 ++- .../Swaps/Confirm/SwapConfirmPresenter.swift | 2 +- .../Confirm/SwapConfirmViewFactory.swift | 18 ++- .../Setup/SwapSetupPresenter+Validating.swift | 6 +- .../Swaps/Setup/SwapSetupPresenter.swift | 26 ++-- .../Swaps/Setup/SwapSetupViewFactory.swift | 21 ++- 13 files changed, 196 insertions(+), 120 deletions(-) create mode 100644 novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index ae3780af58..e00aacf3a2 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -97,6 +97,7 @@ 0C2F86962A72807E00593C01 /* NominationPoolsRewardEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F86952A72807E00593C01 /* NominationPoolsRewardEngine.swift */; }; 0C2F86982A728EE900593C01 /* NPoolsRewardEngineFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F86972A728EE900593C01 /* NPoolsRewardEngineFactory.swift */; }; 0C2F869A2A72948100593C01 /* NominationPoolsApyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F86992A72948100593C01 /* NominationPoolsApyTests.swift */; }; + 0C2FDF192AEB82AA006A6C59 /* AssetHubFeeModelBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2FDF182AEB82AA006A6C59 /* AssetHubFeeModelBuilder.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 */; }; @@ -4138,6 +4139,7 @@ 0C2F86952A72807E00593C01 /* NominationPoolsRewardEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsRewardEngine.swift; sourceTree = ""; }; 0C2F86972A728EE900593C01 /* NPoolsRewardEngineFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsRewardEngineFactory.swift; sourceTree = ""; }; 0C2F86992A72948100593C01 /* NominationPoolsApyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsApyTests.swift; sourceTree = ""; }; + 0C2FDF182AEB82AA006A6C59 /* AssetHubFeeModelBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubFeeModelBuilder.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 = ""; }; @@ -8422,6 +8424,14 @@ path = NominationPools; sourceTree = ""; }; + 0C2FDF172AEB8292006A6C59 /* Model */ = { + isa = PBXGroup; + children = ( + 0C2FDF182AEB82AA006A6C59 /* AssetHubFeeModelBuilder.swift */, + ); + path = Model; + sourceTree = ""; + }; 0C3205BC2A867A46002EB914 /* GasPriceProviders */ = { isa = PBXGroup; children = ( @@ -9623,6 +9633,7 @@ 771901A02AE7E33A00D9C918 /* Base */ = { isa = PBXGroup; children = ( + 0C2FDF172AEB8292006A6C59 /* Model */, 771901A92AE8FFDC00D9C918 /* View */, 771901A12AE7E34D00D9C918 /* SwapBaseInteractor.swift */, 771901A32AE7E48800D9C918 /* SwapBaseProtocols.swift */, @@ -19744,6 +19755,7 @@ 8490146A24A9463B008F705E /* Locale+Localization.swift in Sources */, 84715786291136B100D7D003 /* GovernanceUnlocksViewModel.swift in Sources */, 84DD5F64263DFAB700425ACF /* ErrorConditionViolation.swift in Sources */, + 0C2FDF192AEB82AA006A6C59 /* AssetHubFeeModelBuilder.swift in Sources */, 8428768324AE046300D91AD8 /* LanguageSelectionViewController.swift in Sources */, F452D8CA273E58D5008F7295 /* SettingsTableFooterView.swift in Sources */, 8470D6D2253E3382009E9A5D /* StorageSubscriptionContainer.swift in Sources */, diff --git a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift index 7a2696c05e..a503285130 100644 --- a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift +++ b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift @@ -184,6 +184,14 @@ struct ChainModel: Equatable, Codable, Hashable { return ChainAssetId(chainId: chainId, assetId: utilityAsset.assetId) } + func utilityChainAsset() -> ChainAsset? { + guard let utilityAsset = utilityAssets().first else { + return nil + } + + return ChainAsset(chain: self, asset: utilityAsset) + } + var typesUsage: TypesUsage { if let types = types { return types.overridesCommon ? .onlyOwn : .both diff --git a/novawallet/Modules/AssetConversion/Service/AssetConversionFeeService.swift b/novawallet/Modules/AssetConversion/Service/AssetConversionFeeService.swift index 363c00d4cf..bb0440e868 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetConversionFeeService.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetConversionFeeService.swift @@ -4,12 +4,26 @@ import BigInt extension AssetConversion { struct AmountWithNative { let targetAmount: BigUInt - let nativeAmouunt: BigUInt + let nativeAmount: BigUInt } struct FeeModel { let totalFee: AmountWithNative let networkFeeAddition: AmountWithNative? + + var networkFee: AmountWithNative { + guard let addition = networkFeeAddition else { + return totalFee + } + + let feeInTargetToken = totalFee.targetAmount >= addition.targetAmount ? + totalFee.targetAmount - addition.targetAmount : totalFee.targetAmount + + let feeInNativeToken = totalFee.nativeAmount >= addition.nativeAmount ? + totalFee.nativeAmount - addition.nativeAmount : totalFee.nativeAmount + + return .init(targetAmount: feeInTargetToken, nativeAmount: feeInNativeToken) + } } typealias FeeResult = Result diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubFeeService.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubFeeService.swift index 31d96bbdab..466f50ca47 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubFeeService.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubFeeService.swift @@ -187,7 +187,7 @@ final class AssetHubFeeService: AnyCancellableCleaning { return .init( totalFee: .init( targetAmount: feeAmount, - nativeAmouunt: feeAmount + nativeAmount: feeAmount ), networkFeeAddition: nil ) @@ -242,11 +242,11 @@ final class AssetHubFeeService: AnyCancellableCleaning { return .init( totalFee: .init( targetAmount: quotes[0].amountIn, - nativeAmouunt: feeAmount + edAmount + nativeAmount: feeAmount + edAmount ), networkFeeAddition: .init( targetAmount: quotes[1].amountIn, - nativeAmouunt: edAmount + nativeAmount: edAmount ) ) } diff --git a/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift b/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift new file mode 100644 index 0000000000..578e2413cd --- /dev/null +++ b/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift @@ -0,0 +1,61 @@ +import Foundation + +final class AssetHubFeeModelBuilder { + typealias ResultClosure = (AssetConversion.FeeModel, AssetConversion.CallArgs) -> Void + + let utilityChainAssetId: ChainAssetId + let resultClosure: ResultClosure + + private(set) var feeAsset: ChainAsset? + + private var recepientUtilityBalance: AssetBalance? + private var feeModel: AssetConversion.FeeModel? + private var callArgs: AssetConversion.CallArgs? + + init( + utilityChainAssetId: ChainAssetId, + resultClosure: @escaping ResultClosure + ) { + self.utilityChainAssetId = utilityChainAssetId + self.resultClosure = resultClosure + } + + private func provideResult() { + guard + let balance = recepientUtilityBalance, + let feeModel = feeModel, + let callArgs = callArgs else { + return + } + + let resultModel: AssetConversion.FeeModel + + if balance.totalInPlank >= feeModel.totalFee.nativeAmount { + // we have enough tokens for ed - need to exchange only network fee + let networkFee = feeModel.networkFee + resultModel = .init(totalFee: networkFee, networkFeeAddition: nil) + } else { + resultModel = feeModel + } + + resultClosure(resultModel, callArgs) + } +} + +extension AssetHubFeeModelBuilder { + func apply(recepientUtilityBalance: AssetBalance) { + self.recepientUtilityBalance = recepientUtilityBalance + provideResult() + } + + func apply(feeModel: AssetConversion.FeeModel, args: AssetConversion.CallArgs) { + self.feeModel = feeModel + callArgs = args + + provideResult() + } + + func apply(feeAsset: ChainAsset) { + self.feeAsset = feeAsset + } +} diff --git a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift index d1134841d9..02f998e6cc 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift @@ -5,10 +5,7 @@ import BigInt class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapBaseInteractorInputProtocol { weak var basePresenter: SwapBaseInteractorOutputProtocol? let assetConversionOperationFactory: AssetConversionOperationFactoryProtocol - let assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol - let runtimeService: RuntimeProviderProtocol - let feeProxy: ExtrinsicFeeProxyProtocol - let extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol + let assetConversionFeeService: AssetConversionFeeServiceProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol let currencyManager: CurrencyManagerProtocol @@ -16,18 +13,14 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB private let operationQueue: OperationQueue private var quoteCall: CancellableCall? - private var runtimeOperationCall: CancellableCall? - private var extrinsicService: ExtrinsicServiceProtocol? private var priceProviders: [ChainAssetId: StreamableProvider] = [:] private var assetBalanceProviders: [ChainAssetId: StreamableProvider] = [:] + private var feeModelBuilder: AssetHubFeeModelBuilder? init( assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, - assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol, - runtimeService: RuntimeProviderProtocol, - feeProxy: ExtrinsicFeeProxyProtocol, - extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, + assetConversionFeeService: AssetConversionFeeServiceProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, currencyManager: CurrencyManagerProtocol, @@ -35,10 +28,7 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB operationQueue: OperationQueue ) { self.assetConversionOperationFactory = assetConversionOperationFactory - self.assetConversionExtrinsicService = assetConversionExtrinsicService - self.runtimeService = runtimeService - self.feeProxy = feeProxy - self.extrinsicServiceFactory = extrinsicServiceFactory + self.assetConversionFeeService = assetConversionFeeService self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory self.currencyManager = currencyManager @@ -46,33 +36,17 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB self.operationQueue = operationQueue } - private func updateExtrinsicService(for feeChainAsset: ChainAsset) { - guard let chainAccount = chainAccountResponse(for: feeChainAsset) else { - extrinsicService = nil + func updateFeeModelBuilder(for chain: ChainModel) { + guard + let utilityAsset = chain.utilityChainAsset(), + feeModelBuilder?.utilityChainAssetId != utilityAsset.chainAssetId else { return } - guard !feeChainAsset.isUtilityAsset else { - extrinsicService = extrinsicServiceFactory.createService( - account: chainAccount, - chain: feeChainAsset.chain - ) - return - } - - if - let assetType = feeChainAsset.asset.type, - case .statemine = AssetType(rawValue: assetType), - let typeExtras = feeChainAsset.asset.typeExtras, - let extras = try? typeExtras.map(to: StatemineAssetExtras.self), - let assetId = UInt32(extras.assetId) { - extrinsicService = extrinsicServiceFactory.createService( - account: chainAccount, - chain: feeChainAsset.chain, - feeAssetId: assetId - ) - } else { - extrinsicService = nil + feeModelBuilder = AssetHubFeeModelBuilder( + utilityChainAssetId: utilityAsset.chainAssetId + ) { [weak self] feeModel, callArgs in + self?.basePresenter?.didReceive(fee: feeModel, transactionId: callArgs.identifier) } } @@ -141,37 +115,22 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB } func fee(args: AssetConversion.CallArgs) { - clear(cancellable: &runtimeOperationCall) - guard let extrinsicService = extrinsicService else { + guard let feeAsset = feeModelBuilder?.feeAsset else { return } - let runtimeCoderFactoryOperation = runtimeService.fetchCoderFactoryOperation() - - runtimeCoderFactoryOperation.completionBlock = { [weak self] in - guard let self = self else { - return - } - do { - let runtimeCoderFactory = try runtimeCoderFactoryOperation.extractNoCancellableResultData() - let builder = self.assetConversionExtrinsicService.fetchExtrinsicBuilderClosure( - for: args, - codingFactory: runtimeCoderFactory - ) - self.feeProxy.estimateFee( - using: extrinsicService, - reuseIdentifier: args.identifier, - setupBy: builder - ) - } catch { - DispatchQueue.main.async { - self.basePresenter?.didReceive(error: .fetchFeeFailed(error, args.identifier)) - } + assetConversionFeeService.calculate( + in: feeAsset, + callArgs: args, + runCompletionIn: .main + ) { [weak self] result in + switch result { + case let .success(feeModel): + self?.feeModelBuilder?.apply(feeModel: feeModel, args: args) + case let .failure(error): + self?.basePresenter?.didReceive(error: .fetchFeeFailed(error, args.identifier)) } } - - runtimeOperationCall = runtimeCoderFactoryOperation - operationQueue.addOperation(runtimeCoderFactoryOperation) } func chainAccountResponse(for chainAsset: ChainAsset) -> ChainAccountResponse? { @@ -180,10 +139,18 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB } func set(receiveChainAsset chainAsset: ChainAsset) { + updateFeeModelBuilder(for: chainAsset.chain) + priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) } func set(payChainAsset chainAsset: ChainAsset) { + updateFeeModelBuilder(for: chainAsset.chain) + + if let utilityAsset = chainAsset.chain.utilityChainAsset() { + feeModelBuilder?.apply(feeAsset: utilityAsset) + } + guard let chainAccount = chainAccountResponse(for: chainAsset) else { basePresenter?.didReceive(payAccountId: nil) return @@ -195,15 +162,16 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB } func set(feeChainAsset chainAsset: ChainAsset) { + updateFeeModelBuilder(for: chainAsset.chain) + feeModelBuilder?.apply(feeAsset: chainAsset) + priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) } // MARK: - SwapBaseInteractorInputProtocol - func setup() { - feeProxy.delegate = self - } + func setup() {} func calculateQuote(for args: AssetConversion.QuoteArgs) { quote(args: args) @@ -220,20 +188,6 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB } } -extension SwapBaseInteractor: ExtrinsicFeeProxyDelegate { - func didReceiveFee(result: Result, for transactionId: TransactionFeeId) { - DispatchQueue.main.async { - switch result { - case let .success(dispatchInfo): - let fee = BigUInt(dispatchInfo.fee) - self.basePresenter?.didReceive(fee: fee, transactionId: transactionId) - case let .failure(error): - self.basePresenter?.didReceive(error: .fetchFeeFailed(error, transactionId)) - } - } - } -} - extension SwapBaseInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { func handlePrice(result: Result, priceId: AssetModel.PriceId) { switch result { @@ -259,11 +213,17 @@ extension SwapBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscript for: .init(chainId: chainId, assetId: assetId), accountId: accountId ) + + if feeModelBuilder?.utilityChainAssetId == chainAssetId { + feeModelBuilder?.apply(recepientUtilityBalance: balance) + } + basePresenter?.didReceive( balance: balance, for: chainAssetId, accountId: accountId ) + case let .failure(error): basePresenter?.didReceive(error: .assetBalance(error, chainAssetId, accountId)) } diff --git a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift index ee3b80a4c3..217bc660fa 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift @@ -9,7 +9,7 @@ protocol SwapBaseInteractorInputProtocol: AnyObject { protocol SwapBaseInteractorOutputProtocol: AnyObject { func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) - func didReceive(fee: BigUInt?, transactionId: TransactionFeeId) + func didReceive(fee: AssetConversion.FeeModel?, transactionId: TransactionFeeId) func didReceive(error: SwapSetupError) func didReceive(price: PriceData?, priceId: AssetModel.PriceId) func didReceive(payAccountId: AccountId?) diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift index 734b2187b0..4af191a5b4 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift @@ -2,6 +2,10 @@ import UIKit final class SwapConfirmInteractor: SwapBaseInteractor { weak var presenter: SwapConfirmInteractorOutputProtocol? + + let runtimeService: RuntimeProviderProtocol + let extrinsicService: ExtrinsicServiceProtocol + let assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol let payChainAsset: ChainAsset let receiveChainAsset: ChainAsset let feeChainAsset: ChainAsset @@ -12,17 +16,20 @@ final class SwapConfirmInteractor: SwapBaseInteractor { receiveChainAsset: ChainAsset, feeChainAsset: ChainAsset, slippage: BigRational, + assetConversionFeeService: AssetConversionFeeServiceProtocol, assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol, runtimeService: RuntimeProviderProtocol, - feeProxy: ExtrinsicFeeProxyProtocol, - extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, + extrinsicService: ExtrinsicServiceProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, currencyManager: CurrencyManagerProtocol, selectedAccount: MetaAccountModel, operationQueue: OperationQueue ) { + self.runtimeService = runtimeService + self.extrinsicService = extrinsicService + self.assetConversionExtrinsicService = assetConversionExtrinsicService self.payChainAsset = payChainAsset self.receiveChainAsset = receiveChainAsset self.feeChainAsset = feeChainAsset @@ -30,10 +37,7 @@ final class SwapConfirmInteractor: SwapBaseInteractor { super.init( assetConversionOperationFactory: assetConversionOperationFactory, - assetConversionExtrinsicService: assetConversionExtrinsicService, - runtimeService: runtimeService, - feeProxy: feeProxy, - extrinsicServiceFactory: extrinsicServiceFactory, + assetConversionFeeService: assetConversionFeeService, priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, currencyManager: currencyManager, diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 38ceacc29f..07408b21e6 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -24,7 +24,7 @@ extension SwapConfirmPresenter: SwapConfirmPresenterProtocol { extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { func didReceive(quote _: AssetConversion.Quote, for _: AssetConversion.QuoteArgs) {} - func didReceive(fee _: BigUInt?, transactionId _: TransactionFeeId) {} + func didReceive(fee _: AssetConversion.FeeModel?, transactionId _: TransactionFeeId) {} func didReceive(error _: SwapSetupError) {} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index 33bd346ede..82fabb45c1 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -45,7 +45,8 @@ struct SwapConfirmViewFactory { let runtimeService = chainRegistry.getRuntimeProvider(for: westmintChainId), let chainModel = chainRegistry.getChain(for: westmintChainId), let currencyManager = CurrencyManager.shared, - let selectedAccount = SelectedWalletSettings.shared.value else { + let selectedWallet = SelectedWalletSettings.shared.value, + let selectedAccount = selectedWallet.fetch(for: chainModel.accountRequest()) else { return nil } @@ -57,10 +58,17 @@ struct SwapConfirmViewFactory { connection: connection, operationQueue: operationQueue ) - let extrinsicServiceFactory = ExtrinsicServiceFactory( + + let extrinsicService = ExtrinsicServiceFactory( runtimeRegistry: runtimeService, engine: connection, operationManager: OperationManager(operationQueue: operationQueue) + ).createService(account: selectedAccount, chain: chainModel) + + let feeService = AssetHubFeeService( + wallet: selectedWallet, + chainRegistry: chainRegistry, + operationQueue: operationQueue ) let interactor = SwapConfirmInteractor( @@ -68,15 +76,15 @@ struct SwapConfirmViewFactory { receiveChainAsset: receiveChainAsset, feeChainAsset: feeChainAsset, slippage: slippage, + assetConversionFeeService: feeService, assetConversionOperationFactory: assetConversionOperationFactory, assetConversionExtrinsicService: AssetHubExtrinsicService(chain: chainModel), runtimeService: runtimeService, - feeProxy: ExtrinsicFeeProxy(), - extrinsicServiceFactory: extrinsicServiceFactory, + extrinsicService: extrinsicService, priceLocalSubscriptionFactory: PriceProviderFactory.shared, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, currencyManager: currencyManager, - selectedAccount: selectedAccount, + selectedAccount: selectedWallet, operationQueue: operationQueue ) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift index fa0345d5f3..0e58a0a583 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift @@ -8,11 +8,11 @@ extension SwapSetupPresenter { feeChainAsset: ChainAsset ) -> [DataValidating] { let feeDecimal = fee.map { Decimal.fromSubstrateAmount( - $0, + $0.totalFee.targetAmount, precision: Int16(feeChainAsset.asset.precision) ) } ?? nil - var validators: [DataValidating] = [ + let validators: [DataValidating] = [ dataValidatingFactory.has(fee: feeDecimal, locale: selectedLocale) { [weak self] in self?.estimateFee() }, @@ -24,7 +24,7 @@ extension SwapSetupPresenter { ), dataValidatingFactory.canPayFeeSpendingAmountInPlank( balance: payAssetBalance?.transferable, - fee: payChainAsset.chainAssetId == feeChainAsset.chainAssetId ? fee : nil, + fee: payChainAsset.chainAssetId == feeChainAsset.chainAssetId ? fee?.totalFee.targetAmount : nil, spendingAmount: spendingAmount, asset: feeChainAsset.assetDisplayInfo, locale: selectedLocale diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 739ff16b01..e32cd7a5bd 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -8,6 +8,7 @@ final class SwapSetupPresenter { let interactor: SwapSetupInteractorInputProtocol let viewModelFactory: SwapsSetupViewModelFactoryProtocol let dataValidatingFactory: SwapDataValidatorFactoryProtocol + let logger: LoggerProtocol private(set) var payAssetBalance: AssetBalance? private(set) var feeAssetBalance: AssetBalance? @@ -20,7 +21,7 @@ final class SwapSetupPresenter { private(set) var payAmountInput: AmountInputResult? private(set) var receiveAmountInput: Decimal? - private(set) var fee: BigUInt? + private(set) var fee: AssetConversion.FeeModel? private(set) var quote: AssetConversion.Quote? private(set) var quoteArgs: AssetConversion.QuoteArgs? { didSet { @@ -38,12 +39,14 @@ final class SwapSetupPresenter { wireframe: SwapSetupWireframeProtocol, viewModelFactory: SwapsSetupViewModelFactoryProtocol, dataValidatingFactory: SwapDataValidatorFactoryProtocol, - localizationManager: LocalizationManagerProtocol + localizationManager: LocalizationManagerProtocol, + logger: LoggerProtocol ) { self.interactor = interactor self.wireframe = wireframe self.viewModelFactory = viewModelFactory self.dataValidatingFactory = dataValidatingFactory + self.logger = logger self.localizationManager = localizationManager } @@ -207,7 +210,7 @@ final class SwapSetupPresenter { guard quoteArgs != nil, let feeChainAsset = feeChainAsset else { return } - guard let fee = fee else { + guard let fee = fee?.networkFee.targetAmount else { view?.didReceiveNetworkFee(viewModel: .loading) return } @@ -327,7 +330,7 @@ final class SwapSetupPresenter { return nil } let balanceValue = payAssetBalance?.transferable ?? 0 - let feeValue = payChainAsset.chainAssetId == feeChainAsset?.chainAssetId ? fee : 0 + let feeValue = payChainAsset.chainAssetId == feeChainAsset?.chainAssetId ? fee?.totalFee.targetAmount : 0 let precision = Int16(payChainAsset.asset.precision) @@ -383,15 +386,19 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { func selectPayToken() { wireframe.showPayTokenSelection(from: view, chainAsset: receiveChainAsset) { [weak self] chainAsset in self?.payChainAsset = chainAsset - // TODO: select fee asset - self?.feeChainAsset = chainAsset.chain.utilityAsset().map { + let feeChainAsset = chainAsset.chain.utilityAsset().map { ChainAsset(chain: chainAsset.chain, asset: $0) } + + self?.feeChainAsset = feeChainAsset + self?.providePayAssetViews() self?.provideButtonState() self?.provideSettingsState() - self?.refreshQuote(direction: .sell, forceUpdate: false) self?.interactor.update(payChainAsset: chainAsset) + self?.interactor.update(feeChainAsset: feeChainAsset) + + self?.refreshQuote(direction: .sell, forceUpdate: false) } } @@ -578,10 +585,13 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { provideButtonState() } - func didReceive(fee: BigUInt?, transactionId: TransactionFeeId) { + func didReceive(fee: AssetConversion.FeeModel?, transactionId: TransactionFeeId) { guard feeIdentifier == transactionId else { return } + + logger.debug("Did receive fee: \(fee.map { String($0.totalFee.targetAmount) })") + self.fee = fee provideFeeViewModel() provideButtonState() diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index 9dde9a5373..bc5a1a683c 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -31,7 +31,8 @@ struct SwapSetupViewFactory { wireframe: wireframe, viewModelFactory: viewModelFactory, dataValidatingFactory: dataValidatingFactory, - localizationManager: LocalizationManager.shared + localizationManager: LocalizationManager.shared, + logger: Logger.shared ) let view = SwapSetupViewController( @@ -54,7 +55,7 @@ struct SwapSetupViewFactory { let runtimeService = chainRegistry.getRuntimeProvider(for: westmintChainId), let chainModel = chainRegistry.getChain(for: westmintChainId), let currencyManager = CurrencyManager.shared, - let selectedAccount = SelectedWalletSettings.shared.value else { + let selectedWallet = SelectedWalletSettings.shared.value else { return nil } @@ -66,22 +67,20 @@ struct SwapSetupViewFactory { connection: connection, operationQueue: operationQueue ) - let extrinsicServiceFactory = ExtrinsicServiceFactory( - runtimeRegistry: runtimeService, - engine: connection, - operationManager: OperationManager(operationQueue: operationQueue) + + let feeService = AssetHubFeeService( + wallet: selectedWallet, + chainRegistry: chainRegistry, + operationQueue: operationQueue ) let interactor = SwapSetupInteractor( assetConversionOperationFactory: assetConversionOperationFactory, - assetConversionExtrinsicService: AssetHubExtrinsicService(chain: chainModel), - runtimeService: runtimeService, - feeProxy: ExtrinsicFeeProxy(), - extrinsicServiceFactory: extrinsicServiceFactory, + assetConversionFeeService: feeService, priceLocalSubscriptionFactory: PriceProviderFactory.shared, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, currencyManager: currencyManager, - selectedAccount: selectedAccount, + selectedAccount: selectedWallet, operationQueue: operationQueue ) From 78e7db393cc50c50c54c41286dcd90810f672296 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 27 Oct 2023 17:48:40 +0300 Subject: [PATCH 085/204] bugfix --- .../Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift index 857bd274e2..a3cc17f6f0 100644 --- a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift @@ -127,11 +127,11 @@ extension SwapConfirmViewModelFactory: SwapConfirmViewModelFactoryProtocol { let amountPriceIn = amountInDecimal * priceIn let amountPriceOut = amountOutDecimal * priceOut - guard amountPriceOut != 0, amountPriceIn > amountPriceOut else { + guard amountPriceIn != 0, amountPriceIn > amountPriceOut else { return nil } - var diff = abs(amountPriceIn - amountPriceOut) / amountPriceOut + var diff = abs(amountPriceIn - amountPriceOut) / amountPriceIn let diffString = localizedPercentForamatter.stringFromDecimal(diff) ?? "" switch diff { From 1abb1ae1c0248c13e4f8dfbdb920f8f5c1ac987a Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 27 Oct 2023 17:32:52 +0200 Subject: [PATCH 086/204] remove logs --- novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index e32cd7a5bd..06cab8891e 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -590,8 +590,6 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { return } - logger.debug("Did receive fee: \(fee.map { String($0.totalFee.targetAmount) })") - self.fee = fee provideFeeViewModel() provideButtonState() From 8bcb30c3a85d579d258aa255f35176c0eb7e1939 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 27 Oct 2023 20:16:28 +0300 Subject: [PATCH 087/204] init --- novawallet.xcodeproj/project.pbxproj | 4 ++ .../Extension/Foundation/String+Helpers.swift | 8 +++ ...ceDifferenceViewModelFactoryProtocol.swift | 55 +++++++++++++++ .../Model/SwapConfirmViewModelFactory.swift | 47 +------------ .../Model/SwapsSetupViewModelFactory.swift | 67 +++++++++++-------- .../Swaps/Setup/Model/ViewModels.swift | 5 ++ .../Swaps/Setup/SwapSetupPresenter.swift | 58 ++++++++++------ .../Swaps/Setup/SwapSetupProtocols.swift | 2 +- .../Swaps/Setup/SwapSetupViewController.swift | 4 +- .../Swaps/Setup/SwapSetupViewFactory.swift | 4 +- .../Swaps/Setup/View/SwapAmountInput.swift | 35 ++++++++++ .../Setup/View/SwapAmountInputView.swift | 5 ++ 12 files changed, 197 insertions(+), 97 deletions(-) create mode 100644 novawallet/Modules/Swaps/Base/SwapPriceDifferenceViewModelFactoryProtocol.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 5f7bddadde..66582c161d 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -816,6 +816,7 @@ 77E255692A16148000B644C3 /* StakingRewardsFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E255682A16148000B644C3 /* StakingRewardsFilter.swift */; }; 77E304A92AEB9F76006FD6F0 /* SwapConfirmInitState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304A82AEB9F76006FD6F0 /* SwapConfirmInitState.swift */; }; 77E304AB2AEBB214006FD6F0 /* SlippageBounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304AA2AEBB214006FD6F0 /* SlippageBounds.swift */; }; + 77E304AD2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304AC2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift */; }; 77EA2A232A333C1500B0670B /* french_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A182A333C1500B0670B /* french_output.json */; }; 77EA2A242A333C1500B0670B /* arrays_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A192A333C1500B0670B /* arrays_output.json */; }; 77EA2A252A333C1500B0670B /* weird_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A1A2A333C1500B0670B /* weird_output.json */; }; @@ -4861,6 +4862,7 @@ 77E255682A16148000B644C3 /* StakingRewardsFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardsFilter.swift; sourceTree = ""; }; 77E304A82AEB9F76006FD6F0 /* SwapConfirmInitState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConfirmInitState.swift; sourceTree = ""; }; 77E304AA2AEBB214006FD6F0 /* SlippageBounds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlippageBounds.swift; sourceTree = ""; }; + 77E304AC2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPriceDifferenceViewModelFactoryProtocol.swift; sourceTree = ""; }; 77EA2A182A333C1500B0670B /* french_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = french_output.json; sourceTree = ""; }; 77EA2A192A333C1500B0670B /* arrays_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = arrays_output.json; sourceTree = ""; }; 77EA2A1A2A333C1500B0670B /* weird_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = weird_output.json; sourceTree = ""; }; @@ -9630,6 +9632,7 @@ 771901A32AE7E48800D9C918 /* SwapBaseProtocols.swift */, 771901B12AEA3E5100D9C918 /* ShortTextInfoPresentableExtensions.swift */, 77E304AA2AEBB214006FD6F0 /* SlippageBounds.swift */, + 77E304AC2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift */, ); path = Base; sourceTree = ""; @@ -22376,6 +22379,7 @@ 77F189442A49974A00E8B933 /* UITextView+bind.swift in Sources */, D840B64C33EF47E723905378 /* OperationDetailsViewFactory.swift in Sources */, 84355CE628B507AD004E5C5E /* LedgerAccountAmount.swift in Sources */, + 77E304AD2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift in Sources */, 841E5553282E35D000C8438F /* StakingParachainPresenter.swift in Sources */, 88F33F1329CC1ECD006125D5 /* Web3NameAddressesSelectionState.swift in Sources */, EB8FCA00BD20C63D578D6F80 /* TransferSetupProtocols.swift in Sources */, diff --git a/novawallet/Common/Extension/Foundation/String+Helpers.swift b/novawallet/Common/Extension/Foundation/String+Helpers.swift index 49e05dcf6d..334db1c85d 100644 --- a/novawallet/Common/Extension/Foundation/String+Helpers.swift +++ b/novawallet/Common/Extension/Foundation/String+Helpers.swift @@ -37,6 +37,14 @@ extension String { return self } } + + func inParenthesis() -> String { + guard !isEmpty else { + return "" + } + + return "(\(self))" + } } extension Optional where Wrapped == String { diff --git a/novawallet/Modules/Swaps/Base/SwapPriceDifferenceViewModelFactoryProtocol.swift b/novawallet/Modules/Swaps/Base/SwapPriceDifferenceViewModelFactoryProtocol.swift new file mode 100644 index 0000000000..8d04ca07ed --- /dev/null +++ b/novawallet/Modules/Swaps/Base/SwapPriceDifferenceViewModelFactoryProtocol.swift @@ -0,0 +1,55 @@ +import Foundation + +protocol SwapPriceDifferenceViewModelFactoryProtocol { + var localizedPercentForamatter: NumberFormatter { get } + var priceDifferenceWarningRange: (start: Decimal, end: Decimal) { get } + + func priceDifferenceViewModel( + rateParams: RateParams, + priceIn: PriceData?, + priceOut: PriceData? + ) -> DifferenceViewModel? +} + +extension SwapPriceDifferenceViewModelFactoryProtocol { + func priceDifferenceViewModel( + rateParams params: RateParams, + priceIn: PriceData?, + priceOut: PriceData? + ) -> DifferenceViewModel? { + guard + let amountOutDecimal = Decimal.fromSubstrateAmount( + params.amountOut, + precision: params.assetDisplayInfoOut.assetPrecision + ), + let amountInDecimal = Decimal.fromSubstrateAmount( + params.amountIn, + precision: params.assetDisplayInfoIn.assetPrecision + ) else { + return nil + } + guard let priceIn = priceIn?.decimalRate, + let priceOut = priceOut?.decimalRate else { + return nil + } + + let amountPriceIn = amountInDecimal * priceIn + let amountPriceOut = amountOutDecimal * priceOut + + guard amountPriceIn != 0, amountPriceIn > amountPriceOut else { + return nil + } + + var diff = abs(amountPriceIn - amountPriceOut) / amountPriceIn + let diffString = localizedPercentForamatter.stringFromDecimal(diff)?.inParenthesis() ?? "" + + switch diff { + case _ where diff > priceDifferenceWarningRange.end: + return .init(details: diffString, attention: .high) + case priceDifferenceWarningRange.start ..< priceDifferenceWarningRange.end: + return .init(details: diffString, attention: .medium) + default: + return .init(details: diffString, attention: .low) + } + } +} diff --git a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift index a3cc17f6f0..2b7c10e294 100644 --- a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift @@ -2,7 +2,7 @@ import Foundation import SoraFoundation import BigInt -protocol SwapConfirmViewModelFactoryProtocol { +protocol SwapConfirmViewModelFactoryProtocol: SwapPriceDifferenceViewModelFactoryProtocol { var locale: Locale { get set } func assetViewModel( @@ -26,8 +26,8 @@ final class SwapConfirmViewModelFactory { let balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol let walletViewModelFactory = WalletAccountViewModelFactory() let networkViewModelFactory: NetworkViewModelFactoryProtocol - private var localizedPercentForamatter: NumberFormatter - private var priceDifferenceWarningRange: (start: Decimal, end: Decimal) = (start: 0.1, end: 0.2) + private(set) var localizedPercentForamatter: NumberFormatter + private(set) var priceDifferenceWarningRange: (start: Decimal, end: Decimal) = (start: 0.1, end: 0.2) var locale: Locale { didSet { @@ -103,47 +103,6 @@ extension SwapConfirmViewModelFactory: SwapConfirmViewModelFactoryProtocol { return "\(amountIn) = \(amountOut)" } - func priceDifferenceViewModel( - rateParams params: RateParams, - priceIn: PriceData?, - priceOut: PriceData? - ) -> DifferenceViewModel? { - guard - let amountOutDecimal = Decimal.fromSubstrateAmount( - params.amountOut, - precision: params.assetDisplayInfoOut.assetPrecision - ), - let amountInDecimal = Decimal.fromSubstrateAmount( - params.amountIn, - precision: params.assetDisplayInfoIn.assetPrecision - ) else { - return nil - } - guard let priceIn = priceIn?.decimalRate, - let priceOut = priceOut?.decimalRate else { - return nil - } - - let amountPriceIn = amountInDecimal * priceIn - let amountPriceOut = amountOutDecimal * priceOut - - guard amountPriceIn != 0, amountPriceIn > amountPriceOut else { - return nil - } - - var diff = abs(amountPriceIn - amountPriceOut) / amountPriceIn - let diffString = localizedPercentForamatter.stringFromDecimal(diff) ?? "" - - switch diff { - case _ where diff > priceDifferenceWarningRange.end: - return .init(details: diffString, attention: .high) - case priceDifferenceWarningRange.start ..< priceDifferenceWarningRange.end: - return .init(details: diffString, attention: .medium) - default: - return .init(details: diffString, attention: .low) - } - } - func slippageViewModel(slippage: BigRational) -> String { slippage.decimalValue.map { localizedPercentForamatter.stringFromDecimal($0) ?? "" } ?? "" } diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift index 817aca234a..06428f77cd 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift @@ -8,7 +8,9 @@ struct RateParams { let amountOut: BigUInt } -protocol SwapsSetupViewModelFactoryProtocol { +protocol SwapsSetupViewModelFactoryProtocol: SwapPriceDifferenceViewModelFactoryProtocol { + var locale: Locale { get set } + func buttonState( assetIn: ChainAssetId?, assetOut: ChainAssetId?, @@ -17,38 +19,49 @@ protocol SwapsSetupViewModelFactoryProtocol { ) -> ButtonState func payTitleViewModel( assetDisplayInfo: AssetBalanceDisplayInfo?, - maxValue: BigUInt?, - locale: Locale + maxValue: BigUInt? ) -> TitleHorizontalMultiValueView.Model - func payAssetViewModel(chainAsset: ChainAsset?, locale: Locale) -> SwapAssetInputViewModel + func payAssetViewModel(chainAsset: ChainAsset?) -> SwapAssetInputViewModel func inputPriceViewModel( assetDisplayInfo: AssetBalanceDisplayInfo, amount: Decimal?, - priceData: PriceData?, - locale: Locale + priceData: PriceData? ) -> String? - func receiveTitleViewModel(locale: Locale) -> TitleHorizontalMultiValueView.Model - func receiveAssetViewModel(chainAsset: ChainAsset?, locale: Locale) -> SwapAssetInputViewModel - func amountInputViewModel(chainAsset: ChainAsset, amount: Decimal?, locale: Locale) -> AmountInputViewModelProtocol - func rateViewModel(from params: RateParams, locale: Locale) -> String + func receiveTitleViewModel() -> TitleHorizontalMultiValueView.Model + func receiveAssetViewModel(chainAsset: ChainAsset?) -> SwapAssetInputViewModel + func amountInputViewModel(chainAsset: ChainAsset, amount: Decimal?) -> AmountInputViewModelProtocol + func rateViewModel(from params: RateParams) -> String func feeViewModel( amount: BigUInt, assetDisplayInfo: AssetBalanceDisplayInfo, - priceData: PriceData?, - locale: Locale + priceData: PriceData? ) -> SwapFeeViewModel } final class SwapsSetupViewModelFactory { let balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol let networkViewModelFactory: NetworkViewModelFactoryProtocol + let percentForamatter: LocalizableResource + private(set) var localizedPercentForamatter: NumberFormatter + private(set) var priceDifferenceWarningRange: (start: Decimal, end: Decimal) = (start: 0.1, end: 0.2) + + var locale: Locale { + didSet { + localizedPercentForamatter = percentForamatter.value(for: locale) + } + } init( balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol, - networkViewModelFactory: NetworkViewModelFactoryProtocol + networkViewModelFactory: NetworkViewModelFactoryProtocol, + percentForamatter: LocalizableResource, + locale: Locale ) { self.balanceViewModelFactoryFacade = balanceViewModelFactoryFacade self.networkViewModelFactory = networkViewModelFactory + self.percentForamatter = percentForamatter + self.locale = locale + localizedPercentForamatter = percentForamatter.value(for: locale) } private static func buttonTitle( @@ -84,7 +97,7 @@ final class SwapsSetupViewModelFactory { ) } - private func emptyPayAssetViewModel(locale: Locale) -> EmptySwapsAssetViewModel { + private func emptyPayAssetViewModel() -> EmptySwapsAssetViewModel { EmptySwapsAssetViewModel( imageViewModel: StaticImageViewModel(image: R.image.iconAddSwapAmount()!), title: R.string.localizable.swapsSetupAssetPayTitle(preferredLanguages: locale.rLanguages), @@ -92,7 +105,7 @@ final class SwapsSetupViewModelFactory { ) } - private func emptyReceiveAssetViewModel(locale: Locale) -> EmptySwapsAssetViewModel { + private func emptyReceiveAssetViewModel() -> EmptySwapsAssetViewModel { EmptySwapsAssetViewModel( imageViewModel: StaticImageViewModel(image: R.image.iconAddSwapAmount()!), title: R.string.localizable.swapsSetupAssetReceiveTitle(preferredLanguages: locale.rLanguages), @@ -125,8 +138,7 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { func payTitleViewModel( assetDisplayInfo: AssetBalanceDisplayInfo?, - maxValue: BigUInt?, - locale: Locale + maxValue: BigUInt? ) -> TitleHorizontalMultiValueView.Model { let title = R.string.localizable.swapsSetupAssetSelectPayTitle( preferredLanguages: locale.rLanguages @@ -162,15 +174,14 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { } } - func payAssetViewModel(chainAsset: ChainAsset?, locale: Locale) -> SwapAssetInputViewModel { - chainAsset.map { .asset(assetViewModel(chainAsset: $0)) } ?? .empty(emptyPayAssetViewModel(locale: locale)) + func payAssetViewModel(chainAsset: ChainAsset?) -> SwapAssetInputViewModel { + chainAsset.map { .asset(assetViewModel(chainAsset: $0)) } ?? .empty(emptyPayAssetViewModel()) } func inputPriceViewModel( assetDisplayInfo: AssetBalanceDisplayInfo, amount: Decimal?, - priceData: PriceData?, - locale: Locale + priceData: PriceData? ) -> String? { guard let amount = amount, @@ -184,7 +195,7 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { ).value(for: locale) } - func receiveTitleViewModel(locale: Locale) -> TitleHorizontalMultiValueView.Model { + func receiveTitleViewModel() -> TitleHorizontalMultiValueView.Model { TitleHorizontalMultiValueView.Model( title: R.string.localizable.swapsSetupAssetSelectReceiveTitle(preferredLanguages: locale.rLanguages), @@ -193,11 +204,11 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { ) } - func receiveAssetViewModel(chainAsset: ChainAsset?, locale: Locale) -> SwapAssetInputViewModel { - chainAsset.map { .asset(assetViewModel(chainAsset: $0)) } ?? .empty(emptyReceiveAssetViewModel(locale: locale)) + func receiveAssetViewModel(chainAsset: ChainAsset?) -> SwapAssetInputViewModel { + chainAsset.map { .asset(assetViewModel(chainAsset: $0)) } ?? .empty(emptyReceiveAssetViewModel()) } - func rateViewModel(from params: RateParams, locale: Locale) -> String { + func rateViewModel(from params: RateParams) -> String { guard let amountOutDecimal = Decimal.fromSubstrateAmount( params.amountOut, @@ -227,8 +238,7 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { func amountInputViewModel( chainAsset: ChainAsset, - amount: Decimal?, - locale: Locale + amount: Decimal? ) -> AmountInputViewModelProtocol { balanceViewModelFactoryFacade.createBalanceInputViewModel( targetAssetInfo: chainAsset.assetDisplayInfo, @@ -239,8 +249,7 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { func feeViewModel( amount: BigUInt, assetDisplayInfo: AssetBalanceDisplayInfo, - priceData: PriceData?, - locale: Locale + priceData: PriceData? ) -> SwapFeeViewModel { let amountDecimal = Decimal.fromSubstrateAmount( amount, diff --git a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift index afd5a0e8da..74a8781c42 100644 --- a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift +++ b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift @@ -19,3 +19,8 @@ struct SwapFeeViewModel { var isEditable: Bool var balanceViewModel: BalanceViewModelProtocol } + +struct SwapPriceDifferenceViewModel { + let price: String? + let difference: DifferenceViewModel? +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 7355eddd2d..5fa628155c 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -6,9 +6,9 @@ final class SwapSetupPresenter { weak var view: SwapSetupViewProtocol? let wireframe: SwapSetupWireframeProtocol let interactor: SwapSetupInteractorInputProtocol - let viewModelFactory: SwapsSetupViewModelFactoryProtocol let dataValidatingFactory: SwapDataValidatorFactoryProtocol + private(set) var viewModelFactory: SwapsSetupViewModelFactoryProtocol private(set) var payAssetBalance: AssetBalance? private(set) var feeAssetBalance: AssetBalance? private(set) var payChainAsset: ChainAsset? @@ -64,16 +64,14 @@ final class SwapSetupPresenter { private func providePayTitle() { let payTitleViewModel = viewModelFactory.payTitleViewModel( assetDisplayInfo: payChainAsset?.assetDisplayInfo, - maxValue: payAssetBalance?.transferable, - locale: selectedLocale + maxValue: payAssetBalance?.transferable ) view?.didReceiveTitle(payViewModel: payTitleViewModel) } private func providePayAssetViewModel() { let payAssetViewModel = viewModelFactory.payAssetViewModel( - chainAsset: payChainAsset, - locale: selectedLocale + chainAsset: payChainAsset ) view?.didReceiveInputChainAsset(payViewModel: payAssetViewModel) } @@ -86,21 +84,19 @@ final class SwapSetupPresenter { let inputPriceViewModel = viewModelFactory.inputPriceViewModel( assetDisplayInfo: assetDisplayInfo, amount: getPayAmount(for: payAmountInput), - priceData: payAssetPriceData, - locale: selectedLocale + priceData: payAssetPriceData ) view?.didReceiveAmountInputPrice(payViewModel: inputPriceViewModel) } private func provideReceiveTitle() { - let receiveTitleViewModel = viewModelFactory.receiveTitleViewModel(locale: selectedLocale) + let receiveTitleViewModel = viewModelFactory.receiveTitleViewModel() view?.didReceiveTitle(receiveViewModel: receiveTitleViewModel) } private func provideReceiveAssetViewModel() { let receiveAssetViewModel = viewModelFactory.receiveAssetViewModel( - chainAsset: receiveChainAsset, - locale: selectedLocale + chainAsset: receiveChainAsset ) view?.didReceiveInputChainAsset(receiveViewModel: receiveAssetViewModel) } @@ -110,13 +106,35 @@ final class SwapSetupPresenter { view?.didReceiveAmountInputPrice(receiveViewModel: nil) return } + let inputPriceViewModel = viewModelFactory.inputPriceViewModel( assetDisplayInfo: assetDisplayInfo, amount: receiveAmountInput, - priceData: receiveAssetPriceData, - locale: selectedLocale + priceData: receiveAssetPriceData ) - view?.didReceiveAmountInputPrice(receiveViewModel: inputPriceViewModel) + + let differenceViewModel: DifferenceViewModel? + if let quote = quote, let payAssetDisplayInfo = payChainAsset?.assetDisplayInfo { + let params = RateParams( + assetDisplayInfoIn: payAssetDisplayInfo, + assetDisplayInfoOut: assetDisplayInfo, + amountIn: quote.amountIn, + amountOut: quote.amountOut + ) + + differenceViewModel = viewModelFactory.priceDifferenceViewModel( + rateParams: params, + priceIn: payAssetPriceData, + priceOut: receiveAssetPriceData + ) + } else { + differenceViewModel = nil + } + + view?.didReceiveAmountInputPrice(receiveViewModel: .init( + price: inputPriceViewModel, + difference: differenceViewModel + )) } private func providePayAmountInputViewModel() { @@ -125,8 +143,7 @@ final class SwapSetupPresenter { } let amountInputViewModel = viewModelFactory.amountInputViewModel( chainAsset: payChainAsset, - amount: getPayAmount(for: payAmountInput), - locale: selectedLocale + amount: getPayAmount(for: payAmountInput) ) view?.didReceiveAmount(payInputViewModel: amountInputViewModel) } @@ -137,8 +154,7 @@ final class SwapSetupPresenter { } let amountInputViewModel = viewModelFactory.amountInputViewModel( chainAsset: receiveChainAsset, - amount: receiveAmountInput, - locale: selectedLocale + amount: receiveAmountInput ) view?.didReceiveAmount(receiveInputViewModel: amountInputViewModel) } @@ -198,7 +214,7 @@ final class SwapSetupPresenter { assetDisplayInfoOut: assetDisplayInfoOut, amountIn: quote.amountIn, amountOut: quote.amountOut - ), locale: selectedLocale) + )) view?.didReceiveRate(viewModel: .loaded(value: rateViewModel)) } @@ -214,8 +230,7 @@ final class SwapSetupPresenter { let viewModel = viewModelFactory.feeViewModel( amount: fee, assetDisplayInfo: feeChainAsset.assetDisplayInfo, - priceData: feeAssetPriceData, - locale: selectedLocale + priceData: feeAssetPriceData ) view?.didReceiveNetworkFee(viewModel: .loaded(value: viewModel)) @@ -316,6 +331,7 @@ final class SwapSetupPresenter { if forceUpdate { receiveAmountInput = nil provideReceiveAmountInputViewModel() + provideReceiveInputPriceViewModel() } else { refreshQuote(direction: .buy) } @@ -551,6 +567,7 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { ) ?? 0 } provideReceiveAmountInputViewModel() + provideReceiveInputPriceViewModel() } provideRateViewModel() @@ -606,6 +623,7 @@ extension SwapSetupPresenter: Localizable { func applyLocalization() { if view?.isSetup == true { setup() + viewModelFactory.locale = selectedLocale } } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 442c38ee03..229ebe7295 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -9,7 +9,7 @@ protocol SwapSetupViewProtocol: ControllerBackedProtocol { func didReceiveTitle(payViewModel viewModel: TitleHorizontalMultiValueView.Model) func didReceiveInputChainAsset(receiveViewModel viewModel: SwapAssetInputViewModel) func didReceiveAmount(receiveInputViewModel inputViewModel: AmountInputViewModelProtocol) - func didReceiveAmountInputPrice(receiveViewModel: String?) + func didReceiveAmountInputPrice(receiveViewModel: SwapPriceDifferenceViewModel?) func didReceiveTitle(receiveViewModel viewModel: TitleHorizontalMultiValueView.Model) func didReceiveRate(viewModel: LoadableViewModelState) func didReceiveNetworkFee(viewModel: LoadableViewModelState) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 296d3f129e..2cb0e77297 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -209,8 +209,8 @@ extension SwapSetupViewController: SwapSetupViewProtocol { rootView.receiveAmountInputView.bind(inputViewModel: inputViewModel) } - func didReceiveAmountInputPrice(receiveViewModel viewModel: String?) { - rootView.receiveAmountInputView.bind(priceViewModel: viewModel) + func didReceiveAmountInputPrice(receiveViewModel viewModel: SwapPriceDifferenceViewModel?) { + rootView.receiveAmountInputView.bind(priceDifferenceViewModel: viewModel) } func didReceiveDetailsState(isAvailable: Bool) { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index 9dde9a5373..d5f0e85f29 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -19,7 +19,9 @@ struct SwapSetupViewFactory { let wireframe = SwapSetupWireframe(assetListObservable: assetListObservable) let viewModelFactory = SwapsSetupViewModelFactory( balanceViewModelFactoryFacade: balanceViewModelFactoryFacade, - networkViewModelFactory: NetworkViewModelFactory() + networkViewModelFactory: NetworkViewModelFactory(), + percentForamatter: NumberFormatter.percentSingle.localizableResource(), + locale: LocalizationManager.shared.selectedLocale ) let dataValidatingFactory = SwapDataValidatorFactory( presentable: wireframe, diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift b/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift index e4627878ca..6477571ac6 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift @@ -148,7 +148,42 @@ extension SwapAmountInput { func bind(priceViewModel: String?) { priceLabel.text = priceViewModel + setNeedsLayout() + } + + func bind(priceDifferenceViewModel: SwapPriceDifferenceViewModel?) { + let priceString = NSMutableAttributedString() + if let price = priceDifferenceViewModel?.price { + priceString.append(.init( + string: price, + attributes: [ + .font: UIFont.regularFootnote, + .foregroundColor: R.color.colorTextSecondary()! + ] + )) + } + if let difference = priceDifferenceViewModel?.difference { + priceString.append(.init( + string: difference.details, + attributes: [ + .font: UIFont.regularFootnote, + .foregroundColor: color(for: difference.attention) + ] + )) + } + priceLabel.attributedText = priceString setNeedsLayout() } + + private func color(for attention: AttentionState) -> UIColor { + switch attention { + case .high: + return R.color.colorTextNegative()! + case .medium: + return R.color.colorTextWarning()! + case .low: + return R.color.colorTextSecondary()! + } + } } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift index c7a91a735e..e4b1ba83a5 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift @@ -141,4 +141,9 @@ extension SwapAmountInputView { textInputView.bind(priceViewModel: priceViewModel) setNeedsLayout() } + + func bind(priceDifferenceViewModel: SwapPriceDifferenceViewModel?) { + textInputView.bind(priceDifferenceViewModel: priceDifferenceViewModel) + setNeedsLayout() + } } From 081c8a1786ff676d5cfe85a771a59975d2e62dec Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 27 Oct 2023 20:31:51 +0300 Subject: [PATCH 088/204] add space --- novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift b/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift index 6477571ac6..c8fee61ad8 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAmountInput.swift @@ -164,7 +164,7 @@ extension SwapAmountInput { } if let difference = priceDifferenceViewModel?.difference { priceString.append(.init( - string: difference.details, + string: " " + difference.details, attributes: [ .font: UIFont.regularFootnote, .foregroundColor: color(for: difference.attention) From 98d510f028528140bfed2c38b7c45c0f7f92bdfb Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Mon, 30 Oct 2023 15:00:12 +0300 Subject: [PATCH 089/204] add bottomsheet --- novawallet.xcodeproj/project.pbxproj | 24 +++++ .../SwapNetworkFeeSheetLayout.swift | 91 +++++++++++++++++++ .../SwapNetworkFeeSheetViewController.swift | 78 ++++++++++++++++ .../SwapNetworkFeeSheetViewFactory.swift | 33 +++++++ .../SwapNetworkFeeSheetViewModel.swift | 10 ++ .../Swaps/Setup/SwapSetupProtocols.swift | 4 + .../Swaps/Setup/SwapSetupWireframe.swift | 14 +++ 7 files changed, 254 insertions(+) create mode 100644 novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetLayout.swift create mode 100644 novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewController.swift create mode 100644 novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewFactory.swift create mode 100644 novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewModel.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index ca2ce4eea6..3e89ebf745 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -811,6 +811,10 @@ 77E0DC9E2A6940C400D03724 /* Calendar+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E0DC9D2A6940C400D03724 /* Calendar+Helpers.swift */; }; 77E255672A16145500B644C3 /* StakingRewardsFilterMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E255662A16145500B644C3 /* StakingRewardsFilterMapper.swift */; }; 77E255692A16148000B644C3 /* StakingRewardsFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E255682A16148000B644C3 /* StakingRewardsFilter.swift */; }; + 77E304B02AEFA261006FD6F0 /* SwapNetworkFeeSheetLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304AF2AEFA261006FD6F0 /* SwapNetworkFeeSheetLayout.swift */; }; + 77E304B22AEFC0F3006FD6F0 /* SwapNetworkFeeSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304B12AEFC0F3006FD6F0 /* SwapNetworkFeeSheetViewController.swift */; }; + 77E304B52AEFC303006FD6F0 /* SwapNetworkFeeSheetViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304B42AEFC303006FD6F0 /* SwapNetworkFeeSheetViewFactory.swift */; }; + 77E304B72AEFC348006FD6F0 /* SwapNetworkFeeSheetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304B62AEFC348006FD6F0 /* SwapNetworkFeeSheetViewModel.swift */; }; 77EA2A232A333C1500B0670B /* french_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A182A333C1500B0670B /* french_output.json */; }; 77EA2A242A333C1500B0670B /* arrays_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A192A333C1500B0670B /* arrays_output.json */; }; 77EA2A252A333C1500B0670B /* weird_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A1A2A333C1500B0670B /* weird_output.json */; }; @@ -4851,6 +4855,10 @@ 77E255652A16059A00B644C3 /* MultiassetUserDataModel9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel9.xcdatamodel; sourceTree = ""; }; 77E255662A16145500B644C3 /* StakingRewardsFilterMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardsFilterMapper.swift; sourceTree = ""; }; 77E255682A16148000B644C3 /* StakingRewardsFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardsFilter.swift; sourceTree = ""; }; + 77E304AF2AEFA261006FD6F0 /* SwapNetworkFeeSheetLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeSheetLayout.swift; sourceTree = ""; }; + 77E304B12AEFC0F3006FD6F0 /* SwapNetworkFeeSheetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeSheetViewController.swift; sourceTree = ""; }; + 77E304B42AEFC303006FD6F0 /* SwapNetworkFeeSheetViewFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeSheetViewFactory.swift; sourceTree = ""; }; + 77E304B62AEFC348006FD6F0 /* SwapNetworkFeeSheetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeSheetViewModel.swift; sourceTree = ""; }; 77EA2A182A333C1500B0670B /* french_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = french_output.json; sourceTree = ""; }; 77EA2A192A333C1500B0670B /* arrays_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = arrays_output.json; sourceTree = ""; }; 77EA2A1A2A333C1500B0670B /* weird_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = weird_output.json; sourceTree = ""; }; @@ -9907,6 +9915,17 @@ path = SelectValidatorsConfirm; sourceTree = ""; }; + 77E304B32AEFC2E2006FD6F0 /* NetworkFee */ = { + isa = PBXGroup; + children = ( + 77E304AF2AEFA261006FD6F0 /* SwapNetworkFeeSheetLayout.swift */, + 77E304B12AEFC0F3006FD6F0 /* SwapNetworkFeeSheetViewController.swift */, + 77E304B42AEFC303006FD6F0 /* SwapNetworkFeeSheetViewFactory.swift */, + 77E304B62AEFC348006FD6F0 /* SwapNetworkFeeSheetViewModel.swift */, + ); + path = NetworkFee; + sourceTree = ""; + }; 77E4088EF503B8FD414F14EA /* GovernanceUnlockConfirm */ = { isa = PBXGroup; children = ( @@ -17343,6 +17362,7 @@ 95ED25232B568F2C7DA953CC /* MessageSheet */ = { isa = PBXGroup; children = ( + 77E304B32AEFC2E2006FD6F0 /* NetworkFee */, 8465DA33298EC5D900C7CFF1 /* TitleDetails */, 8465DA32298EC5A800C7CFF1 /* Compound */, ); @@ -20133,6 +20153,7 @@ AE7129B4260872ED000AA3F5 /* EraStakersInfoChanged.swift in Sources */, 842D1E9624D2DD6700C30A7A /* MnemonicDisplayView.swift in Sources */, 8411707A285B10F5006F4DFB /* XcmAssetTransferFee.swift in Sources */, + 77E304B52AEFC303006FD6F0 /* SwapNetworkFeeSheetViewFactory.swift in Sources */, 845B07F729162AB3005785D3 /* Democracy+ConstantPath.swift in Sources */, 84F4B2372A31D2F300113DDD /* ParitySignerType.swift in Sources */, 84DBEA56265ED62700FDF73C /* BaseErrorPresentable.swift in Sources */, @@ -21821,9 +21842,11 @@ 84BB3CEE267CD6B500676FFE /* CrowdloanContributionDict.swift in Sources */, 8473F4B4282BD5A1007CC55A /* StakingRelaychainInteractor.swift in Sources */, 845B821B26EF80BC00D25C72 /* MetaAccountModel.swift in Sources */, + 77E304B02AEFA261006FD6F0 /* SwapNetworkFeeSheetLayout.swift in Sources */, 88FB7DCF295071B100784E08 /* ContainerViewController.swift in Sources */, 841E5536282CDB9E00C8438F /* StakingMainPresenterFactory+Relaychain.swift in Sources */, 8499FEC827BF73F400712589 /* StorageKeyFactory+Size.swift in Sources */, + 77E304B72AEFC348006FD6F0 /* SwapNetworkFeeSheetViewModel.swift in Sources */, 0CB0646A2A40572C00BFBA3F /* AmountInputViewModel.swift in Sources */, 847A25CA28D85204006AC9F5 /* ReferendumInfo.swift in Sources */, 5869563D0EA593FBD02C169C /* StakingPayoutConfirmationProtocols.swift in Sources */, @@ -22696,6 +22719,7 @@ 75DAB313623E900EC475E215 /* LedgerTxConfirmViewFactory.swift in Sources */, 44D9F74D7851B874F2045E7E /* LedgerInstructionsProtocols.swift in Sources */, 84AC794A297FEAFF00E284DE /* GovernanceDelegateDetails.swift in Sources */, + 77E304B22AEFC0F3006FD6F0 /* SwapNetworkFeeSheetViewController.swift in Sources */, 84715783291132B400D7D003 /* GovernanceUnlockTableViewCell.swift in Sources */, 84A5915E292B3C3D00BCCF8F /* EvmTransactionBuilder+Transfer.swift in Sources */, CD4240B756F20C338A8B3589 /* LedgerInstructionsWireframe.swift in Sources */, diff --git a/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetLayout.swift b/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetLayout.swift new file mode 100644 index 0000000000..59e05e0a0c --- /dev/null +++ b/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetLayout.swift @@ -0,0 +1,91 @@ +import UIKit + +final class SwapNetworkFeeSheetLayout: UIView { + let titleLabel: UILabel = .create { + $0.apply(style: .bottomSheetTitle) + $0.numberOfLines = 0 + } + + let detailsLabel: UILabel = .create { + $0.apply(style: .footnoteSecondary) + $0.numberOfLines = 0 + } + + let feeTypeSwitch: RoundedSegmentedControl = .create { + $0.backgroundView.fillColor = R.color.colorSegmentedBackground()! + $0.selectionColor = R.color.colorSegmentedTabActive()! + $0.titleFont = .regularFootnote + $0.selectedTitleColor = R.color.colorTextPrimary()! + $0.titleColor = R.color.colorTextSecondary()! + } + + let hint = HintView() + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = R.color.colorBottomSheetBackground() + + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + let stackView = UIView.vStack([ + titleLabel, + detailsLabel, + feeTypeSwitch, + hint + ]) + + stackView.setCustomSpacing(Constants.titleDetailsOffset, after: titleLabel) + stackView.setCustomSpacing(Constants.detailsSwitchOffset, after: detailsLabel) + stackView.setCustomSpacing(Constants.switchHintOffset, after: feeTypeSwitch) + + addSubview(stackView) + stackView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(16) + make.top.equalToSuperview().inset(Constants.topOffset) + } + } +} + +extension SwapNetworkFeeSheetLayout { + enum Constants { + static let titleDetailsOffset: CGFloat = 8 + static let detailsSwitchOffset: CGFloat = 10 + static let switchHintOffset: CGFloat = 16 + static let topOffset: CGFloat = 10 + static let bottomOffset: CGFloat = 34 + static let controlHeight: CGFloat = 32 + } +} + +extension SwapNetworkFeeSheetLayout { + func contentHeight(model: SwapNetworkFeeSheetViewModel, locale: Locale) -> CGFloat { + let titleHeight = height(for: titleLabel, with: model.title.value(for: locale)) + let messageHeight = height(for: detailsLabel, with: model.message.value(for: locale)) + let hintHeight = height(for: hint.titleLabel, with: model.hint.value(for: locale)) + + let vOffsets = Constants.topOffset + Constants.titleDetailsOffset + + Constants.detailsSwitchOffset + Constants.switchHintOffset + Constants.bottomOffset + + return vOffsets + titleHeight + messageHeight + Constants.controlHeight + hintHeight + } + + private func height(for label: UILabel, with text: String) -> CGFloat { + let width = UIScreen.main.bounds.width - UIConstants.horizontalInset * 2 + let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude) + let boundingBox = text.boundingRect( + with: constraintRect, + options: .usesLineFragmentOrigin, + attributes: [.font: label.font], + context: nil + ) + return boundingBox.height + } +} diff --git a/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewController.swift b/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewController.swift new file mode 100644 index 0000000000..bfdeb7e203 --- /dev/null +++ b/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewController.swift @@ -0,0 +1,78 @@ +import UIKit +import SoraFoundation +import SoraUI + +final class SwapNetworkFeeSheetViewController: UIViewController, ViewHolder { + typealias RootViewType = SwapNetworkFeeSheetLayout + + let presenter: MessageSheetPresenterProtocol + let viewModel: SwapNetworkFeeSheetViewModel + + var allowsSwipeDown: Bool = true + var closeOnSwipeDownClosure: (() -> Void)? + + init( + presenter: MessageSheetPresenterProtocol, + viewModel: SwapNetworkFeeSheetViewModel, + localizationManager: LocalizationManagerProtocol + ) { + self.presenter = presenter + self.viewModel = viewModel + + super.init(nibName: nil, bundle: nil) + + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = SwapNetworkFeeSheetLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupHandlers() + setupLocalization() + } + + private func setupLocalization() { + rootView.titleLabel.text = viewModel.title.value(for: selectedLocale) + rootView.detailsLabel.text = viewModel.message.value(for: selectedLocale) + rootView.hint.titleLabel.text = viewModel.hint.value(for: selectedLocale) + rootView.feeTypeSwitch.titles = (0 ..< viewModel.count).map { viewModel.sectionTitle($0).value(for: selectedLocale) } + } + + private func setupHandlers() { + rootView.feeTypeSwitch.addTarget(self, action: #selector(switchAction), for: .valueChanged) + } + + @objc private func switchAction() { + viewModel.action(rootView.feeTypeSwitch.selectedSegmentIndex) + presenter.goBack(with: nil) + } +} + +extension SwapNetworkFeeSheetViewController: MessageSheetViewProtocol {} + +extension SwapNetworkFeeSheetViewController: ModalPresenterDelegate { + func presenterShouldHide(_: ModalPresenterProtocol) -> Bool { + allowsSwipeDown + } + + func presenterDidHide(_: ModalPresenterProtocol) { + closeOnSwipeDownClosure?() + } +} + +extension SwapNetworkFeeSheetViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} diff --git a/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewFactory.swift b/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewFactory.swift new file mode 100644 index 0000000000..170a5a7640 --- /dev/null +++ b/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewFactory.swift @@ -0,0 +1,33 @@ +import Foundation +import SoraFoundation + +struct SwapNetworkFeeSheetViewFactory { + static func createView( + from viewModel: SwapNetworkFeeSheetViewModel, + allowsSwipeDown: Bool = true + ) -> MessageSheetViewProtocol { + let wireframe = MessageSheetWireframe() + + let presenter = MessageSheetPresenter(wireframe: wireframe) + + let view = SwapNetworkFeeSheetViewController( + presenter: presenter, + viewModel: viewModel, + localizationManager: LocalizationManager.shared + ) + + view.allowsSwipeDown = allowsSwipeDown + let height = view.rootView.contentHeight( + model: viewModel, + locale: LocalizationManager.shared.selectedLocale + ) + + view.preferredContentSize = .init( + width: UIView.noIntrinsicMetric, + height: height + ) + presenter.view = view + + return view + } +} diff --git a/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewModel.swift b/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewModel.swift new file mode 100644 index 0000000000..a975b7fac2 --- /dev/null +++ b/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewModel.swift @@ -0,0 +1,10 @@ +import SoraFoundation + +struct SwapNetworkFeeSheetViewModel { + let title: LocalizableResource + let message: LocalizableResource + let sectionTitle: (Int) -> LocalizableResource + let action: (Int) -> Void + let count: Int + let hint: LocalizableResource +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 30416e8d7a..592edd419b 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -73,6 +73,10 @@ protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryabl feeChainAsset: ChainAsset, slippage: BigRational ) + func showNetworkFeeAssetSelection( + form view: ControllerBackedProtocol?, + viewModel: SwapNetworkFeeSheetViewModel + ) } enum SwapSetupError: Error { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index 26504d0505..0e55710f30 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -90,4 +90,18 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { animated: true ) } + + func showNetworkFeeAssetSelection( + form view: ControllerBackedProtocol?, + viewModel: SwapNetworkFeeSheetViewModel + ) { + let bottomSheet = SwapNetworkFeeSheetViewFactory.createView(from: viewModel) + + let factory = ModalSheetPresentationFactory(configuration: ModalSheetPresentationConfiguration.nova) + + bottomSheet.controller.modalTransitioningFactory = factory + bottomSheet.controller.modalPresentationStyle = .custom + + view?.controller.present(bottomSheet.controller, animated: true) + } } From ac661c5fca6461a27b62ab41e9cdfd9a52f3d6af Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Mon, 30 Oct 2023 21:59:07 +0300 Subject: [PATCH 090/204] add feeChainAssetId to identifier --- novawallet.xcodeproj/project.pbxproj | 6 +- .../Base/Model/AssetHubFeeModelBuilder.swift | 6 +- .../Swaps/Base/SwapBaseInteractor.swift | 10 ++- .../Swaps/Base/SwapBaseProtocols.swift | 2 +- .../Swaps/Confirm/SwapConfirmPresenter.swift | 2 +- .../Model/SwapsSetupViewModelFactory.swift | 5 +- .../Swaps/Setup/Model/ViewModels.swift | 5 ++ .../SwapNetworkFeeSheetLayout.swift | 13 ++-- .../SwapNetworkFeeSheetViewController.swift | 15 ++--- .../SwapNetworkFeeSheetViewFactory.swift | 3 +- .../SwapNetworkFeeSheetViewModel.swift | 3 +- .../Swaps/Setup/SwapSetupPresenter.swift | 67 ++++++++++++++++--- .../Swaps/Setup/SwapSetupProtocols.swift | 2 +- .../Swaps/Setup/View/SwapAssetView.swift | 2 +- 14 files changed, 100 insertions(+), 41 deletions(-) rename novawallet/Modules/{MessageSheet/NetworkFee => Swaps/Setup/NetworkFeeBottomSheet}/SwapNetworkFeeSheetLayout.swift (88%) rename novawallet/Modules/{MessageSheet/NetworkFee => Swaps/Setup/NetworkFeeBottomSheet}/SwapNetworkFeeSheetViewController.swift (83%) rename novawallet/Modules/{MessageSheet/NetworkFee => Swaps/Setup/NetworkFeeBottomSheet}/SwapNetworkFeeSheetViewFactory.swift (90%) rename novawallet/Modules/{MessageSheet/NetworkFee => Swaps/Setup/NetworkFeeBottomSheet}/SwapNetworkFeeSheetViewModel.swift (78%) diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index b89e55c2d5..c4f8f4332f 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -9062,6 +9062,7 @@ 29BD7DA0076BA8BC3411221A /* Setup */ = { isa = PBXGroup; children = ( + 77E304B32AEFC2E2006FD6F0 /* NetworkFeeBottomSheet */, 77C9BCBA2ACD1AE800022EA2 /* Model */, 774091FA2ACC052400172516 /* View */, 53775773F2060B4B7F6D62DA /* SwapSetupProtocols.swift */, @@ -9934,7 +9935,7 @@ path = SelectValidatorsConfirm; sourceTree = ""; }; - 77E304B32AEFC2E2006FD6F0 /* NetworkFee */ = { + 77E304B32AEFC2E2006FD6F0 /* NetworkFeeBottomSheet */ = { isa = PBXGroup; children = ( 77E304AF2AEFA261006FD6F0 /* SwapNetworkFeeSheetLayout.swift */, @@ -9942,7 +9943,7 @@ 77E304B42AEFC303006FD6F0 /* SwapNetworkFeeSheetViewFactory.swift */, 77E304B62AEFC348006FD6F0 /* SwapNetworkFeeSheetViewModel.swift */, ); - path = NetworkFee; + path = NetworkFeeBottomSheet; sourceTree = ""; }; 77E4088EF503B8FD414F14EA /* GovernanceUnlockConfirm */ = { @@ -17382,7 +17383,6 @@ 95ED25232B568F2C7DA953CC /* MessageSheet */ = { isa = PBXGroup; children = ( - 77E304B32AEFC2E2006FD6F0 /* NetworkFee */, 8465DA33298EC5D900C7CFF1 /* TitleDetails */, 8465DA32298EC5A800C7CFF1 /* Compound */, ); diff --git a/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift b/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift index 578e2413cd..712cc119a0 100644 --- a/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift +++ b/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift @@ -1,8 +1,8 @@ import Foundation +typealias FeeChainAssetId = ChainAssetId final class AssetHubFeeModelBuilder { - typealias ResultClosure = (AssetConversion.FeeModel, AssetConversion.CallArgs) -> Void - + typealias ResultClosure = (AssetConversion.FeeModel, AssetConversion.CallArgs, FeeChainAssetId?) -> Void let utilityChainAssetId: ChainAssetId let resultClosure: ResultClosure @@ -38,7 +38,7 @@ final class AssetHubFeeModelBuilder { resultModel = feeModel } - resultClosure(resultModel, callArgs) + resultClosure(resultModel, callArgs, feeAsset?.chainAssetId) } } diff --git a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift index 02f998e6cc..9ad188a14b 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift @@ -45,8 +45,12 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB feeModelBuilder = AssetHubFeeModelBuilder( utilityChainAssetId: utilityAsset.chainAssetId - ) { [weak self] feeModel, callArgs in - self?.basePresenter?.didReceive(fee: feeModel, transactionId: callArgs.identifier) + ) { [weak self] feeModel, callArgs, feeChainAssetId in + self?.basePresenter?.didReceive( + fee: feeModel, + transactionId: callArgs.identifier, + feeChainAssetId: feeChainAssetId + ) } } @@ -128,7 +132,7 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB case let .success(feeModel): self?.feeModelBuilder?.apply(feeModel: feeModel, args: args) case let .failure(error): - self?.basePresenter?.didReceive(error: .fetchFeeFailed(error, args.identifier)) + self?.basePresenter?.didReceive(error: .fetchFeeFailed(error, args.identifier, feeAsset.chainAssetId)) } } } diff --git a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift index 217bc660fa..1c1db91081 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift @@ -9,7 +9,7 @@ protocol SwapBaseInteractorInputProtocol: AnyObject { protocol SwapBaseInteractorOutputProtocol: AnyObject { func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) - func didReceive(fee: AssetConversion.FeeModel?, transactionId: TransactionFeeId) + func didReceive(fee: AssetConversion.FeeModel?, transactionId: TransactionFeeId, feeChainAssetId: ChainAssetId?) func didReceive(error: SwapSetupError) func didReceive(price: PriceData?, priceId: AssetModel.PriceId) func didReceive(payAccountId: AccountId?) diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 07408b21e6..ce3408ed9f 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -24,7 +24,7 @@ extension SwapConfirmPresenter: SwapConfirmPresenterProtocol { extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { func didReceive(quote _: AssetConversion.Quote, for _: AssetConversion.QuoteArgs) {} - func didReceive(fee _: AssetConversion.FeeModel?, transactionId _: TransactionFeeId) {} + func didReceive(fee _: AssetConversion.FeeModel?, transactionId _: TransactionFeeId, feeChainAssetId _: FeeChainAssetId?) {} func didReceive(error _: SwapSetupError) {} diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift index 817aca234a..a91e104c0b 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift @@ -34,6 +34,7 @@ protocol SwapsSetupViewModelFactoryProtocol { func feeViewModel( amount: BigUInt, assetDisplayInfo: AssetBalanceDisplayInfo, + isEditable: Bool, priceData: PriceData?, locale: Locale ) -> SwapFeeViewModel @@ -239,6 +240,7 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { func feeViewModel( amount: BigUInt, assetDisplayInfo: AssetBalanceDisplayInfo, + isEditable: Bool, priceData: PriceData?, locale: Locale ) -> SwapFeeViewModel { @@ -252,7 +254,6 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { priceData: priceData ).value(for: locale) - // TODO: provide isEditable - return .init(isEditable: true, balanceViewModel: balanceViewModel) + return .init(isEditable: isEditable, balanceViewModel: balanceViewModel) } } diff --git a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift index afd5a0e8da..3dc465dd84 100644 --- a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift +++ b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift @@ -19,3 +19,8 @@ struct SwapFeeViewModel { var isEditable: Bool var balanceViewModel: BalanceViewModelProtocol } + +struct SwapSetupFeeIdentifier: Equatable { + let transcationId: String + let feeChainAssetId: ChainAssetId? +} diff --git a/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetLayout.swift b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetLayout.swift similarity index 88% rename from novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetLayout.swift rename to novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetLayout.swift index 59e05e0a0c..98fc003d9b 100644 --- a/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetLayout.swift +++ b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetLayout.swift @@ -17,9 +17,10 @@ final class SwapNetworkFeeSheetLayout: UIView { $0.titleFont = .regularFootnote $0.selectedTitleColor = R.color.colorTextPrimary()! $0.titleColor = R.color.colorTextSecondary()! + $0.selectionCornerRadius = 10 } - let hint = HintView() + let hint: IconDetailsView = .hint() override init(frame: CGRect) { super.init(frame: frame) @@ -51,17 +52,21 @@ final class SwapNetworkFeeSheetLayout: UIView { make.leading.trailing.equalToSuperview().inset(16) make.top.equalToSuperview().inset(Constants.topOffset) } + + feeTypeSwitch.snp.makeConstraints { make in + make.height.equalTo(Constants.controlHeight) + } } } extension SwapNetworkFeeSheetLayout { enum Constants { - static let titleDetailsOffset: CGFloat = 8 + static let titleDetailsOffset: CGFloat = 18 static let detailsSwitchOffset: CGFloat = 10 static let switchHintOffset: CGFloat = 16 static let topOffset: CGFloat = 10 static let bottomOffset: CGFloat = 34 - static let controlHeight: CGFloat = 32 + static let controlHeight: CGFloat = 40 } } @@ -69,7 +74,7 @@ extension SwapNetworkFeeSheetLayout { func contentHeight(model: SwapNetworkFeeSheetViewModel, locale: Locale) -> CGFloat { let titleHeight = height(for: titleLabel, with: model.title.value(for: locale)) let messageHeight = height(for: detailsLabel, with: model.message.value(for: locale)) - let hintHeight = height(for: hint.titleLabel, with: model.hint.value(for: locale)) + let hintHeight = height(for: hint.detailsLabel, with: model.hint.value(for: locale)) let vOffsets = Constants.topOffset + Constants.titleDetailsOffset + Constants.detailsSwitchOffset + Constants.switchHintOffset + Constants.bottomOffset diff --git a/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewController.swift b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewController.swift similarity index 83% rename from novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewController.swift rename to novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewController.swift index bfdeb7e203..dbed208d18 100644 --- a/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewController.swift +++ b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewController.swift @@ -8,9 +8,6 @@ final class SwapNetworkFeeSheetViewController: UIViewController, ViewHolder { let presenter: MessageSheetPresenterProtocol let viewModel: SwapNetworkFeeSheetViewModel - var allowsSwipeDown: Bool = true - var closeOnSwipeDownClosure: (() -> Void)? - init( presenter: MessageSheetPresenterProtocol, viewModel: SwapNetworkFeeSheetViewModel, @@ -38,13 +35,14 @@ final class SwapNetworkFeeSheetViewController: UIViewController, ViewHolder { setupHandlers() setupLocalization() + rootView.feeTypeSwitch.selectedSegmentIndex = viewModel.selectedIndex } private func setupLocalization() { rootView.titleLabel.text = viewModel.title.value(for: selectedLocale) rootView.detailsLabel.text = viewModel.message.value(for: selectedLocale) - rootView.hint.titleLabel.text = viewModel.hint.value(for: selectedLocale) - rootView.feeTypeSwitch.titles = (0 ..< viewModel.count).map { viewModel.sectionTitle($0).value(for: selectedLocale) } + rootView.hint.detailsLabel.text = viewModel.hint.value(for: selectedLocale) + rootView.feeTypeSwitch.titles = (0 ..< viewModel.count).map { viewModel.sectionTitle($0) } } private func setupHandlers() { @@ -53,7 +51,6 @@ final class SwapNetworkFeeSheetViewController: UIViewController, ViewHolder { @objc private func switchAction() { viewModel.action(rootView.feeTypeSwitch.selectedSegmentIndex) - presenter.goBack(with: nil) } } @@ -61,11 +58,7 @@ extension SwapNetworkFeeSheetViewController: MessageSheetViewProtocol {} extension SwapNetworkFeeSheetViewController: ModalPresenterDelegate { func presenterShouldHide(_: ModalPresenterProtocol) -> Bool { - allowsSwipeDown - } - - func presenterDidHide(_: ModalPresenterProtocol) { - closeOnSwipeDownClosure?() + true } } diff --git a/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewFactory.swift b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewFactory.swift similarity index 90% rename from novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewFactory.swift rename to novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewFactory.swift index 170a5a7640..5bcbc63fc0 100644 --- a/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewFactory.swift @@ -4,7 +4,7 @@ import SoraFoundation struct SwapNetworkFeeSheetViewFactory { static func createView( from viewModel: SwapNetworkFeeSheetViewModel, - allowsSwipeDown: Bool = true + allowsSwipeDown _: Bool = true ) -> MessageSheetViewProtocol { let wireframe = MessageSheetWireframe() @@ -16,7 +16,6 @@ struct SwapNetworkFeeSheetViewFactory { localizationManager: LocalizationManager.shared ) - view.allowsSwipeDown = allowsSwipeDown let height = view.rootView.contentHeight( model: viewModel, locale: LocalizationManager.shared.selectedLocale diff --git a/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewModel.swift b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewModel.swift similarity index 78% rename from novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewModel.swift rename to novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewModel.swift index a975b7fac2..c1888d17d4 100644 --- a/novawallet/Modules/MessageSheet/NetworkFee/SwapNetworkFeeSheetViewModel.swift +++ b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewModel.swift @@ -3,8 +3,9 @@ import SoraFoundation struct SwapNetworkFeeSheetViewModel { let title: LocalizableResource let message: LocalizableResource - let sectionTitle: (Int) -> LocalizableResource + let sectionTitle: (Int) -> String let action: (Int) -> Void + let selectedIndex: Int let count: Int let hint: LocalizableResource } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 06cab8891e..0fc52e973a 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -31,7 +31,7 @@ final class SwapSetupPresenter { private var slippage: BigRational? - private var feeIdentifier: String? + private var feeIdentifier: SwapSetupFeeIdentifier? private var accountId: AccountId? init( @@ -217,6 +217,7 @@ final class SwapSetupPresenter { let viewModel = viewModelFactory.feeViewModel( amount: fee, assetDisplayInfo: feeChainAsset.assetDisplayInfo, + isEditable: payChainAsset?.isUtilityAsset == false, priceData: feeAssetPriceData, locale: selectedLocale ) @@ -242,11 +243,16 @@ final class SwapSetupPresenter { slippage: slippage ) - guard args.identifier != feeIdentifier else { + let newIdentifier = SwapSetupFeeIdentifier( + transcationId: args.identifier, + feeChainAssetId: feeChainAsset?.chainAssetId + ) + + guard newIdentifier != feeIdentifier else { return } - feeIdentifier = args.identifier + feeIdentifier = newIdentifier interactor.calculateFee(args: args) } @@ -445,7 +451,46 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } // TODO: show editing fee - func showFeeActions() {} + func showFeeActions() { + guard let payChainAsset = payChainAsset, + let utilityAsset = payChainAsset.chain.utilityChainAsset() else { + return + } + + let viewModel = SwapNetworkFeeSheetViewModel( + title: .init { + R.string.localizable.commonNetworkFee(preferredLanguages: $0.rLanguages) + }, + message: .init { _ in + "Token for paying network fee" + }, + sectionTitle: { section in + section == 0 ? payChainAsset.asset.symbol : utilityAsset.asset.symbol + }, + action: { [weak self] in + if $0 == 0 { + self?.feeChainAsset = payChainAsset + self?.interactor.update(feeChainAsset: payChainAsset) + self?.estimateFee() + } else { + self?.feeChainAsset = utilityAsset + self?.interactor.update(feeChainAsset: utilityAsset) + self?.estimateFee() + } + }, + selectedIndex: + feeChainAsset?.chainAssetId == self.payChainAsset?.chainAssetId ? 0 : 1, + count: 2, + hint: .init { _ in + "Network fee is added on top of entered amount" + } + ) + + wireframe.showNetworkFeeAssetSelection( + form: view, + viewModel: viewModel + ) + } func showFeeInfo() { let title = LocalizableResource { @@ -539,8 +584,9 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in self?.refreshQuote(direction: args.direction) } - case let .fetchFeeFailed(_, id): - guard id == feeIdentifier else { + case let .fetchFeeFailed(_, id, feeChainAssetId): + let identifier = SwapSetupFeeIdentifier(transcationId: id, feeChainAssetId: feeChainAssetId) + guard identifier == feeIdentifier else { return } wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in @@ -585,8 +631,13 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { provideButtonState() } - func didReceive(fee: AssetConversion.FeeModel?, transactionId: TransactionFeeId) { - guard feeIdentifier == transactionId else { + func didReceive(fee: AssetConversion.FeeModel?, transactionId: TransactionFeeId, feeChainAssetId: FeeChainAssetId?) { + let identifier = SwapSetupFeeIdentifier( + transcationId: transactionId, + feeChainAssetId: feeChainAssetId + ) + + guard identifier == feeIdentifier else { return } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 4ae68f6355..40c2b12028 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -79,7 +79,7 @@ protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryabl enum SwapSetupError: Error { case quote(Error, AssetConversion.QuoteArgs) - case fetchFeeFailed(Error, TransactionFeeId) + case fetchFeeFailed(Error, TransactionFeeId, FeeChainAssetId?) case price(Error, AssetModel.PriceId) case assetBalance(Error, ChainAssetId, AccountId) } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAssetView.swift b/novawallet/Modules/Swaps/Setup/View/SwapAssetView.swift index 47dcb4955f..27e0a8f271 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAssetView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAssetView.swift @@ -40,7 +40,7 @@ final class SwapAssetView: GenericPairValueView Date: Mon, 30 Oct 2023 23:17:56 +0300 Subject: [PATCH 091/204] clean up --- .../Swaps/Setup/Model/ViewModels.swift | 29 +++++++++- .../SwapNetworkFeeSheetLayout.swift | 2 +- .../SwapNetworkFeeSheetViewController.swift | 8 ++- .../SwapNetworkFeeSheetViewModel.swift | 2 +- .../Swaps/Setup/SwapSetupPresenter.swift | 53 +++++++++---------- novawallet/en.lproj/Localizable.strings | 2 + novawallet/ru.lproj/Localizable.strings | 2 + 7 files changed, 64 insertions(+), 34 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift index 3dc465dd84..19a537cd60 100644 --- a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift +++ b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift @@ -1,3 +1,5 @@ +import SoraFoundation + struct SwapsAssetViewModel { let symbol: String let imageViewModel: ImageViewModelProtocol? @@ -21,6 +23,31 @@ struct SwapFeeViewModel { } struct SwapSetupFeeIdentifier: Equatable { - let transcationId: String + let transactionId: String let feeChainAssetId: ChainAssetId? } + +enum FeeSelectionViewModel: Int, CaseIterable { + case payAsset + case utilityAsset +} + +extension FeeSelectionViewModel { + static var title = LocalizableResource { + R.string.localizable.commonNetworkFee( + preferredLanguages: $0.rLanguages + ) + } + + static var message = LocalizableResource { + R.string.localizable.swapsSetupNetworkFeeTokenTitle( + preferredLanguages: $0.rLanguages + ) + } + + static var hint = LocalizableResource { + R.string.localizable.swapsSetupNetworkFeeTokenHint( + preferredLanguages: $0.rLanguages + ) + } +} diff --git a/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetLayout.swift b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetLayout.swift index 98fc003d9b..b91e0e80cd 100644 --- a/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetLayout.swift +++ b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetLayout.swift @@ -65,7 +65,7 @@ extension SwapNetworkFeeSheetLayout { static let detailsSwitchOffset: CGFloat = 10 static let switchHintOffset: CGFloat = 16 static let topOffset: CGFloat = 10 - static let bottomOffset: CGFloat = 34 + static let bottomOffset: CGFloat = 8 static let controlHeight: CGFloat = 40 } } diff --git a/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewController.swift b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewController.swift index dbed208d18..7fde440973 100644 --- a/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewController.swift +++ b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewController.swift @@ -35,14 +35,18 @@ final class SwapNetworkFeeSheetViewController: UIViewController, ViewHolder { setupHandlers() setupLocalization() - rootView.feeTypeSwitch.selectedSegmentIndex = viewModel.selectedIndex + setupSwitch() } private func setupLocalization() { rootView.titleLabel.text = viewModel.title.value(for: selectedLocale) rootView.detailsLabel.text = viewModel.message.value(for: selectedLocale) rootView.hint.detailsLabel.text = viewModel.hint.value(for: selectedLocale) - rootView.feeTypeSwitch.titles = (0 ..< viewModel.count).map { viewModel.sectionTitle($0) } + rootView.feeTypeSwitch.titles = (0 ..< viewModel.count).map { viewModel.sectionTitle($0).value(for: selectedLocale) } + } + + private func setupSwitch() { + rootView.feeTypeSwitch.selectedSegmentIndex = viewModel.selectedIndex } private func setupHandlers() { diff --git a/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewModel.swift b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewModel.swift index c1888d17d4..f1072fe1fa 100644 --- a/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewModel.swift +++ b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewModel.swift @@ -3,7 +3,7 @@ import SoraFoundation struct SwapNetworkFeeSheetViewModel { let title: LocalizableResource let message: LocalizableResource - let sectionTitle: (Int) -> String + let sectionTitle: (Int) -> LocalizableResource let action: (Int) -> Void let selectedIndex: Int let count: Int diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 0fc52e973a..ab63ba187b 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -244,7 +244,7 @@ final class SwapSetupPresenter { ) let newIdentifier = SwapSetupFeeIdentifier( - transcationId: args.identifier, + transactionId: args.identifier, feeChainAssetId: feeChainAsset?.chainAssetId ) @@ -331,7 +331,7 @@ final class SwapSetupPresenter { } } - func balanceMinusFee() -> Decimal? { + private func balanceMinusFee() -> Decimal? { guard let payChainAsset = payChainAsset else { return nil } @@ -364,7 +364,7 @@ final class SwapSetupPresenter { } } - func handlePriceError(priceId: AssetModel.PriceId) { + private func handlePriceError(priceId: AssetModel.PriceId) { wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in guard let self = self else { return @@ -375,6 +375,12 @@ final class SwapSetupPresenter { .forEach(self.interactor.remakePriceSubscription) } } + + private func updateFeeChainAsset(_ chainAsset: ChainAsset?) { + feeChainAsset = chainAsset + interactor.update(feeChainAsset: chainAsset) + estimateFee() + } } extension SwapSetupPresenter: SwapSetupPresenterProtocol { @@ -450,40 +456,29 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideButtonState() } - // TODO: show editing fee func showFeeActions() { guard let payChainAsset = payChainAsset, let utilityAsset = payChainAsset.chain.utilityChainAsset() else { return } - + let payAssetSelected = feeChainAsset?.chainAssetId == payChainAsset.chainAssetId let viewModel = SwapNetworkFeeSheetViewModel( - title: .init { - R.string.localizable.commonNetworkFee(preferredLanguages: $0.rLanguages) - }, - message: .init { _ in - "Token for paying network fee" - }, + title: FeeSelectionViewModel.title, + message: FeeSelectionViewModel.message, sectionTitle: { section in - section == 0 ? payChainAsset.asset.symbol : utilityAsset.asset.symbol + .init { _ in + FeeSelectionViewModel(rawValue: section) == .utilityAsset ? + utilityAsset.asset.symbol : payChainAsset.asset.symbol + } }, action: { [weak self] in - if $0 == 0 { - self?.feeChainAsset = payChainAsset - self?.interactor.update(feeChainAsset: payChainAsset) - self?.estimateFee() - } else { - self?.feeChainAsset = utilityAsset - self?.interactor.update(feeChainAsset: utilityAsset) - self?.estimateFee() - } + let chainAsset = FeeSelectionViewModel(rawValue: $0) == .utilityAsset ? utilityAsset : payChainAsset + self?.updateFeeChainAsset(chainAsset) }, - selectedIndex: - feeChainAsset?.chainAssetId == self.payChainAsset?.chainAssetId ? 0 : 1, - count: 2, - hint: .init { _ in - "Network fee is added on top of entered amount" - } + selectedIndex: payAssetSelected ? FeeSelectionViewModel.payAsset.rawValue : + FeeSelectionViewModel.utilityAsset.rawValue, + count: FeeSelectionViewModel.allCases.count, + hint: FeeSelectionViewModel.hint ) wireframe.showNetworkFeeAssetSelection( @@ -585,7 +580,7 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { self?.refreshQuote(direction: args.direction) } case let .fetchFeeFailed(_, id, feeChainAssetId): - let identifier = SwapSetupFeeIdentifier(transcationId: id, feeChainAssetId: feeChainAssetId) + let identifier = SwapSetupFeeIdentifier(transactionId: id, feeChainAssetId: feeChainAssetId) guard identifier == feeIdentifier else { return } @@ -633,7 +628,7 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { func didReceive(fee: AssetConversion.FeeModel?, transactionId: TransactionFeeId, feeChainAssetId: FeeChainAssetId?) { let identifier = SwapSetupFeeIdentifier( - transcationId: transactionId, + transactionId: transactionId, feeChainAssetId: feeChainAssetId ) diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 0d25d2a275..525cd179c6 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1406,3 +1406,5 @@ "common.alert.external.link.disclaimer.message" = "To continue the purchase you will be redirected from Nova Wallet app to %@"; "polkadot.staking.promotion.title" = "Boost your DOT 🚀"; "polkadot.staking.promotion.message" = "Received your DOT back from crowdloans? Start staking your DOT today to get the maximum possible rewards!"; +"swaps.setup.network.fee.token.title" = "Token for paying network fee"; +"swaps.setup.network.fee.token.hint" = "Network fee is added on top of entered amount"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 23bdf47671..0be9b7293e 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1406,3 +1406,5 @@ "common.alert.external.link.disclaimer.message" = "Для продолжения покупки вы будете перенаправлены из приложения Nova Wallet на сайт %@"; "polkadot.staking.promotion.title" = "Максимизируйте\nнаграды от DOT 🚀"; "polkadot.staking.promotion.message" = "Получили свои DOT из краудлоунов? Начните стейкать DOT уже сегодня, чтобы получить максимальные вознаграждения!"; +"swaps.setup.network.fee.token.title" = "Токен для оплаты комиссии сети"; +"swaps.setup.network.fee.token.hint" = "Комиссия сети добавляется к введенной сумме."; From ca4586b738c936b9598b9aae60fb45b83c1209bc Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 31 Oct 2023 09:04:33 +0300 Subject: [PATCH 092/204] fix inputs --- novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index ab63ba187b..f3e6dcecfd 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -346,7 +346,7 @@ final class SwapSetupPresenter { return 0 } - return balance - fee + return max(0, balance - fee) } private func handleAssetBalanceError(chainAssetId: ChainAssetId) { @@ -378,8 +378,10 @@ final class SwapSetupPresenter { private func updateFeeChainAsset(_ chainAsset: ChainAsset?) { feeChainAsset = chainAsset + providePayAssetViews() interactor.update(feeChainAsset: chainAsset) estimateFee() + refreshQuote(direction: .sell) } } From 3cd55923236d128db8f9ac934eec84b8a301f588 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 31 Oct 2023 09:23:17 +0300 Subject: [PATCH 093/204] fix fee validation --- .../Modules/Swaps/Setup/SwapSetupPresenter.swift | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index f3e6dcecfd..de16862163 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -157,19 +157,6 @@ final class SwapSetupPresenter { return input.absoluteValue(from: balanceMinusFee) } - private func getSpendingAmount() -> Decimal? { - guard let input = payAmountInput, let payChainAsset = payChainAsset else { - return nil - } - guard let transferableBalance = Decimal.fromSubstrateAmount( - payAssetBalance?.transferable ?? 0, - precision: Int16(payChainAsset.assetDisplayInfo.assetPrecision) - ) else { - return nil - } - return input.absoluteValue(from: transferableBalance) - } - private func providePayAssetViews() { providePayTitle() providePayAssetViewModel() @@ -532,7 +519,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } let validators = validators( - spendingAmount: getSpendingAmount(), + spendingAmount: getPayAmount(for: payAmountInput), payChainAsset: payChainAsset, feeChainAsset: feeChainAsset ) From 4ee98121f0327551447c5aaa1af2411f98f24480 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 31 Oct 2023 10:31:06 +0100 Subject: [PATCH 094/204] fix validation --- .../Swaps/Confirm/SwapConfirmInteractor.swift | 24 +++++++++---------- .../Swaps/Confirm/SwapConfirmPresenter.swift | 5 +++- .../Setup/SwapSetupPresenter+Validating.swift | 2 +- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift index 1ba4cbae8a..ae0e151d99 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift @@ -57,18 +57,18 @@ final class SwapConfirmInteractor: SwapBaseInteractor { let runtimeCoderFactoryOperation = runtimeService.fetchCoderFactoryOperation() runtimeCoderFactoryOperation.completionBlock = { [weak self] in - guard let self = self else { - return - } - do { - let runtimeCoderFactory = try runtimeCoderFactoryOperation.extractNoCancellableResultData() - let builder = self.assetConversionExtrinsicService.fetchExtrinsicBuilderClosure( - for: args, - codingFactory: runtimeCoderFactory - ) - self.submitClosure(extrinsicService: extrinsicService, builder: builder) - } catch { - DispatchQueue.main.async { + DispatchQueue.main.async { + guard let self = self else { + return + } + do { + let runtimeCoderFactory = try runtimeCoderFactoryOperation.extractNoCancellableResultData() + let builder = self.assetConversionExtrinsicService.fetchExtrinsicBuilderClosure( + for: args, + codingFactory: runtimeCoderFactory + ) + self.submitClosure(extrinsicService: self.extrinsicService, builder: builder) + } catch { self.presenter?.didReceive(error: .submit(error)) } } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index f5bf67db83..247fda1068 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -177,6 +177,9 @@ final class SwapConfirmPresenter { precision: Int16(initState.feeChainAsset.asset.precision) ) } ?? nil + let feeInPayAsset = initState.chainAssetIn.chainAssetId == initState.feeChainAsset.chainAssetId ? + fee?.totalFee.targetAmount : 0 + let payAssetBalance = balances[initState.chainAssetIn.chainAssetId] let validators: [DataValidating] = [ @@ -191,7 +194,7 @@ final class SwapConfirmPresenter { ), dataValidatingFactory.canPayFeeSpendingAmountInPlank( balance: payAssetBalance??.transferable, - fee: initState.chainAssetIn.chainAssetId == initState.feeChainAsset.chainAssetId ? fee?.totalFee.targetAmount : nil, + fee: feeInPayAsset, spendingAmount: spendingAmount, asset: initState.feeChainAsset.assetDisplayInfo, locale: selectedLocale diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift index 0e58a0a583..57b9217dc4 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift @@ -24,7 +24,7 @@ extension SwapSetupPresenter { ), dataValidatingFactory.canPayFeeSpendingAmountInPlank( balance: payAssetBalance?.transferable, - fee: payChainAsset.chainAssetId == feeChainAsset.chainAssetId ? fee?.totalFee.targetAmount : nil, + fee: payChainAsset.chainAssetId == feeChainAsset.chainAssetId ? fee?.totalFee.targetAmount : 0, spendingAmount: spendingAmount, asset: feeChainAsset.assetDisplayInfo, locale: selectedLocale From 64ccefb54e413b064e3bb7689a8bc739bf6dd27d Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 31 Oct 2023 13:42:22 +0300 Subject: [PATCH 095/204] add focus, fix amounts --- .../Swaps/Setup/Model/ViewModels.swift | 5 ++ .../Swaps/Setup/SwapSetupPresenter.swift | 46 +++++++++++-------- .../Swaps/Setup/SwapSetupProtocols.swift | 3 +- .../Swaps/Setup/SwapSetupViewController.swift | 22 ++++++++- 4 files changed, 56 insertions(+), 20 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift index afd5a0e8da..fb95d60d11 100644 --- a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift +++ b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift @@ -19,3 +19,8 @@ struct SwapFeeViewModel { var isEditable: Bool var balanceViewModel: BalanceViewModelProtocol } + +enum TextFieldFocus { + case payAsset + case receiveAsset +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 06cab8891e..865ed4af0e 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -12,6 +12,7 @@ final class SwapSetupPresenter { private(set) var payAssetBalance: AssetBalance? private(set) var feeAssetBalance: AssetBalance? + private(set) var receiveAssetBalance: AssetBalance? private(set) var payChainAsset: ChainAsset? private(set) var receiveChainAsset: ChainAsset? private(set) var feeChainAsset: ChainAsset? @@ -157,19 +158,6 @@ final class SwapSetupPresenter { return input.absoluteValue(from: balanceMinusFee) } - private func getSpendingAmount() -> Decimal? { - guard let input = payAmountInput, let payChainAsset = payChainAsset else { - return nil - } - guard let transferableBalance = Decimal.fromSubstrateAmount( - payAssetBalance?.transferable ?? 0, - precision: Int16(payChainAsset.assetDisplayInfo.assetPrecision) - ) else { - return nil - } - return input.absoluteValue(from: transferableBalance) - } - private func providePayAssetViews() { providePayTitle() providePayAssetViewModel() @@ -424,17 +412,36 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideButtonState() } - func swap() { + func flip(currentFocus: TextFieldFocus?) { + let payAmount = getPayAmount(for: payAmountInput) + let receiveAmount = receiveAmountInput.map { AmountInputResult.absolute($0) } + Swift.swap(&payChainAsset, &receiveChainAsset) + Swift.swap(&payAssetBalance, &receiveAssetBalance) + Swift.swap(&payAssetPriceData, &receiveAssetPriceData) + interactor.update(payChainAsset: payChainAsset) interactor.update(receiveChainAsset: receiveChainAsset) - payAmountInput = nil - receiveAmountInput = nil + let newFocus: TextFieldFocus? + + switch currentFocus { + case .payAsset: + receiveAmountInput = payAmount + payAmountInput = nil + refreshQuote(direction: .buy, forceUpdate: false) + newFocus = .receiveAsset + case .receiveAsset, .none: + payAmountInput = receiveAmount + receiveAmountInput = nil + refreshQuote(direction: .sell, forceUpdate: false) + newFocus = currentFocus == nil ? nil : .payAsset + } + providePayAssetViews() provideReceiveAssetViews() provideButtonState() provideSettingsState() - refreshQuote(direction: .sell, forceUpdate: false) + view?.didReceive(focus: newFocus) } func selectMaxPayAmount() { @@ -490,7 +497,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } let validators = validators( - spendingAmount: getSpendingAmount(), + spendingAmount: getPayAmount(for: payAmountInput), payChainAsset: payChainAsset, feeChainAsset: feeChainAsset ) @@ -627,6 +634,9 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { provideButtonState() } } + if chainAsset == receiveChainAsset?.chainAssetId { + receiveAssetBalance = balance + } } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 4e4960c9f2..48117498b5 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -15,6 +15,7 @@ protocol SwapSetupViewProtocol: ControllerBackedProtocol { func didReceiveNetworkFee(viewModel: LoadableViewModelState) func didReceiveDetailsState(isAvailable: Bool) func didReceiveSettingsState(isAvailable: Bool) + func didReceive(focus: TextFieldFocus?) } protocol SwapSetupPresenterProtocol: AnyObject { @@ -22,7 +23,7 @@ protocol SwapSetupPresenterProtocol: AnyObject { func selectPayToken() func selectReceiveToken() func proceed() - func swap() + func flip(currentFocus: TextFieldFocus?) func updatePayAmount(_ amount: Decimal?) func updateReceiveAmount(_ amount: Decimal?) func showFeeActions() diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index e2a26583a5..10db560e2a 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -127,8 +127,16 @@ final class SwapSetupViewController: UIViewController, ViewHolder { } @objc private func swapAction() { + let currentFocus: TextFieldFocus? + if rootView.payAmountInputView.textInputView.textField.isFirstResponder { + currentFocus = .payAsset + } else if rootView.receiveAmountInputView.textInputView.textField.isFirstResponder { + currentFocus = .receiveAsset + } else { + currentFocus = nil + } view.endEditing(true) - presenter.swap() + presenter.flip(currentFocus: currentFocus) } @objc private func payAmountChangeAction() { @@ -228,6 +236,18 @@ extension SwapSetupViewController: SwapSetupViewProtocol { func didReceiveSettingsState(isAvailable: Bool) { navigationItem.rightBarButtonItem?.isEnabled = isAvailable } + + func didReceive(focus: TextFieldFocus?) { + switch focus { + case .none: + rootView.payAmountInputView.textInputView.textField.resignFirstResponder() + rootView.receiveAmountInputView.textInputView.textField.resignFirstResponder() + case .payAsset: + rootView.payAmountInputView.textInputView.textField.becomeFirstResponder() + case .receiveAsset: + rootView.receiveAmountInputView.textInputView.textField.becomeFirstResponder() + } + } } extension SwapSetupViewController: Localizable { From 0ba5eb85d028740bdd6e2dcea0257c9a6717e596 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 31 Oct 2023 13:53:40 +0300 Subject: [PATCH 096/204] fix background color for segmented control --- .../Contents.json | 20 +++++++++++++++++++ .../SwapNetworkFeeSheetLayout.swift | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 novawallet/Assets.xcassets/colors/background/colorSegmentedBackgroundOnBlack.colorset/Contents.json diff --git a/novawallet/Assets.xcassets/colors/background/colorSegmentedBackgroundOnBlack.colorset/Contents.json b/novawallet/Assets.xcassets/colors/background/colorSegmentedBackgroundOnBlack.colorset/Contents.json new file mode 100644 index 0000000000..ddc7073407 --- /dev/null +++ b/novawallet/Assets.xcassets/colors/background/colorSegmentedBackgroundOnBlack.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.100", + "blue" : "0xC7", + "green" : "0x9E", + "red" : "0x99" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetLayout.swift b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetLayout.swift index b91e0e80cd..eefb395e05 100644 --- a/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetLayout.swift +++ b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetLayout.swift @@ -12,7 +12,7 @@ final class SwapNetworkFeeSheetLayout: UIView { } let feeTypeSwitch: RoundedSegmentedControl = .create { - $0.backgroundView.fillColor = R.color.colorSegmentedBackground()! + $0.backgroundView.fillColor = R.color.colorSegmentedBackgroundOnBlack()! $0.selectionColor = R.color.colorSegmentedTabActive()! $0.titleFont = .regularFootnote $0.selectedTitleColor = R.color.colorTextPrimary()! From dc56a5f25d9a7bd4199502cb87e12b33dfbdd61a Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 31 Oct 2023 14:17:54 +0300 Subject: [PATCH 097/204] fixes after merge --- .../Swaps/Confirm/SwapConfirmPresenter.swift | 28 ++++++++++++++++++- .../Swaps/Setup/Model/ViewModels.swift | 1 + .../Swaps/Setup/SwapSetupPresenter.swift | 6 +++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 3bafa3cf28..7ebc9ca88b 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -367,7 +367,7 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { } } - func didReceive(fee: AssetConversion.FeeModel?, transactionId _: TransactionFeeId) { + func didReceive(fee: AssetConversion.FeeModel?, transactionId _: TransactionFeeId, feeChainAssetId _: ChainAssetId?) { self.fee = fee provideFeeViewModel() } @@ -398,6 +398,32 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { balances[chainAsset] = balance } + func didReceive(baseError: SwapSetupError) { + switch baseError { + case let .quote(_, args): + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.calculateQuote(for: args) + } + case let .fetchFeeFailed: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.estimateFee() + } + case let .price(_, priceId): + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + guard let self = self else { + return + } + [self.initState.chainAssetIn, self.initState.chainAssetOut, self.initState.feeChainAsset] + .compactMap { $0 } + .filter { $0.asset.priceId == priceId } + .forEach(self.interactor.remakePriceSubscription) + } + case let .assetBalance: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.setup() + } + } + } func didReceive(error: SwapConfirmError) { view?.didReceiveStopLoading() diff --git a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift index b1952cfaa7..330d5fee19 100644 --- a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift +++ b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift @@ -26,6 +26,7 @@ struct SwapPriceDifferenceViewModel { let price: String? let difference: DifferenceViewModel? } + struct SwapSetupFeeIdentifier: Equatable { let transactionId: String let feeChainAssetId: ChainAssetId? diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 8ae3fe352a..44d288d182 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -612,7 +612,11 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { provideButtonState() } - func didReceive(fee: AssetConversion.FeeModel?, transactionId: TransactionFeeId, feeChainAssetId: FeeChainAssetId?) { + func didReceive( + fee: AssetConversion.FeeModel?, + transactionId: TransactionFeeId, + feeChainAssetId: FeeChainAssetId? + ) { let identifier = SwapSetupFeeIdentifier( transactionId: transactionId, feeChainAssetId: feeChainAssetId From ee44d7110f4c26106ee0e08e66f426be6eafd9c0 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 31 Oct 2023 15:11:19 +0300 Subject: [PATCH 098/204] bugfix focus for empty fields --- novawallet/Modules/Swaps/Setup/Model/ViewModels.swift | 2 ++ .../Modules/Swaps/Setup/SwapSetupPresenter.swift | 6 +++--- .../Modules/Swaps/Setup/SwapSetupViewController.swift | 8 ++++---- .../Swaps/Setup/View/SwapAmountInputView.swift | 11 +++++++++++ 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift index 778c9a4546..d82a763977 100644 --- a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift +++ b/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift @@ -19,10 +19,12 @@ struct SwapFeeViewModel { var isEditable: Bool var balanceViewModel: BalanceViewModelProtocol } + enum TextFieldFocus { case payAsset case receiveAsset } + struct SwapPriceDifferenceViewModel { let price: String? let difference: DifferenceViewModel? diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index f2f6d419fb..449c5c7ca9 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -441,16 +441,16 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { let newFocus: TextFieldFocus? switch currentFocus { - case .payAsset: + case .payAsset, .none: receiveAmountInput = payAmount payAmountInput = nil refreshQuote(direction: .buy, forceUpdate: false) newFocus = .receiveAsset - case .receiveAsset, .none: + case .receiveAsset: payAmountInput = receiveAmount receiveAmountInput = nil refreshQuote(direction: .sell, forceUpdate: false) - newFocus = currentFocus == nil ? nil : .payAsset + newFocus = .payAsset } providePayAssetViews() diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index f18119584d..56a76764d7 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -240,12 +240,12 @@ extension SwapSetupViewController: SwapSetupViewProtocol { func didReceive(focus: TextFieldFocus?) { switch focus { case .none: - rootView.payAmountInputView.textInputView.textField.resignFirstResponder() - rootView.receiveAmountInputView.textInputView.textField.resignFirstResponder() + rootView.payAmountInputView.set(focus: false) + rootView.receiveAmountInputView.set(focus: false) case .payAsset: - rootView.payAmountInputView.textInputView.textField.becomeFirstResponder() + rootView.payAmountInputView.set(focus: true) case .receiveAsset: - rootView.receiveAmountInputView.textInputView.textField.becomeFirstResponder() + rootView.receiveAmountInputView.set(focus: true) } } } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift index e4b1ba83a5..2bc2e0d73a 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift @@ -146,4 +146,15 @@ extension SwapAmountInputView { textInputView.bind(priceDifferenceViewModel: priceDifferenceViewModel) setNeedsLayout() } + + func set(focus: Bool) { + guard !textInputView.isHidden else { + return + } + if focus { + textInputView.textField.becomeFirstResponder() + } else { + textInputView.textField.resignFirstResponder() + } + } } From 2d78e7f77b4294c8101e59462cad4c567173e436 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 31 Oct 2023 15:15:45 +0300 Subject: [PATCH 099/204] clean up --- .../Modules/Swaps/Setup/SwapSetupViewController.swift | 8 ++++---- .../Modules/Swaps/Setup/View/SwapAmountInputView.swift | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 56a76764d7..90f7a8fbab 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -240,12 +240,12 @@ extension SwapSetupViewController: SwapSetupViewProtocol { func didReceive(focus: TextFieldFocus?) { switch focus { case .none: - rootView.payAmountInputView.set(focus: false) - rootView.receiveAmountInputView.set(focus: false) + rootView.payAmountInputView.set(focused: false) + rootView.receiveAmountInputView.set(focused: false) case .payAsset: - rootView.payAmountInputView.set(focus: true) + rootView.payAmountInputView.set(focused: true) case .receiveAsset: - rootView.receiveAmountInputView.set(focus: true) + rootView.receiveAmountInputView.set(focused: true) } } } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift index 2bc2e0d73a..265dbf1fbe 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift @@ -147,11 +147,11 @@ extension SwapAmountInputView { setNeedsLayout() } - func set(focus: Bool) { + func set(focused: Bool) { guard !textInputView.isHidden else { return } - if focus { + if focused { textInputView.textField.becomeFirstResponder() } else { textInputView.textField.resignFirstResponder() From ab76bec8420dacf228c5406ecd3e3c938234cac0 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 31 Oct 2023 17:13:27 +0100 Subject: [PATCH 100/204] improve swap open navigation --- novawallet.xcodeproj/project.pbxproj | 8 +- .../Helpers/SubmoduleNavigationStrategy.swift | 22 ++ .../Migration/SubstrateStorageVersion.swift | 3 + .../ChainRegistry/LocalChain/ChainModel.swift | 9 + .../EntityToModel/ChainModelMapper.swift | 5 + .../.xccurrentversion | 2 +- .../SubstrateDataModel20.xcdatamodel/contents | 193 ++++++++++++++++++ .../Storage/SubstrateDataStorageFacade.swift | 2 +- .../AssetDetails/AssetDetailsInteractor.swift | 4 + .../AssetDetails/AssetDetailsPresenter.swift | 4 + .../AssetDetails/AssetDetailsProtocols.swift | 2 + .../AssetDetailsViewController.swift | 6 + .../AssetDetailsViewFactory.swift | 8 +- .../AssetDetails/AssetDetailsWireframe.swift | 19 ++ .../AssetDetailsContainerProtocols.swift | 6 +- .../AssetDetailsContainerViewFactory.swift | 7 +- .../Model/AssetDetailsOperation.swift | 1 + .../View/AssetDetailsViewLayout.swift | 8 +- .../AssetList/AssetListPresenter.swift | 34 +-- .../AssetList/AssetListWireframe.swift | 26 ++- .../Models/AssetListBuilderResult.swift | 4 + .../View/AssetListTotalBalanceCell.swift | 2 + .../ViewModel/AssetListViewModel.swift | 1 + .../ViewModel/AssetListViewModelFactory.swift | 57 +++--- .../Buy/BuyAssetOperationWireframe.swift | 7 +- .../ReceiveAssetOperationWireframe.swift | 4 +- .../Send/SendAssetOperationWireframe.swift | 4 +- .../AssetSearch/AssetsSearchProtocols.swift | 8 +- .../AssetSearch/AssetsSearchWireframe.swift | 4 +- .../Swaps/SwapAssetOperationPresenter.swift | 12 +- .../Swaps/SwapAssetOperationWireframe.swift | 4 +- .../SwapAssetsOperationViewFactory.swift | 8 + .../Swaps/Setup/SwapSetupPresenter.swift | 6 + .../Swaps/Setup/SwapSetupViewFactory.swift | 6 +- 34 files changed, 431 insertions(+), 65 deletions(-) create mode 100644 novawallet/Common/Helpers/SubmoduleNavigationStrategy.swift create mode 100644 novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel20.xcdatamodel/contents diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index faa976e53f..0d2b1a112a 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -275,6 +275,7 @@ 0CE629DE2AA9B6BF00E250BD /* RewardDestinationViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE629DC2AA9B6BE00E250BD /* RewardDestinationViewModelFactory.swift */; }; 0CE629E02AA9B70200E250BD /* CalculatedReward.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE629DF2AA9B70200E250BD /* CalculatedReward.swift */; }; 0CE629E22AA9BE3000E250BD /* StakingSetupTypeEntityFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE629E12AA9BE3000E250BD /* StakingSetupTypeEntityFacade.swift */; }; + 0CEB4ED12AF14B8B0048FD84 /* SubmoduleNavigationStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEB4ED02AF14B8B0048FD84 /* SubmoduleNavigationStrategy.swift */; }; 0CF193D12A843DA9003F12F6 /* StakingTypeBalanceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D02A843DA9003F12F6 /* StakingTypeBalanceFactory.swift */; }; 0CF193D32A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D22A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift */; }; 0CF193D52A861926003F12F6 /* PredefinedTimeShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D42A861925003F12F6 /* PredefinedTimeShortcut.swift */; }; @@ -4325,6 +4326,8 @@ 0CE629DC2AA9B6BE00E250BD /* RewardDestinationViewModelFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RewardDestinationViewModelFactory.swift; sourceTree = ""; }; 0CE629DF2AA9B70200E250BD /* CalculatedReward.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalculatedReward.swift; sourceTree = ""; }; 0CE629E12AA9BE3000E250BD /* StakingSetupTypeEntityFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingSetupTypeEntityFacade.swift; sourceTree = ""; }; + 0CEB4ECF2AF148500048FD84 /* SubstrateDataModel20.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel20.xcdatamodel; sourceTree = ""; }; + 0CEB4ED02AF14B8B0048FD84 /* SubmoduleNavigationStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmoduleNavigationStrategy.swift; sourceTree = ""; }; 0CF193D02A843DA9003F12F6 /* StakingTypeBalanceFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StakingTypeBalanceFactory.swift; sourceTree = ""; }; 0CF193D22A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedStakingViewModelFactory.swift; sourceTree = ""; }; 0CF193D42A861925003F12F6 /* PredefinedTimeShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PredefinedTimeShortcut.swift; sourceTree = ""; }; @@ -13567,6 +13570,7 @@ 0C9525E62A7AFA2C00BD724D /* ValueResolver.swift */, 0CF193D42A861925003F12F6 /* PredefinedTimeShortcut.swift */, 0CBF5DE62AB1A60500087EBF /* SharedOperationStatus.swift */, + 0CEB4ED02AF14B8B0048FD84 /* SubmoduleNavigationStrategy.swift */, ); path = Helpers; sourceTree = ""; @@ -21216,6 +21220,7 @@ 84282298289BC50A00163031 /* SwitchAccount+ParitySignerAddConfirmWireframe.swift in Sources */, AE805FC526B3DF8B00007CE9 /* ValidatorInfoInteractorBase.swift in Sources */, 84FB9E28285C736A00B42FC0 /* XcmTransferFactory.swift in Sources */, + 0CEB4ED12AF14B8B0048FD84 /* SubmoduleNavigationStrategy.swift in Sources */, AEE5FB0526415E5D002B8FDC /* StakingRebondSetupViewFactory.swift in Sources */, AEAC68F726E9FB8400346599 /* CoingeckoOperationFactory.swift in Sources */, 843910C5253F561500E3C217 /* CompoundOperationWrapper+Result.swift in Sources */, @@ -24099,6 +24104,7 @@ 843910CA253F7E6500E3C217 /* SubstrateDataModel.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 0CEB4ECF2AF148500048FD84 /* SubstrateDataModel20.xcdatamodel */, 0C59E8C72AA5C3F4001E11F3 /* SubstrateDataModel19.xcdatamodel */, 0CC2E55C2A6AAFFD004092E7 /* SubstrateDataModel18.xcdatamodel */, 0C04290B2A67A42A00C3583A /* SubstrateDataModel17.xcdatamodel */, @@ -24119,7 +24125,7 @@ 88787F0328DB3A7B00B115AB /* SubstrateDataModel2.xcdatamodel */, 843910CB253F7E6500E3C217 /* SubstrateDataModel.xcdatamodel */, ); - currentVersion = 0C59E8C72AA5C3F4001E11F3 /* SubstrateDataModel19.xcdatamodel */; + currentVersion = 0CEB4ECF2AF148500048FD84 /* SubstrateDataModel20.xcdatamodel */; path = SubstrateDataModel.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/novawallet/Common/Helpers/SubmoduleNavigationStrategy.swift b/novawallet/Common/Helpers/SubmoduleNavigationStrategy.swift new file mode 100644 index 0000000000..4eaaa5805a --- /dev/null +++ b/novawallet/Common/Helpers/SubmoduleNavigationStrategy.swift @@ -0,0 +1,22 @@ +import Foundation + +enum SubmoduleNavigationStrategy { + typealias DismissCompletion = () -> Void + typealias DismissStart = (DismissCompletion?) -> Void + typealias Callback = () -> Void + + case callbackBeforeDismissal + case callbackAfterDismissal + + func applyStrategy(for dismissStart: DismissStart, callback: @escaping Callback) { + switch self { + case .callbackBeforeDismissal: + callback() + dismissStart(nil) + case .callbackAfterDismissal: + dismissStart { + callback() + } + } + } +} diff --git a/novawallet/Common/Migration/SubstrateStorageVersion.swift b/novawallet/Common/Migration/SubstrateStorageVersion.swift index 0e5c3f3de6..498998e6dc 100644 --- a/novawallet/Common/Migration/SubstrateStorageVersion.swift +++ b/novawallet/Common/Migration/SubstrateStorageVersion.swift @@ -18,6 +18,7 @@ enum SubstrateStorageVersion: String, CaseIterable { case version17 = "SubstrateDataModel17" case version18 = "SubstrateDataModel18" case version19 = "SubstrateDataModel19" + case version20 = "SubstrateDataModel20" static var current: SubstrateStorageVersion { allCases.last! @@ -62,6 +63,8 @@ enum SubstrateStorageVersion: String, CaseIterable { case .version18: return .version19 case .version19: + return .version20 + case .version20: return nil } } diff --git a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift index a503285130..78557280e8 100644 --- a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift +++ b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift @@ -138,6 +138,14 @@ struct ChainModel: Equatable, Codable, Hashable { options?.contains(where: { $0 == .governance }) ?? false } + var hasSwapHub: Bool { + options?.contains(where: { $0 == .swapHub }) ?? false + } + + var hasSwaps: Bool { + hasSwapHub + } + var noSubstrateRuntime: Bool { options?.contains(where: { $0 == .noSubstrateRuntime }) ?? false } @@ -224,6 +232,7 @@ enum ChainOptions: String, Codable { case governance case governanceV1 = "governance-v1" case noSubstrateRuntime + case swapHub = "swap-hub" } extension ChainModel { diff --git a/novawallet/Common/Storage/EntityToModel/ChainModelMapper.swift b/novawallet/Common/Storage/EntityToModel/ChainModelMapper.swift index bd46ab741d..db071febb3 100644 --- a/novawallet/Common/Storage/EntityToModel/ChainModelMapper.swift +++ b/novawallet/Common/Storage/EntityToModel/ChainModelMapper.swift @@ -301,6 +301,10 @@ final class ChainModelMapper { options.append(.noSubstrateRuntime) } + if entity.hasSwapHub { + options.append(.swapHub) + } + return !options.isEmpty ? options : nil } } @@ -379,6 +383,7 @@ extension ChainModelMapper: CoreDataMapperProtocol { entity.hasGovernanceV1 = model.hasGovernanceV1 entity.hasGovernance = model.hasGovernanceV2 entity.noSubstrateRuntime = model.noSubstrateRuntime + entity.hasSwapHub = model.hasSwapHub entity.order = model.order entity.nodeSwitchStrategy = model.nodeSwitchStrategy.rawValue entity.additional = try model.additional.map { diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion index 46d2cdef68..716e4bb7fc 100644 --- a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - SubstrateDataModel19.xcdatamodel + SubstrateDataModel20.xcdatamodel diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel20.xcdatamodel/contents b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel20.xcdatamodel/contents new file mode 100644 index 0000000000..7c9e246325 --- /dev/null +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel20.xcdatamodel/contents @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/novawallet/Common/Storage/SubstrateDataStorageFacade.swift b/novawallet/Common/Storage/SubstrateDataStorageFacade.swift index 6b6e13d478..a8ccd5b55f 100644 --- a/novawallet/Common/Storage/SubstrateDataStorageFacade.swift +++ b/novawallet/Common/Storage/SubstrateDataStorageFacade.swift @@ -4,7 +4,7 @@ import CoreData enum SubstrateStorageParams { static let databaseName = "SubstrateDataModel.sqlite" static let modelDirectory: String = "SubstrateDataModel.momd" - static let modelVersion: SubstrateStorageVersion = .version19 + static let modelVersion: SubstrateStorageVersion = .version20 static let storageDirectoryURL: URL = { let baseURL = FileManager.default.urls( diff --git a/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift b/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift index 10ffe0bbe4..bf3b1c28df 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift @@ -71,6 +71,10 @@ final class AssetDetailsInteractor { operations.insert(.receive) + if chainAsset.chain.hasSwaps { + operations.insert(.swap) + } + presenter?.didReceive(purchaseActions: actions) presenter?.didReceive(availableOperations: operations) } diff --git a/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift b/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift index a3042f1a2c..9586f9884a 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift @@ -195,6 +195,10 @@ extension AssetDetailsPresenter: AssetDetailsPresenterProtocol { ) wireframe.showLocks(from: view, model: model) } + + func handleSwap() { + wireframe.showSwaps(from: view, chainAsset: chainAsset) + } } extension AssetDetailsPresenter: AssetDetailsInteractorOutputProtocol { diff --git a/novawallet/Modules/AssetDetails/AssetDetailsProtocols.swift b/novawallet/Modules/AssetDetails/AssetDetailsProtocols.swift index b02c76bb10..40fb7e866a 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsProtocols.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsProtocols.swift @@ -14,6 +14,7 @@ protocol AssetDetailsPresenterProtocol: AnyObject { func handleReceive() func handleBuy() func handleLocks() + func handleSwap() } protocol AssetDetailsInteractorInputProtocol: AnyObject { @@ -40,6 +41,7 @@ protocol AssetDetailsWireframeProtocol: AnyObject, PurchasePresentable, AlertPre func showNoSigning(from view: AssetDetailsViewProtocol?) func showLedgerNotSupport(for tokenName: String, from view: AssetDetailsViewProtocol?) func showLocks(from view: AssetDetailsViewProtocol?, model: AssetDetailsLocksViewModel) + func showSwaps(from view: AssetDetailsViewProtocol?, chainAsset: ChainAsset) } enum AssetDetailsError: Error { diff --git a/novawallet/Modules/AssetDetails/AssetDetailsViewController.swift b/novawallet/Modules/AssetDetails/AssetDetailsViewController.swift index 6f83c399be..2aa2b77440 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsViewController.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsViewController.swift @@ -40,6 +40,7 @@ final class AssetDetailsViewController: UIViewController, ViewHolder { rootView.sendButton.addTarget(self, action: #selector(didTapSendButton), for: .touchUpInside) rootView.receiveButton.addTarget(self, action: #selector(didTapReceiveButton), for: .touchUpInside) rootView.buyButton.addTarget(self, action: #selector(didTapBuyButton), for: .touchUpInside) + rootView.swapButton.addTarget(self, action: #selector(didTapSwapButton), for: .touchUpInside) rootView.lockCell.addTarget(self, action: #selector(didTapLocks), for: .touchUpInside) } @@ -55,6 +56,10 @@ final class AssetDetailsViewController: UIViewController, ViewHolder { presenter.handleBuy() } + @objc private func didTapSwapButton() { + presenter.handleSwap() + } + @objc private func didTapLocks() { presenter.handleLocks() } @@ -81,6 +86,7 @@ extension AssetDetailsViewController: AssetDetailsViewProtocol { func didReceive(availableOperations: AssetDetailsOperation) { rootView.sendButton.isEnabled = availableOperations.contains(.send) rootView.receiveButton.isEnabled = availableOperations.contains(.receive) + rootView.swapButton.isEnabled = availableOperations.contains(.swap) rootView.buyButton.isEnabled = availableOperations.contains(.buy) } } diff --git a/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift b/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift index 439688da81..16591b1761 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift @@ -2,7 +2,11 @@ import Foundation import SoraFoundation struct AssetDetailsViewFactory { - static func createView(chain: ChainModel, asset: AssetModel) -> AssetDetailsViewProtocol? { + static func createView( + assetListObservable: AssetListModelObservable, + chain: ChainModel, + asset: AssetModel + ) -> AssetDetailsViewProtocol? { guard let currencyManager = CurrencyManager.shared else { return nil } @@ -19,7 +23,7 @@ struct AssetDetailsViewFactory { externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactory.shared, currencyManager: currencyManager ) - let wireframe = AssetDetailsWireframe() + let wireframe = AssetDetailsWireframe(assetListObservable: assetListObservable) let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) let viewModelFactory = AssetDetailsViewModelFactory( diff --git a/novawallet/Modules/AssetDetails/AssetDetailsWireframe.swift b/novawallet/Modules/AssetDetails/AssetDetailsWireframe.swift index 706bfcc5c8..948e904cba 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsWireframe.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsWireframe.swift @@ -4,6 +4,12 @@ import SoraUI import SoraFoundation final class AssetDetailsWireframe: AssetDetailsWireframeProtocol { + let assetListObservable: AssetListModelObservable + + init(assetListObservable: AssetListModelObservable) { + self.assetListObservable = assetListObservable + } + func showPurchaseTokens( from view: AssetDetailsViewProtocol?, action: PurchaseAction, @@ -93,6 +99,19 @@ final class AssetDetailsWireframe: AssetDetailsWireframeProtocol { present(confirmationView.controller, from: view) } + func showSwaps(from view: AssetDetailsViewProtocol?, chainAsset: ChainAsset) { + guard let swapsView = SwapSetupViewFactory.createView( + assetListObservable: assetListObservable, + payChainAsset: chainAsset + ) else { + return + } + + let navigationController = ImportantFlowViewFactory.createNavigation(from: swapsView.controller) + + view?.controller.present(navigationController, animated: true) + } + private func present(_ viewController: UIViewController, from view: AssetDetailsViewProtocol?) { guard let navigationController = view?.controller.navigationController else { return diff --git a/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerProtocols.swift b/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerProtocols.swift index 10abb8643c..146772530d 100644 --- a/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerProtocols.swift +++ b/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerProtocols.swift @@ -1,5 +1,9 @@ protocol AssetDetailsContainerViewFactoryProtocol { - static func createView(chain: ChainModel, asset: AssetModel) -> AssetDetailsContainerViewProtocol? + static func createView( + assetListObservable: AssetListModelObservable, + chain: ChainModel, + asset: AssetModel + ) -> AssetDetailsContainerViewProtocol? } protocol AssetDetailsContainerViewProtocol: ControllerBackedProtocol {} diff --git a/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift b/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift index 357341c922..f41f84fc4c 100644 --- a/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift +++ b/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift @@ -1,9 +1,14 @@ import SoraFoundation final class AssetDetailsContainerViewFactory: AssetDetailsContainerViewFactoryProtocol { - static func createView(chain: ChainModel, asset: AssetModel) -> AssetDetailsContainerViewProtocol? { + static func createView( + assetListObservable: AssetListModelObservable, + chain: ChainModel, + asset: AssetModel + ) -> AssetDetailsContainerViewProtocol? { guard let accountView = AssetDetailsViewFactory.createView( + assetListObservable: assetListObservable, chain: chain, asset: asset ), diff --git a/novawallet/Modules/AssetDetails/Model/AssetDetailsOperation.swift b/novawallet/Modules/AssetDetails/Model/AssetDetailsOperation.swift index 68abb21635..49bc2c12b4 100644 --- a/novawallet/Modules/AssetDetails/Model/AssetDetailsOperation.swift +++ b/novawallet/Modules/AssetDetails/Model/AssetDetailsOperation.swift @@ -6,4 +6,5 @@ struct AssetDetailsOperation: OptionSet { static let send = AssetDetailsOperation(rawValue: 1 << 0) static let receive = AssetDetailsOperation(rawValue: 1 << 1) static let buy = AssetDetailsOperation(rawValue: 1 << 2) + static let swap = AssetDetailsOperation(rawValue: 1 << 3) } diff --git a/novawallet/Modules/AssetDetails/View/AssetDetailsViewLayout.swift b/novawallet/Modules/AssetDetails/View/AssetDetailsViewLayout.swift index e08cbdded4..98ad858b5f 100644 --- a/novawallet/Modules/AssetDetails/View/AssetDetailsViewLayout.swift +++ b/novawallet/Modules/AssetDetails/View/AssetDetailsViewLayout.swift @@ -56,10 +56,11 @@ final class AssetDetailsViewLayout: UIView { let sendButton: RoundedButton = createOperationButton(icon: R.image.iconSend()) let receiveButton: RoundedButton = createOperationButton(icon: R.image.iconReceive()) let buyButton: RoundedButton = createOperationButton(icon: R.image.iconBuy()) + let swapButton = createOperationButton(icon: R.image.iconActionChange()) private lazy var buttonsRow = PayButtonsRow( frame: .zero, - views: [sendButton, receiveButton, buyButton] + views: [sendButton, receiveButton, swapButton, buyButton] ) private let balanceTableView: StackTableView = .create { @@ -181,6 +182,11 @@ final class AssetDetailsViewLayout: UIView { ) receiveButton.invalidateLayout() + swapButton.imageWithTitleView?.title = R.string.localizable.commonSwap( + preferredLanguages: languages + ) + swapButton.invalidateLayout() + buyButton.imageWithTitleView?.title = R.string.localizable.walletAssetBuy( preferredLanguages: languages ) diff --git a/novawallet/Modules/AssetList/AssetListPresenter.swift b/novawallet/Modules/AssetList/AssetListPresenter.swift index d4d8794412..9adba72652 100644 --- a/novawallet/Modules/AssetList/AssetListPresenter.swift +++ b/novawallet/Modules/AssetList/AssetListPresenter.swift @@ -55,12 +55,17 @@ final class AssetListPresenter { guard case let .success(priceMapping) = model.priceResult, !model.balanceResults.isEmpty else { let viewModel = viewModelFactory.createHeaderViewModel( - from: name, - walletIdenticon: walletIdenticon, - walletType: walletType, - prices: nil, - locks: nil, - walletConnectSessionsCount: walletConnectSessionsCount, + params: .init( + title: name, + wallet: .init( + walletIdenticon: walletIdenticon, + walletType: walletType, + walletConnectSessionsCount: walletConnectSessionsCount + ), + prices: nil, + locks: nil, + hasSwaps: model.hasSwaps() + ), locale: selectedLocale ) @@ -140,12 +145,17 @@ final class AssetListPresenter { let totalLocks = createHeaderLockState(from: priceMapping, externalBalances: externalBalances) let viewModel = viewModelFactory.createHeaderViewModel( - from: name, - walletIdenticon: walletIdenticon, - walletType: walletType, - prices: totalValue, - locks: totalLocks, - walletConnectSessionsCount: walletConnectSessionsCount, + params: .init( + title: name, + wallet: .init( + walletIdenticon: walletIdenticon, + walletType: walletType, + walletConnectSessionsCount: walletConnectSessionsCount + ), + prices: totalValue, + locks: totalLocks, + hasSwaps: model.hasSwaps() + ), locale: selectedLocale ) diff --git a/novawallet/Modules/AssetList/AssetListWireframe.swift b/novawallet/Modules/AssetList/AssetListWireframe.swift index de8d85ad82..65777a0fef 100644 --- a/novawallet/Modules/AssetList/AssetListWireframe.swift +++ b/novawallet/Modules/AssetList/AssetListWireframe.swift @@ -16,6 +16,7 @@ final class AssetListWireframe: AssetListWireframeProtocol { func showAssetDetails(from view: AssetListViewProtocol?, chain: ChainModel, asset: AssetModel) { guard let assetDetailsView = AssetDetailsContainerViewFactory.createView( + assetListObservable: assetListModelObservable, chain: chain, asset: asset ), @@ -128,14 +129,20 @@ final class AssetListWireframe: AssetListWireframeProtocol { } func showSwapTokens(from view: AssetListViewProtocol?) { - guard let swapTokensView = SwapSetupViewFactory.createView( - assetListObservable: assetListModelObservable + let selectClosure: (ChainAsset) -> Void = { [weak self] chainAsset in + self?.showSwapTokens(from: view, payAsset: chainAsset) + } + + guard let swapDirectionsView = SwapAssetsOperationViewFactory.createSelectPayTokenView( + for: assetListModelObservable, + selectClosureStrategy: .callbackAfterDismissal, + selectClosure: selectClosure ) else { return } let navigationController = NovaNavigationController( - rootViewController: swapTokensView.controller + rootViewController: swapDirectionsView.controller ) view?.controller.present(navigationController, animated: true, completion: nil) @@ -181,4 +188,17 @@ final class AssetListWireframe: AssetListWireframeProtocol { tabBarController.selectedIndex = MainTabBarIndex.staking } + + private func showSwapTokens(from view: AssetListViewProtocol?, payAsset: ChainAsset) { + guard let swapTokensView = SwapSetupViewFactory.createView( + assetListObservable: assetListModelObservable, + payChainAsset: payAsset + ) else { + return + } + + let navigationController = ImportantFlowViewFactory.createNavigation(from: swapTokensView.controller) + + view?.controller.present(navigationController, animated: true, completion: nil) + } } diff --git a/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift b/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift index 992c98ca31..537fff4e47 100644 --- a/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift +++ b/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift @@ -48,6 +48,10 @@ struct AssetListBuilderResult { locksResult: locksResult ) } + + func hasSwaps() -> Bool { + allChains.values.contains { $0.hasSwaps } + } } enum ChangeKind { diff --git a/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift b/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift index bc2d61b632..1e23fdc3cc 100644 --- a/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift +++ b/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift @@ -151,6 +151,8 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { setupStateWithoutLocks() startLoadingIfNeeded() } + + swapButton.isEnabled = viewModel.hasSwaps } private func totalAmountString(from model: AssetListTotalAmountViewModel) -> NSAttributedString { diff --git a/novawallet/Modules/AssetList/ViewModel/AssetListViewModel.swift b/novawallet/Modules/AssetList/ViewModel/AssetListViewModel.swift index 7c75c262f7..57fc4fb880 100644 --- a/novawallet/Modules/AssetList/ViewModel/AssetListViewModel.swift +++ b/novawallet/Modules/AssetList/ViewModel/AssetListViewModel.swift @@ -49,6 +49,7 @@ struct AssetListHeaderViewModel { let amount: LoadableViewModelState let locksAmount: String? let walletSwitch: WalletSwitchViewModel + let hasSwaps: Bool } struct AssetListTotalAmountViewModel { diff --git a/novawallet/Modules/AssetList/ViewModel/AssetListViewModelFactory.swift b/novawallet/Modules/AssetList/ViewModel/AssetListViewModelFactory.swift index 0b31d7eef7..15a9da9db6 100644 --- a/novawallet/Modules/AssetList/ViewModel/AssetListViewModelFactory.swift +++ b/novawallet/Modules/AssetList/ViewModel/AssetListViewModelFactory.swift @@ -9,16 +9,22 @@ struct AssetListAssetAccountPrice { let price: PriceData } +struct AssetListHeaderParams { + struct Wallet { + let walletIdenticon: Data? + let walletType: MetaAccountModelType + let walletConnectSessionsCount: Int + } + + let title: String + let wallet: Wallet + let prices: LoadableViewModelState<[AssetListAssetAccountPrice]>? + let locks: [AssetListAssetAccountPrice]? + let hasSwaps: Bool +} + protocol AssetListViewModelFactoryProtocol: AssetListAssetViewModelFactoryProtocol { - func createHeaderViewModel( - from title: String, - walletIdenticon: Data?, - walletType: MetaAccountModelType, - prices: LoadableViewModelState<[AssetListAssetAccountPrice]>?, - locks: [AssetListAssetAccountPrice]?, - walletConnectSessionsCount: Int, - locale: Locale - ) -> AssetListHeaderViewModel + func createHeaderViewModel(params: AssetListHeaderParams, locale: Locale) -> AssetListHeaderViewModel func createNftsViewModel(from nfts: [NftModel], locale: Locale) -> AssetListNftsViewModel } @@ -81,39 +87,36 @@ final class AssetListViewModelFactory: AssetListAssetViewModelFactory { } extension AssetListViewModelFactory: AssetListViewModelFactoryProtocol { - func createHeaderViewModel( - from title: String, - walletIdenticon: Data?, - walletType: MetaAccountModelType, - prices: LoadableViewModelState<[AssetListAssetAccountPrice]>?, - locks: [AssetListAssetAccountPrice]?, - walletConnectSessionsCount: Int, - locale: Locale - ) -> AssetListHeaderViewModel { - let icon = walletIdenticon.flatMap { try? iconGenerator.generateFromAccountId($0) } + func createHeaderViewModel(params: AssetListHeaderParams, locale: Locale) -> AssetListHeaderViewModel { + let icon = params.wallet.walletIdenticon.flatMap { try? iconGenerator.generateFromAccountId($0) } let walletSwitch = WalletSwitchViewModel( - type: WalletsListSectionViewModel.SectionType(walletType: walletType), + type: WalletsListSectionViewModel.SectionType(walletType: params.wallet.walletType), iconViewModel: icon.map { DrawableIconViewModel(icon: $0) } ) + + let walletConnectSessionsCount = params.wallet.walletConnectSessionsCount let formattedWalletConnectSessionsCount = walletConnectSessionsCount > 0 ? - quantityFormatter.value(for: locale).string(from: NSNumber(value: walletConnectSessionsCount)) : nil + quantityFormatter.value(for: locale).string(from: NSNumber(value: walletConnectSessionsCount)) : + nil - if let prices = prices { + if let prices = params.prices { let totalPrice = createTotalPrice(from: prices, locale: locale) return AssetListHeaderViewModel( walletConnectSessionsCount: formattedWalletConnectSessionsCount, - title: title, + title: params.title, amount: totalPrice, - locksAmount: locks.map { formatTotalPrice(from: $0, locale: locale) }, - walletSwitch: walletSwitch + locksAmount: params.locks.map { formatTotalPrice(from: $0, locale: locale) }, + walletSwitch: walletSwitch, + hasSwaps: params.hasSwaps ) } else { return AssetListHeaderViewModel( walletConnectSessionsCount: formattedWalletConnectSessionsCount, - title: title, + title: params.title, amount: .loading, locksAmount: nil, - walletSwitch: walletSwitch + walletSwitch: walletSwitch, + hasSwaps: params.hasSwaps ) } } diff --git a/novawallet/Modules/AssetsSearch/AssetOperation/Buy/BuyAssetOperationWireframe.swift b/novawallet/Modules/AssetsSearch/AssetOperation/Buy/BuyAssetOperationWireframe.swift index 52e0234fd7..627e8a78d3 100644 --- a/novawallet/Modules/AssetsSearch/AssetOperation/Buy/BuyAssetOperationWireframe.swift +++ b/novawallet/Modules/AssetsSearch/AssetOperation/Buy/BuyAssetOperationWireframe.swift @@ -1,12 +1,13 @@ import UIKit import SoraUI -protocol BuyAssetOperationWireframeProtocol: AssetsSearchWireframeProtocol, MessageSheetPresentable, PurchasePresentable, AlertPresentable {} +protocol BuyAssetOperationWireframeProtocol: AssetsSearchWireframeProtocol, MessageSheetPresentable, + PurchasePresentable, AlertPresentable {} final class BuyAssetOperationWireframe: BuyAssetOperationWireframeProtocol {} extension BuyAssetOperationWireframe: AssetsSearchWireframeProtocol { - func close(view: AssetsSearchViewProtocol?) { - view?.controller.presentingViewController?.dismiss(animated: true) + func close(view: AssetsSearchViewProtocol?, completion: (() -> Void)?) { + view?.controller.presentingViewController?.dismiss(animated: true, completion: completion) } } diff --git a/novawallet/Modules/AssetsSearch/AssetOperation/Receive/ReceiveAssetOperationWireframe.swift b/novawallet/Modules/AssetsSearch/AssetOperation/Receive/ReceiveAssetOperationWireframe.swift index 54f77e30de..9118657c8c 100644 --- a/novawallet/Modules/AssetsSearch/AssetOperation/Receive/ReceiveAssetOperationWireframe.swift +++ b/novawallet/Modules/AssetsSearch/AssetOperation/Receive/ReceiveAssetOperationWireframe.swift @@ -24,7 +24,7 @@ final class ReceiveAssetOperationWireframe: ReceiveAssetOperationWireframeProtoc } extension ReceiveAssetOperationWireframe: AssetsSearchWireframeProtocol { - func close(view: AssetsSearchViewProtocol?) { - view?.controller.presentingViewController?.dismiss(animated: true) + func close(view: AssetsSearchViewProtocol?, completion: (() -> Void)?) { + view?.controller.presentingViewController?.dismiss(animated: true, completion: completion) } } diff --git a/novawallet/Modules/AssetsSearch/AssetOperation/Send/SendAssetOperationWireframe.swift b/novawallet/Modules/AssetsSearch/AssetOperation/Send/SendAssetOperationWireframe.swift index e9271dadb6..16ba8bf2ce 100644 --- a/novawallet/Modules/AssetsSearch/AssetOperation/Send/SendAssetOperationWireframe.swift +++ b/novawallet/Modules/AssetsSearch/AssetOperation/Send/SendAssetOperationWireframe.swift @@ -34,7 +34,7 @@ final class SendAssetOperationWireframe: SendAssetOperationWireframeProtocol { } extension SendAssetOperationWireframe: AssetsSearchWireframeProtocol { - func close(view: AssetsSearchViewProtocol?) { - view?.controller.presentingViewController?.dismiss(animated: true) + func close(view: AssetsSearchViewProtocol?, completion: (() -> Void)?) { + view?.controller.presentingViewController?.dismiss(animated: true, completion: completion) } } diff --git a/novawallet/Modules/AssetsSearch/AssetSearch/AssetsSearchProtocols.swift b/novawallet/Modules/AssetsSearch/AssetSearch/AssetsSearchProtocols.swift index 6fda7b652c..aedbbbd565 100644 --- a/novawallet/Modules/AssetsSearch/AssetSearch/AssetsSearchProtocols.swift +++ b/novawallet/Modules/AssetsSearch/AssetSearch/AssetsSearchProtocols.swift @@ -19,7 +19,13 @@ protocol AssetsSearchInteractorOutputProtocol: AnyObject { } protocol AssetsSearchWireframeProtocol: AnyObject { - func close(view: AssetsSearchViewProtocol?) + func close(view: AssetsSearchViewProtocol?, completion: (() -> Void)?) +} + +extension AssetsSearchWireframeProtocol { + func close(view: AssetsSearchViewProtocol?) { + close(view: view, completion: nil) + } } protocol AssetsSearchDelegate: AnyObject { diff --git a/novawallet/Modules/AssetsSearch/AssetSearch/AssetsSearchWireframe.swift b/novawallet/Modules/AssetsSearch/AssetSearch/AssetsSearchWireframe.swift index 4141750a3b..f9c4f6254d 100644 --- a/novawallet/Modules/AssetsSearch/AssetSearch/AssetsSearchWireframe.swift +++ b/novawallet/Modules/AssetsSearch/AssetSearch/AssetsSearchWireframe.swift @@ -1,7 +1,7 @@ import Foundation final class AssetsSearchWireframe: AssetsSearchWireframeProtocol { - func close(view: AssetsSearchViewProtocol?) { - view?.controller.presentingViewController?.dismiss(animated: true) + func close(view: AssetsSearchViewProtocol?, completion: (() -> Void)?) { + view?.controller.presentingViewController?.dismiss(animated: true, completion: completion) } } diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationPresenter.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationPresenter.swift index cee53517d6..fa0e7dbc3f 100644 --- a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationPresenter.swift +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationPresenter.swift @@ -14,14 +14,18 @@ final class SwapAssetsOperationPresenter: AssetsSearchPresenter { let selectClosure: (ChainAsset) -> Void + let selectClosureStrategy: SubmoduleNavigationStrategy + init( selectClosure: @escaping (ChainAsset) -> Void, + selectClosureStrategy: SubmoduleNavigationStrategy, interactor: SwapAssetsOperationInteractorInputProtocol, viewModelFactory: AssetListAssetViewModelFactoryProtocol, localizationManager: LocalizationManagerProtocol, wireframe: SwapAssetsOperationWireframeProtocol ) { self.selectClosure = selectClosure + self.selectClosureStrategy = selectClosureStrategy super.init( delegate: nil, @@ -41,8 +45,12 @@ final class SwapAssetsOperationPresenter: AssetsSearchPresenter { guard let chainAsset = result?.state.chainAsset(for: chainAssetId) else { return } - selectClosure(chainAsset) - wireframe.close(view: view) + + selectClosureStrategy.applyStrategy(for: { dismissalCallback in + self.wireframe.close(view: self.view, completion: dismissalCallback) + }, callback: { + self.selectClosure(chainAsset) + }) } } diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationWireframe.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationWireframe.swift index bbfdf5be53..3f6ae7cfa6 100644 --- a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationWireframe.swift +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationWireframe.swift @@ -4,7 +4,7 @@ import SoraUI final class SwapAssetsOperationWireframe: SwapAssetsOperationWireframeProtocol {} extension SwapAssetsOperationWireframe: AssetsSearchWireframeProtocol { - func close(view: AssetsSearchViewProtocol?) { - view?.controller.presentingViewController?.dismiss(animated: true) + func close(view: AssetsSearchViewProtocol?, completion: (() -> Void)?) { + view?.controller.presentingViewController?.dismiss(animated: true, completion: completion) } } diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift index 8eaaf64f6a..814edd487e 100644 --- a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift @@ -5,6 +5,7 @@ enum SwapAssetsOperationViewFactory { static func createSelectPayTokenView( for stateObservable: AssetListModelObservable, chainAsset: ChainAsset? = nil, + selectClosureStrategy: SubmoduleNavigationStrategy = .callbackBeforeDismissal, selectClosure: @escaping (ChainAsset) -> Void ) -> AssetsSearchViewProtocol? { let title: LocalizableResource = .init { @@ -17,6 +18,7 @@ enum SwapAssetsOperationViewFactory { for: stateObservable, chainAsset: chainAsset, title: title, + selectClosureStrategy: selectClosureStrategy, selectClosure: selectClosure ) } @@ -24,6 +26,7 @@ enum SwapAssetsOperationViewFactory { static func createSelectReceiveTokenView( for stateObservable: AssetListModelObservable, chainAsset: ChainAsset? = nil, + selectClosureStrategy: SubmoduleNavigationStrategy = .callbackBeforeDismissal, selectClosure: @escaping (ChainAsset) -> Void ) -> AssetsSearchViewProtocol? { let title: LocalizableResource = .init { @@ -36,6 +39,7 @@ enum SwapAssetsOperationViewFactory { for: stateObservable, chainAsset: chainAsset, title: title, + selectClosureStrategy: selectClosureStrategy, selectClosure: selectClosure ) } @@ -44,6 +48,7 @@ enum SwapAssetsOperationViewFactory { for stateObservable: AssetListModelObservable, chainAsset: ChainAsset? = nil, title: LocalizableResource, + selectClosureStrategy: SubmoduleNavigationStrategy, selectClosure: @escaping (ChainAsset) -> Void ) -> AssetsSearchViewProtocol? { guard let currencyManager = CurrencyManager.shared else { @@ -62,6 +67,7 @@ enum SwapAssetsOperationViewFactory { stateObservable: stateObservable, viewModelFactory: viewModelFactory, chainAsset: chainAsset, + selectClosureStrategy: selectClosureStrategy, selectClosure: selectClosure ) else { return nil @@ -84,6 +90,7 @@ enum SwapAssetsOperationViewFactory { stateObservable: AssetListModelObservable, viewModelFactory: AssetListAssetViewModelFactoryProtocol, chainAsset: ChainAsset?, + selectClosureStrategy: SubmoduleNavigationStrategy, selectClosure: @escaping (ChainAsset) -> Void ) -> SwapAssetsOperationPresenter? { let westmintChainId = KnowChainId.westmint @@ -114,6 +121,7 @@ enum SwapAssetsOperationViewFactory { let presenter = SwapAssetsOperationPresenter( selectClosure: selectClosure, + selectClosureStrategy: selectClosureStrategy, interactor: interactor, viewModelFactory: viewModelFactory, localizationManager: LocalizationManager.shared, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index b8b1aad23e..78eb4f9a3c 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -35,6 +35,7 @@ final class SwapSetupPresenter { private var accountId: AccountId? init( + payChainAsset: ChainAsset?, interactor: SwapSetupInteractorInputProtocol, wireframe: SwapSetupWireframeProtocol, viewModelFactory: SwapsSetupViewModelFactoryProtocol, @@ -42,6 +43,8 @@ final class SwapSetupPresenter { localizationManager: LocalizationManagerProtocol, logger: LoggerProtocol ) { + self.payChainAsset = payChainAsset + feeChainAsset = payChainAsset?.chain.utilityChainAsset() self.interactor = interactor self.wireframe = wireframe self.viewModelFactory = viewModelFactory @@ -396,7 +399,10 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideSettingsState() // TODO: get from settings slippage = .fraction(from: AssetConversionConstants.defaultSlippage)?.fromPercents() + interactor.setup() + interactor.update(payChainAsset: payChainAsset) + interactor.update(feeChainAsset: feeChainAsset) } func selectPayToken() { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index 5e1f360e7b..795a8d4710 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -3,7 +3,10 @@ import SoraFoundation import RobinHood struct SwapSetupViewFactory { - static func createView(assetListObservable: AssetListModelObservable) -> SwapSetupViewProtocol? { + static func createView( + assetListObservable: AssetListModelObservable, + payChainAsset: ChainAsset + ) -> SwapSetupViewProtocol? { guard let currencyManager = CurrencyManager.shared else { return nil @@ -29,6 +32,7 @@ struct SwapSetupViewFactory { ) let presenter = SwapSetupPresenter( + payChainAsset: payChainAsset, interactor: interactor, wireframe: wireframe, viewModelFactory: viewModelFactory, From ff9feed00420e410d05fd0299f360aacc789a934 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 31 Oct 2023 22:25:58 +0300 Subject: [PATCH 101/204] fix cells --- novawallet/Common/View/CollapsableContainerView.swift | 9 +++++++-- .../Swaps/Base/View/SwapNetworkFeeViewCell.swift | 9 +++++++++ .../Modules/Swaps/Setup/SwapSetupViewController.swift | 4 ++-- .../Modules/Swaps/Setup/View/SwapDetailsView.swift | 10 ++++++---- .../Modules/Swaps/Setup/View/SwapSetupViewLayout.swift | 5 +++-- 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/novawallet/Common/View/CollapsableContainerView.swift b/novawallet/Common/View/CollapsableContainerView.swift index 7f7e6a4f29..477694e955 100644 --- a/novawallet/Common/View/CollapsableContainerView.swift +++ b/novawallet/Common/View/CollapsableContainerView.swift @@ -10,7 +10,6 @@ class CollapsableContainerView: UIView { private enum Constants { static let headerHeight: CGFloat = 32 static let rowHeight: CGFloat = 44 - static let contentMargins = UIEdgeInsets(top: 4, left: 16, bottom: 4, right: 16) static let stackViewBottomInset: CGFloat = 4 } @@ -44,10 +43,16 @@ class CollapsableContainerView: UIView { $0.distribution = .fill $0.alignment = .fill $0.spacing = 0.0 - $0.layoutMargins = UIEdgeInsets(top: 0.0, left: 16.0, bottom: 0.0, right: 16.0) + $0.layoutMargins = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) $0.isLayoutMarginsRelativeArrangement = true } + var contentInsets: UIEdgeInsets = .zero { + didSet { + stackView.layoutMargins = contentInsets + } + } + weak var delegate: CollapsableNetworkInfoViewDelegate? lazy var expansionAnimator: BlockViewAnimatorProtocol = BlockViewAnimator() diff --git a/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeViewCell.swift b/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeViewCell.swift index d41031812b..d60a5c5816 100644 --- a/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeViewCell.swift +++ b/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeViewCell.swift @@ -8,4 +8,13 @@ final class SwapNetworkFeeViewCell: RowView, StackTableViewC func bind(loadableViewModel: LoadableViewModelState) { rowContentView.bind(loadableViewModel: loadableViewModel) } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let pointInContentViewSpace = convert(point, to: rowContentView) + if rowContentView.valueView.frame.contains(pointInContentViewSpace) { + return valueTopButton + } else { + return super.hitTest(point, with: event) + } + } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 90f7a8fbab..a2d430e49c 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -69,7 +69,7 @@ final class SwapSetupViewController: UIViewController, ViewHolder { action: #selector(receiveAmountChangeAction), for: .editingChanged ) - rootView.rateCell.titleButton.addTarget( + rootView.rateCell.addTarget( self, action: #selector(rateInfoAction), for: .touchUpInside @@ -79,7 +79,7 @@ final class SwapSetupViewController: UIViewController, ViewHolder { action: #selector(changeNetworkFeeAction), for: .touchUpInside ) - rootView.networkFeeCell.titleButton.addTarget( + rootView.networkFeeCell.addTarget( self, action: #selector(networkFeeInfoAction), for: .touchUpInside diff --git a/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift b/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift index 022f77812d..b01e9704be 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift @@ -1,14 +1,16 @@ import UIKit final class SwapDetailsView: CollapsableContainerView { - let rateCell: SwapInfoView = .create { + let rateCell: SwapInfoViewCell = .create { $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote - $0.titleView.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() - $0.addBottomSeparator() + $0.titleButton.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() + $0.contentInsets = .init(top: 8, left: 16, bottom: 8, right: 16) } - let networkFeeCell = SwapNetworkFeeView(frame: .zero) + let networkFeeCell: SwapNetworkFeeViewCell = .create { + $0.contentInsets = .init(top: 8, left: 16, bottom: 8, right: 16) + } override var rows: [UIView] { [rateCell, networkFeeCell] diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index 084dc75683..d269a200c0 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -25,13 +25,14 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { let detailsView: SwapDetailsView = .create { $0.setExpanded(false, animated: false) + $0.contentInsets = .zero } - var rateCell: SwapInfoView { + var rateCell: SwapInfoViewCell { detailsView.rateCell } - var networkFeeCell: SwapNetworkFeeView { + var networkFeeCell: SwapNetworkFeeViewCell { detailsView.networkFeeCell } From 79ddc96b51a69869e18b1a619eea23de72319eb1 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 31 Oct 2023 22:44:41 +0300 Subject: [PATCH 102/204] add button --- .../Modules/Swaps/Setup/View/SwapSetupViewLayout.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index 084dc75683..d230d35f81 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -5,6 +5,12 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { let payAmountView = SwapSetupTitleView(frame: .zero) let payAmountInputView = SwapAmountInputView() + + let depositTokenButton: TriangularedButton = .create { + $0.applySecondaryStyle() + $0.imageWithTitleView?.titleColor = R.color.colorButtonTextAccent() + $0.isHidden = true + } let receiveAmountView: TitleHorizontalMultiValueView = .create { $0.titleView.apply(style: .footnoteSecondary) From a3a3af5edac4f485f60855cc2b8cf19643d6bcbb Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 1 Nov 2023 06:23:51 +0100 Subject: [PATCH 103/204] check pairs availability for operations --- novawallet.xcodeproj/project.pbxproj | 8 ++ .../Helpers/CancellableCallHelper.swift | 57 ++++++++++++ .../AssetConversionAggregationFactory.swift | 91 +++++++++++++++++++ .../AssetDetails/AssetDetailsInteractor.swift | 54 ++++++++++- .../AssetDetails/AssetDetailsPresenter.swift | 2 +- .../AssetDetails/AssetDetailsProtocols.swift | 1 + .../AssetDetailsViewFactory.swift | 10 ++ 7 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 novawallet/Common/Helpers/CancellableCallHelper.swift create mode 100644 novawallet/Modules/AssetConversion/Service/AssetConversionAggregationFactory.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 0d2b1a112a..f8d633ab21 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -276,6 +276,8 @@ 0CE629E02AA9B70200E250BD /* CalculatedReward.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE629DF2AA9B70200E250BD /* CalculatedReward.swift */; }; 0CE629E22AA9BE3000E250BD /* StakingSetupTypeEntityFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE629E12AA9BE3000E250BD /* StakingSetupTypeEntityFacade.swift */; }; 0CEB4ED12AF14B8B0048FD84 /* SubmoduleNavigationStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEB4ED02AF14B8B0048FD84 /* SubmoduleNavigationStrategy.swift */; }; + 0CEB4ED32AF1689D0048FD84 /* AssetConversionAggregationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEB4ED22AF1689D0048FD84 /* AssetConversionAggregationFactory.swift */; }; + 0CEB4ED52AF20EB90048FD84 /* CancellableCallHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEB4ED42AF20EB90048FD84 /* CancellableCallHelper.swift */; }; 0CF193D12A843DA9003F12F6 /* StakingTypeBalanceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D02A843DA9003F12F6 /* StakingTypeBalanceFactory.swift */; }; 0CF193D32A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D22A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift */; }; 0CF193D52A861926003F12F6 /* PredefinedTimeShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D42A861925003F12F6 /* PredefinedTimeShortcut.swift */; }; @@ -4328,6 +4330,8 @@ 0CE629E12AA9BE3000E250BD /* StakingSetupTypeEntityFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingSetupTypeEntityFacade.swift; sourceTree = ""; }; 0CEB4ECF2AF148500048FD84 /* SubstrateDataModel20.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel20.xcdatamodel; sourceTree = ""; }; 0CEB4ED02AF14B8B0048FD84 /* SubmoduleNavigationStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmoduleNavigationStrategy.swift; sourceTree = ""; }; + 0CEB4ED22AF1689D0048FD84 /* AssetConversionAggregationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionAggregationFactory.swift; sourceTree = ""; }; + 0CEB4ED42AF20EB90048FD84 /* CancellableCallHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellableCallHelper.swift; sourceTree = ""; }; 0CF193D02A843DA9003F12F6 /* StakingTypeBalanceFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StakingTypeBalanceFactory.swift; sourceTree = ""; }; 0CF193D22A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedStakingViewModelFactory.swift; sourceTree = ""; }; 0CF193D42A861925003F12F6 /* PredefinedTimeShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PredefinedTimeShortcut.swift; sourceTree = ""; }; @@ -8326,6 +8330,7 @@ 0C0CB3812AC545A800EAC516 /* AssetConversionExtrinsicService.swift */, 0CD352962ACAFADA00B3E446 /* AssetConversionOperationFactory.swift */, 0CD3A67B2AEAA3B90059BBEC /* AssetConversionFeeService.swift */, + 0CEB4ED22AF1689D0048FD84 /* AssetConversionAggregationFactory.swift */, ); path = Service; sourceTree = ""; @@ -13571,6 +13576,7 @@ 0CF193D42A861925003F12F6 /* PredefinedTimeShortcut.swift */, 0CBF5DE62AB1A60500087EBF /* SharedOperationStatus.swift */, 0CEB4ED02AF14B8B0048FD84 /* SubmoduleNavigationStrategy.swift */, + 0CEB4ED42AF20EB90048FD84 /* CancellableCallHelper.swift */, ); path = Helpers; sourceTree = ""; @@ -20077,6 +20083,7 @@ 84FB9E24285C6ACC00B42FC0 /* XcmMultiasset.swift in Sources */, 841E5547282D7F5800C8438F /* StakingMainPresenterFactory+Parachain.swift in Sources */, 84F4A90A254CC863000CF0A3 /* KeystoreExportWrapper.swift in Sources */, + 0CEB4ED52AF20EB90048FD84 /* CancellableCallHelper.swift in Sources */, 843074F928BF6201009D463B /* NoAccountSupportPresentable.swift in Sources */, 77F189402A49972300E8B933 /* UILabel+bind.swift in Sources */, 8467FCFE24E5C50B005D486C /* KeystoreImportService.swift in Sources */, @@ -21897,6 +21904,7 @@ 841E556B282EAC3600C8438F /* ParachainStakingDelegatorState.swift in Sources */, 880E40FF298CF1ED0077B18B /* VotesProtocols.swift in Sources */, 84FEF3E528089FFB0042CBE7 /* TextInputField.swift in Sources */, + 0CEB4ED32AF1689D0048FD84 /* AssetConversionAggregationFactory.swift in Sources */, 84D17EE12805A62600F7BAFF /* DAppAlertPresentable.swift in Sources */, 771901932AE2736E00D9C918 /* SwapFeeParams.swift in Sources */, A871B6ABACAE8A811010F792 /* StakingPayoutConfirmationWireframe.swift in Sources */, diff --git a/novawallet/Common/Helpers/CancellableCallHelper.swift b/novawallet/Common/Helpers/CancellableCallHelper.swift new file mode 100644 index 0000000000..3bfa08880e --- /dev/null +++ b/novawallet/Common/Helpers/CancellableCallHelper.swift @@ -0,0 +1,57 @@ +import Foundation +import RobinHood + +final class CancellableCallStore { + private var cancellableCall: CancellableCall? + + func store(call: CancellableCall) { + cancellableCall = call + } + + func clear() { + cancellableCall = nil + } + + func cancel() { + let copy = cancellableCall + cancellableCall = nil + copy?.cancel() + } + + func clearIfMatches(call: CancellableCall) -> Bool { + guard cancellableCall === call else { + return false + } + + cancellableCall = nil + + return true + } +} + +func executeCancellable( + wrapper: CompoundOperationWrapper, + inOperationQueue operationQueue: OperationQueue, + backingCallIn callStore: CancellableCallStore, + runningCallbackIn callbackQueue: DispatchQueue?, + callbackClosure: @escaping (Result) -> Void +) { + wrapper.targetOperation.completionBlock = { + dispatchInQueueWhenPossible(callbackQueue) { + guard callStore.clearIfMatches(call: wrapper) else { + return + } + + do { + let value = try wrapper.targetOperation.extractNoCancellableResultData() + callbackClosure(.success(value)) + } catch { + callbackClosure(.failure(error)) + } + } + } + + callStore.store(call: wrapper) + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) +} diff --git a/novawallet/Modules/AssetConversion/Service/AssetConversionAggregationFactory.swift b/novawallet/Modules/AssetConversion/Service/AssetConversionAggregationFactory.swift new file mode 100644 index 0000000000..4ff663058e --- /dev/null +++ b/novawallet/Modules/AssetConversion/Service/AssetConversionAggregationFactory.swift @@ -0,0 +1,91 @@ +import Foundation +import RobinHood + +protocol AssetConversionAggregationFactoryProtocol { + func createAvailableDirectionsWrapper( + for chainAsset: ChainAsset + ) -> CompoundOperationWrapper> + + func createAvailableDirectionsWrapper( + for chain: ChainModel + ) -> CompoundOperationWrapper<[ChainAssetId: Set]> +} + +enum AssetConversionAggregationFactoryError: Error { + case unavailableProvider(ChainModel) +} + +final class AssetConversionAggregationFactory { + let operationQueue: OperationQueue + let chainRegistry: ChainRegistryProtocol + + init( + chainRegistry: ChainRegistryProtocol, + operationQueue: OperationQueue + ) { + self.chainRegistry = chainRegistry + self.operationQueue = operationQueue + } + + private func createAssetHubAllDirections( + for chain: ChainModel + ) -> CompoundOperationWrapper<[ChainAssetId: Set]> { + guard let connection = chainRegistry.getConnection(for: chain.chainId) else { + return .createWithError(ChainRegistryError.connectionUnavailable) + } + + guard let runtimeService = chainRegistry.getRuntimeProvider(for: chain.chainId) else { + return .createWithError(ChainRegistryError.runtimeMetadaUnavailable) + } + + return AssetHubSwapOperationFactory( + chain: chain, + runtimeService: runtimeService, + connection: connection, + operationQueue: operationQueue + ).availableDirections() + } + + private func createAssetHubDirections(for chainAsset: ChainAsset) -> CompoundOperationWrapper> { + guard let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId) else { + return .createWithError(ChainRegistryError.connectionUnavailable) + } + + guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { + return .createWithError(ChainRegistryError.runtimeMetadaUnavailable) + } + + return AssetHubSwapOperationFactory( + chain: chainAsset.chain, + runtimeService: runtimeService, + connection: connection, + operationQueue: operationQueue + ).availableDirectionsForAsset(chainAsset.chainAssetId) + } +} + +extension AssetConversionAggregationFactory: AssetConversionAggregationFactoryProtocol { + func createAvailableDirectionsWrapper( + for chainAsset: ChainAsset + ) -> CompoundOperationWrapper> { + if chainAsset.chain.hasSwapHub { + return createAssetHubDirections(for: chainAsset) + } else { + return CompoundOperationWrapper.createWithError( + AssetConversionAggregationFactoryError.unavailableProvider(chainAsset.chain) + ) + } + } + + func createAvailableDirectionsWrapper( + for chain: ChainModel + ) -> CompoundOperationWrapper<[ChainAssetId: Set]> { + if chain.hasSwapHub { + return createAssetHubAllDirections(for: chain) + } else { + return CompoundOperationWrapper.createWithError( + AssetConversionAggregationFactoryError.unavailableProvider(chain) + ) + } + } +} diff --git a/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift b/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift index bf3b1c28df..e358285c6d 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift @@ -1,20 +1,23 @@ import UIKit import RobinHood -final class AssetDetailsInteractor { +final class AssetDetailsInteractor: AnyCancellableCleaning { weak var presenter: AssetDetailsInteractorOutputProtocol? let chainAsset: ChainAsset let selectedMetaAccount: MetaAccountModel let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol let externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactoryProtocol + let assetConvertionAggregator: AssetConversionAggregationFactoryProtocol let purchaseProvider: PurchaseProviderProtocol let assetMapper: CustomAssetMapper + let operationQueue: OperationQueue private var assetLocksSubscription: StreamableProvider? private var priceSubscription: StreamableProvider? private var assetBalanceSubscription: StreamableProvider? private var externalBalanceSubscription: StreamableProvider? + private var swapsCall = CancellableCallStore() private var accountId: AccountId? { selectedMetaAccount.fetch(for: chainAsset.chain.accountRequest())?.accountId @@ -27,6 +30,8 @@ final class AssetDetailsInteractor { walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactoryProtocol, + assetConvertionAggregator: AssetConversionAggregationFactoryProtocol, + operationQueue: OperationQueue, currencyManager: CurrencyManagerProtocol ) { self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory @@ -35,6 +40,8 @@ final class AssetDetailsInteractor { self.selectedMetaAccount = selectedMetaAccount self.chainAsset = chainAsset self.purchaseProvider = purchaseProvider + self.assetConvertionAggregator = assetConvertionAggregator + self.operationQueue = operationQueue assetMapper = CustomAssetMapper( type: chainAsset.asset.type, typeExtras: chainAsset.asset.typeExtras @@ -42,6 +49,10 @@ final class AssetDetailsInteractor { self.currencyManager = currencyManager } + deinit { + swapsCall.cancel() + } + private func subscribePrice() { if let priceId = chainAsset.asset.priceId { priceSubscription = subscribeToPrice(for: priceId, currency: selectedCurrency) @@ -50,7 +61,36 @@ final class AssetDetailsInteractor { } } - private func setAvailableOperations() { + private func fetchSwapsAndProvideOperations() { + swapsCall.cancel() + + guard chainAsset.chain.hasSwaps else { + return + } + + let wrapper = assetConvertionAggregator.createAvailableDirectionsWrapper(for: chainAsset) + + executeCancellable( + wrapper: wrapper, + inOperationQueue: operationQueue, + backingCallIn: swapsCall, + runningCallbackIn: .main + ) { [weak self] result in + guard let self = self else { + return + } + + switch result { + case let .success(directions): + let hasSwaps = !directions.isEmpty + self.setAvailableOperations(hasSwaps: hasSwaps) + case let .failure(error): + self.presenter?.didReceive(error: .swaps(error)) + } + } + } + + private func setAvailableOperations(hasSwaps: Bool) { guard let accountId = accountId else { return } @@ -71,7 +111,7 @@ final class AssetDetailsInteractor { operations.insert(.receive) - if chainAsset.chain.hasSwaps { + if hasSwaps { operations.insert(.swap) } @@ -106,7 +146,11 @@ extension AssetDetailsInteractor: AssetDetailsInteractorInputProtocol { externalBalanceSubscription = nil } - setAvailableOperations() + setAvailableOperations(hasSwaps: false) + + if chainAsset.chain.hasSwaps { + fetchSwapsAndProvideOperations() + } } } @@ -130,7 +174,7 @@ extension AssetDetailsInteractor: WalletLocalStorageSubscriber, WalletLocalSubsc accountId: accountId )) case let .failure(error): - presenter?.didReceive(error: .accountBalance(error)) + presenter?.didReceive(error: .swaps(error)) } } diff --git a/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift b/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift index 9586f9884a..75dbf29a8d 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift @@ -233,7 +233,7 @@ extension AssetDetailsPresenter: AssetDetailsInteractorOutputProtocol { } func didReceive(error: AssetDetailsError) { - logger?.error(error.localizedDescription) + logger?.error("Did receive error: \(error)") } } diff --git a/novawallet/Modules/AssetDetails/AssetDetailsProtocols.swift b/novawallet/Modules/AssetDetails/AssetDetailsProtocols.swift index 40fb7e866a..ab3c545647 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsProtocols.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsProtocols.swift @@ -49,4 +49,5 @@ enum AssetDetailsError: Error { case price(Error) case locks(Error) case externalBalances(Error) + case swaps(Error) } diff --git a/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift b/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift index 16591b1761..119db6906c 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift @@ -13,7 +13,14 @@ struct AssetDetailsViewFactory { guard let selectedAccount = SelectedWalletSettings.shared.value else { return nil } + let chainAsset = ChainAsset(chain: chain, asset: asset) + + let assetConversionAggregator = AssetConversionAggregationFactory( + chainRegistry: ChainRegistryFacade.sharedRegistry, + operationQueue: OperationManagerFacade.sharedDefaultQueue + ) + let interactor = AssetDetailsInteractor( selectedMetaAccount: selectedAccount, chainAsset: chainAsset, @@ -21,8 +28,11 @@ struct AssetDetailsViewFactory { walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactory.shared, + assetConvertionAggregator: assetConversionAggregator, + operationQueue: OperationManagerFacade.sharedDefaultQueue, currencyManager: currencyManager ) + let wireframe = AssetDetailsWireframe(assetListObservable: assetListObservable) let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) From a6e90db6700215709830be0efe302354a6845683 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 1 Nov 2023 07:14:33 +0100 Subject: [PATCH 104/204] load all available pairs --- .../Helpers/CancellableCallHelper.swift | 6 +- .../Model/AssetSearchBuilder.swift | 10 ++ .../Swaps/SwapAssetOperationPresenter.swift | 10 +- .../Swaps/SwapAssetsOperationInteractor.swift | 142 ++++++++++++------ .../SwapAssetsOperationViewFactory.swift | 24 +-- 5 files changed, 131 insertions(+), 61 deletions(-) diff --git a/novawallet/Common/Helpers/CancellableCallHelper.swift b/novawallet/Common/Helpers/CancellableCallHelper.swift index 3bfa08880e..87a72282de 100644 --- a/novawallet/Common/Helpers/CancellableCallHelper.swift +++ b/novawallet/Common/Helpers/CancellableCallHelper.swift @@ -19,7 +19,7 @@ final class CancellableCallStore { } func clearIfMatches(call: CancellableCall) -> Bool { - guard cancellableCall === call else { + guard matches(call: call) else { return false } @@ -27,6 +27,10 @@ final class CancellableCallStore { return true } + + func matches(call: CancellableCall) -> Bool { + cancellableCall === call + } } func executeCancellable( diff --git a/novawallet/Modules/AssetsSearch/Model/AssetSearchBuilder.swift b/novawallet/Modules/AssetsSearch/Model/AssetSearchBuilder.swift index 351c2f57b2..8bef5b7fd0 100644 --- a/novawallet/Modules/AssetsSearch/Model/AssetSearchBuilder.swift +++ b/novawallet/Modules/AssetsSearch/Model/AssetSearchBuilder.swift @@ -213,4 +213,14 @@ extension AssetSearchBuilder { self.rebuildResult(for: self.query, filter: self.filter) } } + + func reload() { + workingQueue.async { [weak self] in + guard let self = self else { + return + } + + self.rebuildResult(for: self.query, filter: self.filter) + } + } } diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationPresenter.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationPresenter.swift index fa0e7dbc3f..f592751951 100644 --- a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationPresenter.swift +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationPresenter.swift @@ -16,16 +16,20 @@ final class SwapAssetsOperationPresenter: AssetsSearchPresenter { let selectClosureStrategy: SubmoduleNavigationStrategy + let logger: LoggerProtocol + init( selectClosure: @escaping (ChainAsset) -> Void, selectClosureStrategy: SubmoduleNavigationStrategy, interactor: SwapAssetsOperationInteractorInputProtocol, viewModelFactory: AssetListAssetViewModelFactoryProtocol, localizationManager: LocalizationManagerProtocol, - wireframe: SwapAssetsOperationWireframeProtocol + wireframe: SwapAssetsOperationWireframeProtocol, + logger: Logger ) { self.selectClosure = selectClosure self.selectClosureStrategy = selectClosureStrategy + self.logger = logger super.init( delegate: nil, @@ -59,7 +63,9 @@ extension SwapAssetsOperationPresenter: SwapAssetsOperationPresenterProtocol { swapAssetsView?.didStopLoading() } - func didReceive(error _: SwapAssetsOperationError) { + func didReceive(error: SwapAssetsOperationError) { + logger.error("Did receive error: \(error)") + swapAssetsWireframe?.presentRequestStatus( on: swapAssetsView, locale: selectedLocale, diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift index 16098fcd58..7c2158d028 100644 --- a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift @@ -7,76 +7,132 @@ final class SwapAssetsOperationInteractor: AnyCancellableCleaning { let stateObservable: AssetListModelObservable let logger: LoggerProtocol let chainAsset: ChainAsset? - let assetConversionOperationFactory: AssetConversionOperationFactoryProtocol + let assetConversionAggregation: AssetConversionAggregationFactoryProtocol private let operationQueue: OperationQueue private var builder: AssetSearchBuilder? - private var directionsCall: CancellableCall? - private var availableDirections: [ChainAssetId: Set]? + private var directionsCall = CancellableCallStore() + private var availableDirections: [ChainAssetId: Set] = [:] + private var availableChains: Set = [] init( stateObservable: AssetListModelObservable, chainAsset: ChainAsset?, - assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, - logger: LoggerProtocol, - operationQueue: OperationQueue + assetConversionAggregation: AssetConversionAggregationFactoryProtocol, + operationQueue: OperationQueue, + logger: LoggerProtocol ) { self.stateObservable = stateObservable self.logger = logger self.chainAsset = chainAsset - self.assetConversionOperationFactory = assetConversionOperationFactory + self.assetConversionAggregation = assetConversionAggregation self.operationQueue = operationQueue } - private func loadDirections() { + deinit { + directionsCall.cancel() + } + + private func reloadDirectionsIfNeeded() { if let chainAsset = chainAsset { - loadDirections(for: chainAsset) + guard !availableChains.contains(chainAsset.chain.chainId), chainAsset.chain.hasSwaps else { + presenter?.directionsLoaded() + return + } + + availableChains.insert(chainAsset.chain.chainId) + availableDirections = [:] + loadAssetDirections(for: chainAsset) } else { - loadAllDirections() + let allChains = stateObservable.state.value.allChains.values + + let chainsWithSwaps = allChains.filter(\.hasSwaps) + let chainsWithSwapsIds = Set(chainsWithSwaps.map(\.chainId)) + + if chainsWithSwapsIds != availableChains { + availableChains = chainsWithSwapsIds + availableDirections = [:] + + loadDirections(for: chainsWithSwaps) + } else { + presenter?.directionsLoaded() + } } } - private func loadAllDirections() { - let wrapper = assetConversionOperationFactory.availableDirections() - loadDirections(wrapper: wrapper, mapper: { $0 }) - } + private func loadDirections(for chains: [ChainModel]) { + directionsCall.cancel() + + let wrappers = chains.map { assetConversionAggregation.createAvailableDirectionsWrapper(for: $0) } - private func loadDirections(for chainAsset: ChainAsset) { - let wrapper = assetConversionOperationFactory.availableDirectionsForAsset(chainAsset.chainAssetId) - loadDirections(wrapper: wrapper) { - var result = [ChainAssetId: Set]() - result[chainAsset.chainAssetId] = $0 - return result + let dependencies = wrappers.flatMap(\.allOperations) + + let mergingOperation = ClosureOperation { + try wrappers.forEach { _ = try $0.targetOperation.extractNoCancellableResultData() } } - } - private func loadDirections( - wrapper: CompoundOperationWrapper, - mapper: @escaping (Result) -> [ChainAssetId: Set] - ) { - clear(cancellable: &directionsCall) + dependencies.forEach { + mergingOperation.addDependency($0) + } - wrapper.targetOperation.completionBlock = { [weak self] in - guard self?.directionsCall === wrapper else { - return - } + let commonWrapper = CompoundOperationWrapper(targetOperation: mergingOperation, dependencies: dependencies) - self?.directionsCall = nil + wrappers.forEach { wrapper in + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard let self = self else { + return + } - DispatchQueue.main.async { - do { - let result = try wrapper.targetOperation.extractNoCancellableResultData() - self?.availableDirections = mapper(result) - self?.createBuilder() - self?.presenter?.directionsLoaded() - } catch { - self?.presenter?.didReceive(error: .directions(error)) + if case let .success(directions) = wrapper.targetOperation.result { + self.updateAvailableDirections(directions) + } } } } - directionsCall = wrapper - operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + executeCancellable( + wrapper: commonWrapper, + inOperationQueue: operationQueue, + backingCallIn: directionsCall, + runningCallbackIn: .main + ) { [weak self] result in + switch result { + case .success: + self?.presenter?.directionsLoaded() + case let .failure(error): + self?.presenter?.didReceive(error: .directions(error)) + } + } + } + + private func loadAssetDirections(for chainAsset: ChainAsset) { + directionsCall.cancel() + + let wrapper = assetConversionAggregation.createAvailableDirectionsWrapper(for: chainAsset) + + executeCancellable( + wrapper: wrapper, + inOperationQueue: operationQueue, + backingCallIn: directionsCall, + runningCallbackIn: .main + ) { [weak self] result in + switch result { + case let .success(directions): + self?.updateAvailableDirections([chainAsset.chainAssetId: directions]) + self?.presenter?.directionsLoaded() + case let .failure(error): + self?.presenter?.didReceive(error: .directions(error)) + } + } + } + + private func updateAvailableDirections(_ newDirections: [ChainAssetId: Set]) { + availableDirections = newDirections.reduce(into: availableDirections) { accum, keyValue in + accum[keyValue.key] = keyValue.value + } + + builder?.reload() } private func createBuilder() { @@ -108,13 +164,15 @@ final class SwapAssetsOperationInteractor: AnyCancellableCleaning { return } self.builder?.apply(model: newState.value) + self.reloadDirectionsIfNeeded() } } } extension SwapAssetsOperationInteractor: SwapAssetsOperationInteractorInputProtocol { func setup() { - loadDirections() + createBuilder() + reloadDirectionsIfNeeded() } func search(query: String) { diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift index 814edd487e..e77a9992d0 100644 --- a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift @@ -93,30 +93,21 @@ enum SwapAssetsOperationViewFactory { selectClosureStrategy: SubmoduleNavigationStrategy, selectClosure: @escaping (ChainAsset) -> Void ) -> SwapAssetsOperationPresenter? { - let westmintChainId = KnowChainId.westmint let chainRegistry = ChainRegistryFacade.sharedRegistry - guard let connection = chainRegistry.getConnection(for: westmintChainId), - let runtimeService = chainRegistry.getRuntimeProvider(for: westmintChainId), - let chainModel = chainRegistry.getChain(for: westmintChainId) else { - return nil - } - let operationQueue = OperationManagerFacade.sharedDefaultQueue - let assetConversionOperationFactory = AssetHubSwapOperationFactory( - chain: chainModel, - runtimeService: runtimeService, - connection: connection, - operationQueue: operationQueue + let assetConversionAggregator = AssetConversionAggregationFactory( + chainRegistry: chainRegistry, + operationQueue: OperationManagerFacade.sharedDefaultQueue ) let interactor = SwapAssetsOperationInteractor( stateObservable: stateObservable, chainAsset: chainAsset, - assetConversionOperationFactory: assetConversionOperationFactory, - logger: Logger.shared, - operationQueue: operationQueue + assetConversionAggregation: assetConversionAggregator, + operationQueue: operationQueue, + logger: Logger.shared ) let presenter = SwapAssetsOperationPresenter( @@ -125,7 +116,8 @@ enum SwapAssetsOperationViewFactory { interactor: interactor, viewModelFactory: viewModelFactory, localizationManager: LocalizationManager.shared, - wireframe: SwapAssetsOperationWireframe() + wireframe: SwapAssetsOperationWireframe(), + logger: Logger.shared ) interactor.presenter = presenter From 474c5cb651d3872092775aab4d035ccb4a4a7d53 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 1 Nov 2023 11:06:32 +0300 Subject: [PATCH 105/204] init --- novawallet.xcodeproj/project.pbxproj | 16 ++++++++++------ .../Swaps/SwapAssetOperationPresenter.swift | 16 ++++++++++++---- .../Swaps/SwapAssetsOperationInteractor.swift | 17 +++++++++++++++-- .../Swaps/SwapAssetsOperationProtocols.swift | 2 +- .../Swaps/SwapAssetsOperationViewFactory.swift | 8 ++++---- .../Swaps/SwapSelectedChainAsset.swift | 4 ++++ .../Swaps/Setup/SwapSetupPresenter.swift | 17 ++++++++++++----- .../Swaps/Setup/SwapSetupProtocols.swift | 4 ++-- .../Swaps/Setup/SwapSetupWireframe.swift | 4 ++-- 9 files changed, 62 insertions(+), 26 deletions(-) create mode 100644 novawallet/Modules/AssetsSearch/Swaps/SwapSelectedChainAsset.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index cbc47efaf3..a92e36b21e 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -797,6 +797,7 @@ 77AB55592AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */; }; 77AB555B2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */; }; 77AB555D2AA24BA90058814E /* OperationDetailsPoolRewardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */; }; + 77C9761E2AF220130049272C /* SwapSelectedChainAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9761D2AF220130049272C /* SwapSelectedChainAsset.swift */; }; 77C9BCBC2ACD1AF500022EA2 /* ViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */; }; 77C9BCBE2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */; }; 77C9BCC42ACD570100022EA2 /* SwapAssetsOperationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */; }; @@ -818,13 +819,13 @@ 77E0DC9E2A6940C400D03724 /* Calendar+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E0DC9D2A6940C400D03724 /* Calendar+Helpers.swift */; }; 77E255672A16145500B644C3 /* StakingRewardsFilterMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E255662A16145500B644C3 /* StakingRewardsFilterMapper.swift */; }; 77E255692A16148000B644C3 /* StakingRewardsFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E255682A16148000B644C3 /* StakingRewardsFilter.swift */; }; + 77E304A92AEB9F76006FD6F0 /* SwapConfirmInitState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304A82AEB9F76006FD6F0 /* SwapConfirmInitState.swift */; }; + 77E304AB2AEBB214006FD6F0 /* SlippageBounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304AA2AEBB214006FD6F0 /* SlippageBounds.swift */; }; + 77E304AD2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304AC2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift */; }; 77E304B02AEFA261006FD6F0 /* SwapNetworkFeeSheetLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304AF2AEFA261006FD6F0 /* SwapNetworkFeeSheetLayout.swift */; }; 77E304B22AEFC0F3006FD6F0 /* SwapNetworkFeeSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304B12AEFC0F3006FD6F0 /* SwapNetworkFeeSheetViewController.swift */; }; 77E304B52AEFC303006FD6F0 /* SwapNetworkFeeSheetViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304B42AEFC303006FD6F0 /* SwapNetworkFeeSheetViewFactory.swift */; }; 77E304B72AEFC348006FD6F0 /* SwapNetworkFeeSheetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304B62AEFC348006FD6F0 /* SwapNetworkFeeSheetViewModel.swift */; }; - 77E304A92AEB9F76006FD6F0 /* SwapConfirmInitState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304A82AEB9F76006FD6F0 /* SwapConfirmInitState.swift */; }; - 77E304AB2AEBB214006FD6F0 /* SlippageBounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304AA2AEBB214006FD6F0 /* SlippageBounds.swift */; }; - 77E304AD2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304AC2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift */; }; 77EA2A232A333C1500B0670B /* french_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A182A333C1500B0670B /* french_output.json */; }; 77EA2A242A333C1500B0670B /* arrays_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A192A333C1500B0670B /* arrays_output.json */; }; 77EA2A252A333C1500B0670B /* weird_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A1A2A333C1500B0670B /* weird_output.json */; }; @@ -4850,6 +4851,7 @@ 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashModel.swift; sourceTree = ""; }; 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashViewModel.swift; sourceTree = ""; }; 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsPoolRewardView.swift; sourceTree = ""; }; + 77C9761D2AF220130049272C /* SwapSelectedChainAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapSelectedChainAsset.swift; sourceTree = ""; }; 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModels.swift; sourceTree = ""; }; 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapsSetupViewModelFactory.swift; sourceTree = ""; }; 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationInteractor.swift; sourceTree = ""; }; @@ -4872,13 +4874,13 @@ 77E255652A16059A00B644C3 /* MultiassetUserDataModel9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel9.xcdatamodel; sourceTree = ""; }; 77E255662A16145500B644C3 /* StakingRewardsFilterMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardsFilterMapper.swift; sourceTree = ""; }; 77E255682A16148000B644C3 /* StakingRewardsFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardsFilter.swift; sourceTree = ""; }; + 77E304A82AEB9F76006FD6F0 /* SwapConfirmInitState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConfirmInitState.swift; sourceTree = ""; }; + 77E304AA2AEBB214006FD6F0 /* SlippageBounds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlippageBounds.swift; sourceTree = ""; }; + 77E304AC2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPriceDifferenceViewModelFactoryProtocol.swift; sourceTree = ""; }; 77E304AF2AEFA261006FD6F0 /* SwapNetworkFeeSheetLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeSheetLayout.swift; sourceTree = ""; }; 77E304B12AEFC0F3006FD6F0 /* SwapNetworkFeeSheetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeSheetViewController.swift; sourceTree = ""; }; 77E304B42AEFC303006FD6F0 /* SwapNetworkFeeSheetViewFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeSheetViewFactory.swift; sourceTree = ""; }; 77E304B62AEFC348006FD6F0 /* SwapNetworkFeeSheetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeSheetViewModel.swift; sourceTree = ""; }; - 77E304A82AEB9F76006FD6F0 /* SwapConfirmInitState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConfirmInitState.swift; sourceTree = ""; }; - 77E304AA2AEBB214006FD6F0 /* SlippageBounds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlippageBounds.swift; sourceTree = ""; }; - 77E304AC2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPriceDifferenceViewModelFactoryProtocol.swift; sourceTree = ""; }; 77EA2A182A333C1500B0670B /* french_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = french_output.json; sourceTree = ""; }; 77EA2A192A333C1500B0670B /* arrays_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = arrays_output.json; sourceTree = ""; }; 77EA2A1A2A333C1500B0670B /* weird_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = weird_output.json; sourceTree = ""; }; @@ -9938,6 +9940,7 @@ 77C9BCCD2ACD691300022EA2 /* SwapAssetsOperationViewFactory.swift */, 77C9BCCF2ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift */, 77C9BCD12ACD8AF400022EA2 /* SwapAssetsOperationViewController.swift */, + 77C9761D2AF220130049272C /* SwapSelectedChainAsset.swift */, ); path = Swaps; sourceTree = ""; @@ -21149,6 +21152,7 @@ 8467FD4124ED3C72005D486C /* AlignableContentControl.swift in Sources */, 84C2063C28D1D25C006D0D52 /* ModalPickerFactory+YieldBoost.swift in Sources */, 84D1ABDE27E1B5240073C631 /* AssetViewModel.swift in Sources */, + 77C9761E2AF220130049272C /* SwapSelectedChainAsset.swift in Sources */, 840D626F29CB39EE00D5E894 /* URLBuilder.swift in Sources */, 842BA36D27B64F1000D31EEF /* DAppViewModel.swift in Sources */, 8439399C2636FAB40087658D /* YourValidatorViewModel.swift in Sources */, diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationPresenter.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationPresenter.swift index cee53517d6..e2f6ddb169 100644 --- a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationPresenter.swift +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetOperationPresenter.swift @@ -4,6 +4,8 @@ import RobinHood import SoraFoundation final class SwapAssetsOperationPresenter: AssetsSearchPresenter { + private var selfSufficientChainAsssets: Set = .init() + var swapAssetsWireframe: SwapAssetsOperationWireframeProtocol? { wireframe as? SwapAssetsOperationWireframeProtocol } @@ -12,10 +14,10 @@ final class SwapAssetsOperationPresenter: AssetsSearchPresenter { view as? SwapAssetsViewProtocol } - let selectClosure: (ChainAsset) -> Void + let selectClosure: (SwapSelectedChainAsset) -> Void init( - selectClosure: @escaping (ChainAsset) -> Void, + selectClosure: @escaping (SwapSelectedChainAsset) -> Void, interactor: SwapAssetsOperationInteractorInputProtocol, viewModelFactory: AssetListAssetViewModelFactoryProtocol, localizationManager: LocalizationManagerProtocol, @@ -41,13 +43,19 @@ final class SwapAssetsOperationPresenter: AssetsSearchPresenter { guard let chainAsset = result?.state.chainAsset(for: chainAssetId) else { return } - selectClosure(chainAsset) + selectClosure( + .init( + selfSufficient: selfSufficientChainAsssets.contains(chainAssetId), + chainAsset: chainAsset + ) + ) wireframe.close(view: view) } } extension SwapAssetsOperationPresenter: SwapAssetsOperationPresenterProtocol { - func directionsLoaded() { + func didReceive(selfSufficientChainAsssets: Set) { + self.selfSufficientChainAsssets = selfSufficientChainAsssets swapAssetsView?.didStopLoading() } diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift index 16098fcd58..aaa7254afa 100644 --- a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift @@ -66,9 +66,22 @@ final class SwapAssetsOperationInteractor: AnyCancellableCleaning { DispatchQueue.main.async { do { let result = try wrapper.targetOperation.extractNoCancellableResultData() - self?.availableDirections = mapper(result) + let directions = mapper(result) + self?.availableDirections = directions self?.createBuilder() - self?.presenter?.directionsLoaded() + + let chains = self?.stateObservable.state.value.allChains ?? [:] + let selfSufficientAssets = directions.reduce(into: [ChainAssetId]()) { + guard let utilityChainAssetId = chains[$1.key.chainId]?.utilityChainAssetId() else { + return + } + if $1.key == utilityChainAssetId { + $0.append(contentsOf: $1.value) + } else if $1.value.contains(utilityChainAssetId) { + $0.append($1.key) + } + } + self?.presenter?.didReceive(selfSufficientChainAsssets: Set(selfSufficientAssets)) } catch { self?.presenter?.didReceive(error: .directions(error)) } diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationProtocols.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationProtocols.swift index 2f139cff57..da490d7aad 100644 --- a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationProtocols.swift +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationProtocols.swift @@ -2,7 +2,7 @@ protocol SwapAssetsOperationWireframeProtocol: AssetsSearchWireframeProtocol, Er AlertPresentable, CommonRetryable {} protocol SwapAssetsOperationPresenterProtocol: AssetsSearchInteractorOutputProtocol { - func directionsLoaded() + func didReceive(selfSufficientChainAsssets: Set) func didReceive(error: SwapAssetsOperationError) } diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift index 8eaaf64f6a..e89389330b 100644 --- a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationViewFactory.swift @@ -5,7 +5,7 @@ enum SwapAssetsOperationViewFactory { static func createSelectPayTokenView( for stateObservable: AssetListModelObservable, chainAsset: ChainAsset? = nil, - selectClosure: @escaping (ChainAsset) -> Void + selectClosure: @escaping (SwapSelectedChainAsset) -> Void ) -> AssetsSearchViewProtocol? { let title: LocalizableResource = .init { R.string.localizable.swapsPayTokenSelectionTitle( @@ -24,7 +24,7 @@ enum SwapAssetsOperationViewFactory { static func createSelectReceiveTokenView( for stateObservable: AssetListModelObservable, chainAsset: ChainAsset? = nil, - selectClosure: @escaping (ChainAsset) -> Void + selectClosure: @escaping (SwapSelectedChainAsset) -> Void ) -> AssetsSearchViewProtocol? { let title: LocalizableResource = .init { R.string.localizable.swapsReceiveTokenSelectionTitle( @@ -44,7 +44,7 @@ enum SwapAssetsOperationViewFactory { for stateObservable: AssetListModelObservable, chainAsset: ChainAsset? = nil, title: LocalizableResource, - selectClosure: @escaping (ChainAsset) -> Void + selectClosure: @escaping (SwapSelectedChainAsset) -> Void ) -> AssetsSearchViewProtocol? { guard let currencyManager = CurrencyManager.shared else { return nil @@ -84,7 +84,7 @@ enum SwapAssetsOperationViewFactory { stateObservable: AssetListModelObservable, viewModelFactory: AssetListAssetViewModelFactoryProtocol, chainAsset: ChainAsset?, - selectClosure: @escaping (ChainAsset) -> Void + selectClosure: @escaping (SwapSelectedChainAsset) -> Void ) -> SwapAssetsOperationPresenter? { let westmintChainId = KnowChainId.westmint let chainRegistry = ChainRegistryFacade.sharedRegistry diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapSelectedChainAsset.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapSelectedChainAsset.swift new file mode 100644 index 0000000000..06f08530f4 --- /dev/null +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapSelectedChainAsset.swift @@ -0,0 +1,4 @@ +struct SwapSelectedChainAsset { + let selfSufficient: Bool + let chainAsset: ChainAsset +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 44d288d182..253b6b903f 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -18,7 +18,8 @@ final class SwapSetupPresenter { private(set) var payAssetPriceData: PriceData? private(set) var receiveAssetPriceData: PriceData? private(set) var feeAssetPriceData: PriceData? - + private(set) var payAssetSelfSufficient: Bool = false + private(set) var receiveAssetSelfSufficient: Bool = false private(set) var payAmountInput: AmountInputResult? private(set) var receiveAmountInput: Decimal? private(set) var fee: AssetConversion.FeeModel? @@ -217,10 +218,11 @@ final class SwapSetupPresenter { view?.didReceiveNetworkFee(viewModel: .loading) return } + let isEditable = (payChainAsset?.isUtilityAsset == false) && payAssetSelfSufficient let viewModel = viewModelFactory.feeViewModel( amount: fee, assetDisplayInfo: feeChainAsset.assetDisplayInfo, - isEditable: payChainAsset?.isUtilityAsset == false, + isEditable: isEditable, priceData: feeAssetPriceData ) @@ -401,14 +403,15 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } func selectPayToken() { - wireframe.showPayTokenSelection(from: view, chainAsset: receiveChainAsset) { [weak self] chainAsset in + wireframe.showPayTokenSelection(from: view, chainAsset: receiveChainAsset) { [weak self] chainAssetModel in + let chainAsset = chainAssetModel.chainAsset self?.payChainAsset = chainAsset let feeChainAsset = chainAsset.chain.utilityAsset().map { ChainAsset(chain: chainAsset.chain, asset: $0) } self?.feeChainAsset = feeChainAsset - + self?.payAssetSelfSufficient = chainAssetModel.selfSufficient self?.providePayAssetViews() self?.provideButtonState() self?.provideSettingsState() @@ -420,8 +423,10 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } func selectReceiveToken() { - wireframe.showReceiveTokenSelection(from: view, chainAsset: payChainAsset) { [weak self] chainAsset in + wireframe.showReceiveTokenSelection(from: view, chainAsset: payChainAsset) { [weak self] chainAssetModel in + let chainAsset = chainAssetModel.chainAsset self?.receiveChainAsset = chainAsset + self?.receiveAssetSelfSufficient = chainAssetModel.selfSufficient self?.provideReceiveAssetViews() self?.provideButtonState() self?.refreshQuote(direction: .buy, forceUpdate: false) @@ -443,6 +448,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { func swap() { Swift.swap(&payChainAsset, &receiveChainAsset) + Swift.swap(&payAssetSelfSufficient, &receiveAssetSelfSufficient) interactor.update(payChainAsset: payChainAsset) interactor.update(receiveChainAsset: receiveChainAsset) payAmountInput = nil @@ -451,6 +457,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideReceiveAssetViews() provideButtonState() provideSettingsState() + provideFeeViewModel() refreshQuote(direction: .sell, forceUpdate: false) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 55d3f5491f..b9d53a0213 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -46,12 +46,12 @@ protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryabl func showPayTokenSelection( from view: ControllerBackedProtocol?, chainAsset: ChainAsset?, - completionHandler: @escaping (ChainAsset) -> Void + completionHandler: @escaping (SwapSelectedChainAsset) -> Void ) func showReceiveTokenSelection( from view: ControllerBackedProtocol?, chainAsset: ChainAsset?, - completionHandler: @escaping (ChainAsset) -> Void + completionHandler: @escaping (SwapSelectedChainAsset) -> Void ) func showSettings( from view: ControllerBackedProtocol?, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index 1e91ad7af5..bb3f8eda1c 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -12,7 +12,7 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { func showPayTokenSelection( from view: ControllerBackedProtocol?, chainAsset: ChainAsset?, - completionHandler: @escaping (ChainAsset) -> Void + completionHandler: @escaping (SwapSelectedChainAsset) -> Void ) { guard let selectTokenView = SwapAssetsOperationViewFactory.createSelectPayTokenView( for: assetListObservable, @@ -32,7 +32,7 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { func showReceiveTokenSelection( from view: ControllerBackedProtocol?, chainAsset: ChainAsset?, - completionHandler: @escaping (ChainAsset) -> Void + completionHandler: @escaping (SwapSelectedChainAsset) -> Void ) { guard let selectTokenView = SwapAssetsOperationViewFactory.createSelectReceiveTokenView( for: assetListObservable, From cfe7231ba2b31c47219de9c8a45e0649c1862dd1 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 1 Nov 2023 13:50:24 +0100 Subject: [PATCH 106/204] get rid of hadrcoded network in setup --- .../ChainRegistry/LocalChain/ChainModel.swift | 4 ++ .../AssetConversionPallet.swift | 40 +++----------- .../AssetConversionAggregationFactory.swift | 38 +++++++++++++ .../AssetHubSwapOperationFactory.swift | 31 +++++++++-- .../AssetHub/AssetHubTokensConverter.swift | 49 +++++++++++++++-- .../Swaps/SwapAssetsOperationInteractor.swift | 13 ++++- .../Swaps/Base/SwapBaseInteractor.swift | 54 +++++++++++-------- .../Swaps/Confirm/SwapConfirmInteractor.swift | 4 +- .../Confirm/SwapConfirmViewFactory.swift | 20 ++++--- .../Swaps/Setup/SwapSetupPresenter.swift | 2 + .../Swaps/Setup/SwapSetupViewFactory.swift | 17 ++---- 11 files changed, 182 insertions(+), 90 deletions(-) diff --git a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift index 78557280e8..f089f9588b 100644 --- a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift +++ b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift @@ -219,6 +219,10 @@ struct ChainModel: Equatable, Codable, Hashable { var defaultBlockTimeMillis: BlockTime? { additional?.defaultBlockTime?.unsignedIntValue } + + var isUtilityTokenOnRelaychain: Bool { + additional?.relaychainAsNative?.boolValue ?? false + } } extension ChainModel: Identifiable { diff --git a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift index c8c487754b..be2c3dd2b1 100644 --- a/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift +++ b/novawallet/Common/Substrate/Types/AssetConversionPallet/AssetConversionPallet.swift @@ -12,37 +12,16 @@ enum AssetConversionPallet { case assets(pallet: UInt8, index: BigUInt) case foreign(AssetId) case undefined(AssetId) - - init(multilocation: XcmV3.Multilocation) { - let junctions = multilocation.interior.items - - if multilocation.parents == 0 { - guard !junctions.isEmpty else { - self = .native - return - } - - switch junctions[0] { - case let .palletInstance(pallet): - if - junctions.count == 2, - case let .generalIndex(index) = junctions[1] { - self = .assets(pallet: pallet, index: index) - } else { - self = .undefined(multilocation) - } - default: - self = .undefined(multilocation) - } - } else { - self = .foreign(multilocation) - } - } } - struct PoolAssetPair: JSONListConvertible { + struct PoolAssetPair { let asset1: PoolAsset let asset2: PoolAsset + } + + struct AssetIdPair: JSONListConvertible { + let asset1: AssetId + let asset2: AssetId init(jsonList: [JSON], context: [CodingUserInfoKey: Any]?) throws { let expectedFieldsCount = 1 @@ -58,11 +37,8 @@ enum AssetConversionPallet { throw JSONListConvertibleError.unexpectedValue(jsonList[0]) } - let multilocation1 = try poolId[0].map(to: AssetId.self, with: context) - let multilocation2 = try poolId[1].map(to: AssetId.self, with: context) - - asset1 = PoolAsset(multilocation: multilocation1) - asset2 = PoolAsset(multilocation: multilocation2) + asset1 = try poolId[0].map(to: AssetId.self, with: context) + asset2 = try poolId[1].map(to: AssetId.self, with: context) } } } diff --git a/novawallet/Modules/AssetConversion/Service/AssetConversionAggregationFactory.swift b/novawallet/Modules/AssetConversion/Service/AssetConversionAggregationFactory.swift index 4ff663058e..2e0a7c3ac7 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetConversionAggregationFactory.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetConversionAggregationFactory.swift @@ -9,6 +9,11 @@ protocol AssetConversionAggregationFactoryProtocol { func createAvailableDirectionsWrapper( for chain: ChainModel ) -> CompoundOperationWrapper<[ChainAssetId: Set]> + + func createQuoteWrapper( + for chain: ChainModel, + args: AssetConversion.QuoteArgs + ) -> CompoundOperationWrapper } enum AssetConversionAggregationFactoryError: Error { @@ -62,6 +67,26 @@ final class AssetConversionAggregationFactory { operationQueue: operationQueue ).availableDirectionsForAsset(chainAsset.chainAssetId) } + + private func createAssetHubQuote( + for chain: ChainModel, + args: AssetConversion.QuoteArgs + ) -> CompoundOperationWrapper { + guard let connection = chainRegistry.getConnection(for: chain.chainId) else { + return .createWithError(ChainRegistryError.connectionUnavailable) + } + + guard let runtimeService = chainRegistry.getRuntimeProvider(for: chain.chainId) else { + return .createWithError(ChainRegistryError.runtimeMetadaUnavailable) + } + + return AssetHubSwapOperationFactory( + chain: chain, + runtimeService: runtimeService, + connection: connection, + operationQueue: operationQueue + ).quote(for: args) + } } extension AssetConversionAggregationFactory: AssetConversionAggregationFactoryProtocol { @@ -88,4 +113,17 @@ extension AssetConversionAggregationFactory: AssetConversionAggregationFactoryPr ) } } + + func createQuoteWrapper( + for chain: ChainModel, + args: AssetConversion.QuoteArgs + ) -> CompoundOperationWrapper { + if chain.hasSwapHub { + return createAssetHubQuote(for: chain, args: args) + } else { + return CompoundOperationWrapper.createWithError( + AssetConversionAggregationFactoryError.unavailableProvider(chain) + ) + } + } } diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift index 55f8b9d1b1..ad28e3a795 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubSwapOperationFactory.swift @@ -25,7 +25,8 @@ final class AssetHubSwapOperationFactory { } private func fetchAllPairsWrapper( - dependingOn codingFactoryOperation: BaseOperation + dependingOn codingFactoryOperation: BaseOperation, + chain: ChainModel ) -> CompoundOperationWrapper<[AssetConversionPallet.PoolAssetPair]> { let prefixEncodingOperation = UnkeyedEncodingOperation( path: AssetConversionPallet.poolsPath, @@ -49,7 +50,7 @@ final class AssetHubSwapOperationFactory { keysFetchOperation.addDependency(prefixEncodingOperation) - let decodingOperation = StorageKeyDecodingOperation( + let decodingOperation = StorageKeyDecodingOperation( path: AssetConversionPallet.poolsPath ) @@ -64,9 +65,29 @@ final class AssetHubSwapOperationFactory { decodingOperation.addDependency(keysFetchOperation) + let mappingOperation = ClosureOperation<[AssetConversionPallet.PoolAssetPair]> { + let decodedPairs = try decodingOperation.extractNoCancellableResultData() + + return decodedPairs.map { assetIdPair in + let asset1 = AssetHubTokensConverter.convertFromMultilocation( + assetIdPair.asset1, + chain: chain + ) + + let asset2 = AssetHubTokensConverter.convertFromMultilocation( + assetIdPair.asset2, + chain: chain + ) + + return .init(asset1: asset1, asset2: asset2) + } + } + + mappingOperation.addDependency(decodingOperation) + return CompoundOperationWrapper( - targetOperation: decodingOperation, - dependencies: [prefixEncodingOperation, keysFetchOperation] + targetOperation: mappingOperation, + dependencies: [prefixEncodingOperation, keysFetchOperation, decodingOperation] ) } @@ -147,7 +168,7 @@ final class AssetHubSwapOperationFactory { extension AssetHubSwapOperationFactory: AssetConversionOperationFactoryProtocol { func availableDirections() -> CompoundOperationWrapper<[ChainAssetId: Set]> { let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() - let fetchRemoteWrapper = fetchAllPairsWrapper(dependingOn: codingFactoryOperation) + let fetchRemoteWrapper = fetchAllPairsWrapper(dependingOn: codingFactoryOperation, chain: chain) let mappingOperation = mapRemotePairsOperation( for: chain, dependingOn: codingFactoryOperation, diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift index 3a53564e37..9cda1f9ac4 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift @@ -19,20 +19,61 @@ enum AssetHubTokensConverter { return nil } - return convertToMultilocation(asset: localAsset, codingFactory: codingFactory) + return convertToMultilocation( + chainAsset: ChainAsset(chain: chain, asset: localAsset), + codingFactory: codingFactory + ) + } + + static func convertFromMultilocation( + _ assetId: AssetConversionPallet.AssetId, + chain: ChainModel + ) -> AssetConversionPallet.PoolAsset { + let junctions = assetId.interior.items + + if assetId.parents == 0 { + guard !junctions.isEmpty else { + return .native + } + + switch junctions[0] { + case let .palletInstance(pallet): + if + junctions.count == 2, + case let .generalIndex(index) = junctions[1] { + return .assets(pallet: pallet, index: index) + } else { + return .undefined(assetId) + } + default: + return .undefined(assetId) + } + } else if assetId.parents == 1, junctions.isEmpty, chain.isUtilityTokenOnRelaychain { + return .native + } else { + return .foreign(assetId) + } } static func convertToMultilocation( - asset: AssetModel, + chainAsset: ChainAsset, codingFactory: RuntimeCoderFactoryProtocol ) -> AssetConversionPallet.AssetId? { - guard let storageInfo = try? AssetStorageInfo.extract(from: asset, codingFactory: codingFactory) else { + guard + let storageInfo = try? AssetStorageInfo.extract( + from: chainAsset.asset, + codingFactory: codingFactory + ) else { return nil } switch storageInfo { case .native: - return .init(parents: 0, interior: .init(items: [])) + if chainAsset.chain.isUtilityTokenOnRelaychain { + return .init(parents: 1, interior: .init(items: [])) + } else { + return .init(parents: 0, interior: .init(items: [])) + } case let .statemine(info): if info.assetIdString.isHex() { let remoteAssetId = try? info.assetId.map( diff --git a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift index 7c2158d028..4a057ae9fd 100644 --- a/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift +++ b/novawallet/Modules/AssetsSearch/Swaps/SwapAssetsOperationInteractor.swift @@ -101,7 +101,7 @@ final class SwapAssetsOperationInteractor: AnyCancellableCleaning { case .success: self?.presenter?.directionsLoaded() case let .failure(error): - self?.presenter?.didReceive(error: .directions(error)) + self?.handleDirectionsResponse(error: error) } } } @@ -122,11 +122,20 @@ final class SwapAssetsOperationInteractor: AnyCancellableCleaning { self?.updateAvailableDirections([chainAsset.chainAssetId: directions]) self?.presenter?.directionsLoaded() case let .failure(error): - self?.presenter?.didReceive(error: .directions(error)) + self?.handleDirectionsResponse(error: error) } } } + private func handleDirectionsResponse(error: Error) { + if let encodingError = error as? StorageKeyEncodingOperationError, encodingError == .invalidStoragePath { + // ignore not retryable errors + presenter?.directionsLoaded() + } else { + presenter?.didReceive(error: .directions(error)) + } + } + private func updateAvailableDirections(_ newDirections: [ChainAssetId: Set]) { availableDirections = newDirections.reduce(into: availableDirections) { accum, keyValue in accum[keyValue.key] = keyValue.value diff --git a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift index 80e161a732..37c3b41eb3 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift @@ -4,7 +4,7 @@ import BigInt class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapBaseInteractorInputProtocol { weak var basePresenter: SwapBaseInteractorOutputProtocol? - let assetConversionOperationFactory: AssetConversionOperationFactoryProtocol + let assetConversionAggregator: AssetConversionAggregationFactoryProtocol let assetConversionFeeService: AssetConversionFeeServiceProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol @@ -12,14 +12,15 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB let selectedWallet: MetaAccountModel private let operationQueue: OperationQueue - private var quoteCall: CancellableCall? + private var quoteCall = CancellableCallStore() private var priceProviders: [ChainAssetId: StreamableProvider] = [:] private var assetBalanceProviders: [ChainAssetId: StreamableProvider] = [:] private var feeModelBuilder: AssetHubFeeModelBuilder? + private var currentChain: ChainModel? init( - assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, + assetConversionAggregator: AssetConversionAggregationFactoryProtocol, assetConversionFeeService: AssetConversionFeeServiceProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, @@ -27,7 +28,7 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB selectedWallet: MetaAccountModel, operationQueue: OperationQueue ) { - self.assetConversionOperationFactory = assetConversionOperationFactory + self.assetConversionAggregator = assetConversionAggregator self.assetConversionFeeService = assetConversionFeeService self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory @@ -36,6 +37,10 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB self.operationQueue = operationQueue } + deinit { + quoteCall.cancel() + } + func updateFeeModelBuilder(for chain: ChainModel) { guard let utilityAsset = chain.utilityChainAsset(), @@ -92,26 +97,27 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB } func quote(args: AssetConversion.QuoteArgs) { - clear(cancellable: "eCall) - - let wrapper = assetConversionOperationFactory.quote(for: args) - wrapper.targetOperation.completionBlock = { [weak self, args] in - DispatchQueue.main.async { - guard self?.quoteCall === wrapper else { - return - } - do { - let result = try wrapper.targetOperation.extractNoCancellableResultData() - - self?.basePresenter?.didReceive(quote: result, for: args) - } catch { - self?.basePresenter?.didReceive(baseError: .quote(error, args)) - } - } + quoteCall.cancel() + + guard let chain = currentChain else { + return } - quoteCall = wrapper - operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + let wrapper = assetConversionAggregator.createQuoteWrapper(for: chain, args: args) + + executeCancellable( + wrapper: wrapper, + inOperationQueue: operationQueue, + backingCallIn: quoteCall, + runningCallbackIn: .main + ) { [weak self] result in + switch result { + case let .success(quote): + self?.basePresenter?.didReceive(quote: quote, for: args) + case let .failure(error): + self?.basePresenter?.didReceive(baseError: .quote(error, args)) + } + } } func fee(args: AssetConversion.CallArgs) { @@ -139,12 +145,16 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB } func set(receiveChainAsset chainAsset: ChainAsset) { + currentChain = chainAsset.chain + updateFeeModelBuilder(for: chainAsset.chain) priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) } func set(payChainAsset chainAsset: ChainAsset) { + currentChain = chainAsset.chain + updateFeeModelBuilder(for: chainAsset.chain) if let utilityAsset = chainAsset.chain.utilityChainAsset() { diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift index ae0e151d99..37348a256f 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift @@ -16,7 +16,7 @@ final class SwapConfirmInteractor: SwapBaseInteractor { init( initState: SwapConfirmInitState, assetConversionFeeService: AssetConversionFeeServiceProtocol, - assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, + assetConversionAggregator: AssetConversionAggregationFactoryProtocol, assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol, runtimeService: RuntimeProviderProtocol, extrinsicService: ExtrinsicServiceProtocol, @@ -35,7 +35,7 @@ final class SwapConfirmInteractor: SwapBaseInteractor { self.assetConversionExtrinsicService = assetConversionExtrinsicService super.init( - assetConversionOperationFactory: assetConversionOperationFactory, + assetConversionAggregator: assetConversionAggregator, assetConversionFeeService: assetConversionFeeService, priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index 7223ec6de6..cd4d1657cb 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -63,13 +63,13 @@ struct SwapConfirmViewFactory { wallet: MetaAccountModel, initState: SwapConfirmInitState ) -> SwapConfirmInteractor? { - let chainId = initState.chainAssetIn.chain.chainId let chainRegistry = ChainRegistryFacade.sharedRegistry let accountRequest = initState.chainAssetIn.chain.accountRequest() - guard let connection = chainRegistry.getConnection(for: chainId), - let runtimeService = chainRegistry.getRuntimeProvider(for: chainId), - let chainModel = chainRegistry.getChain(for: chainId), + let chain = initState.chainAssetIn.chain + + guard let connection = chainRegistry.getConnection(for: chain.chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chain.chainId), let currencyManager = CurrencyManager.shared, let selectedAccount = wallet.fetchMetaChainAccount(for: accountRequest) else { return nil @@ -77,10 +77,8 @@ struct SwapConfirmViewFactory { let operationQueue = OperationManagerFacade.sharedDefaultQueue - let assetConversionOperationFactory = AssetHubSwapOperationFactory( - chain: chainModel, - runtimeService: runtimeService, - connection: connection, + let assetConversionAggregator = AssetConversionAggregationFactory( + chainRegistry: chainRegistry, operationQueue: operationQueue ) @@ -88,7 +86,7 @@ struct SwapConfirmViewFactory { runtimeRegistry: runtimeService, engine: connection, operationManager: OperationManager(operationQueue: operationQueue) - ).createService(account: selectedAccount.chainAccount, chain: chainModel) + ).createService(account: selectedAccount.chainAccount, chain: chain) let feeService = AssetHubFeeService( wallet: wallet, @@ -104,8 +102,8 @@ struct SwapConfirmViewFactory { let interactor = SwapConfirmInteractor( initState: initState, assetConversionFeeService: feeService, - assetConversionOperationFactory: assetConversionOperationFactory, - assetConversionExtrinsicService: AssetHubExtrinsicService(chain: chainModel), + assetConversionAggregator: assetConversionAggregator, + assetConversionExtrinsicService: AssetHubExtrinsicService(chain: chain), runtimeService: runtimeService, extrinsicService: extrinsicService, priceLocalSubscriptionFactory: PriceProviderFactory.shared, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 78eb4f9a3c..278a2c3832 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -533,6 +533,8 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { func didReceive(baseError: SwapSetupError) { + logger.error("Did receive error: \(baseError)") + switch baseError { case let .quote(_, args): guard args == quoteArgs else { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index 795a8d4710..df27117f2f 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -54,23 +54,16 @@ struct SwapSetupViewFactory { } private static func createInteractor() -> SwapSetupInteractor? { - let westmintChainId = KnowChainId.westmint - let chainRegistry = ChainRegistryFacade.sharedRegistry - - guard let connection = chainRegistry.getConnection(for: westmintChainId), - let runtimeService = chainRegistry.getRuntimeProvider(for: westmintChainId), - let chainModel = chainRegistry.getChain(for: westmintChainId), - let currencyManager = CurrencyManager.shared, + guard let currencyManager = CurrencyManager.shared, let selectedWallet = SelectedWalletSettings.shared.value else { return nil } + let chainRegistry = ChainRegistryFacade.sharedRegistry let operationQueue = OperationManagerFacade.sharedDefaultQueue - let assetConversionOperationFactory = AssetHubSwapOperationFactory( - chain: chainModel, - runtimeService: runtimeService, - connection: connection, + let assetConversionAggregator = AssetConversionAggregationFactory( + chainRegistry: chainRegistry, operationQueue: operationQueue ) @@ -81,7 +74,7 @@ struct SwapSetupViewFactory { ) let interactor = SwapSetupInteractor( - assetConversionOperationFactory: assetConversionOperationFactory, + assetConversionAggregator: assetConversionAggregator, assetConversionFeeService: feeService, priceLocalSubscriptionFactory: PriceProviderFactory.shared, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, From df1568ef8f25d1872a8798f89a6947e73d756965 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 1 Nov 2023 18:18:31 +0100 Subject: [PATCH 107/204] fix can pay fee asset --- .../Swaps/Base/SwapBaseInteractor.swift | 2 +- .../Swaps/Base/SwapBaseProtocols.swift | 9 ++++- .../Swaps/Confirm/SwapConfirmInteractor.swift | 2 - .../Swaps/Confirm/SwapConfirmPresenter.swift | 8 +++- .../Swaps/Setup/SwapSetupInteractor.swift | 37 ++++++++++++++++++- .../Swaps/Setup/SwapSetupPresenter.swift | 25 ++++++++++++- .../Swaps/Setup/SwapSetupProtocols.swift | 10 ++--- 7 files changed, 78 insertions(+), 15 deletions(-) diff --git a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift index 3ba91a8d66..b92dea68b7 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift @@ -10,8 +10,8 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol let currencyManager: CurrencyManagerProtocol let selectedWallet: MetaAccountModel + let operationQueue: OperationQueue - private let operationQueue: OperationQueue private var quoteCall = CancellableCallStore() private var priceProviders: [ChainAssetId: StreamableProvider] = [:] diff --git a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift index 53d568b1f8..0e3381e892 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift @@ -10,8 +10,15 @@ protocol SwapBaseInteractorInputProtocol: AnyObject { protocol SwapBaseInteractorOutputProtocol: AnyObject { func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) func didReceive(fee: AssetConversion.FeeModel?, transactionId: TransactionFeeId, feeChainAssetId: ChainAssetId?) - func didReceive(baseError: SwapSetupError) + func didReceive(baseError: SwapBaseError) func didReceive(price: PriceData?, priceId: AssetModel.PriceId) func didReceive(payAccountId: AccountId?) func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId: AccountId) } + +enum SwapBaseError: Error { + case quote(Error, AssetConversion.QuoteArgs) + case fetchFeeFailed(Error, TransactionFeeId, FeeChainAssetId?) + case price(Error, AssetModel.PriceId) + case assetBalance(Error, ChainAssetId, AccountId) +} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift index 37348a256f..e0438b072d 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift @@ -11,7 +11,6 @@ final class SwapConfirmInteractor: SwapBaseInteractor { let extrinsicService: ExtrinsicServiceProtocol let assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol let signer: SigningWrapperProtocol - let operationQueue: OperationQueue init( initState: SwapConfirmInitState, @@ -29,7 +28,6 @@ final class SwapConfirmInteractor: SwapBaseInteractor { ) { self.initState = initState self.signer = signer - self.operationQueue = operationQueue self.runtimeService = runtimeService self.extrinsicService = extrinsicService self.assetConversionExtrinsicService = assetConversionExtrinsicService diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 7ebc9ca88b..96f9d82abe 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -367,7 +367,11 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { } } - func didReceive(fee: AssetConversion.FeeModel?, transactionId _: TransactionFeeId, feeChainAssetId _: ChainAssetId?) { + func didReceive( + fee: AssetConversion.FeeModel?, + transactionId _: TransactionFeeId, + feeChainAssetId _: ChainAssetId? + ) { self.fee = fee provideFeeViewModel() } @@ -398,7 +402,7 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { balances[chainAsset] = balance } - func didReceive(baseError: SwapSetupError) { + func didReceive(baseError: SwapBaseError) { switch baseError { case let .quote(_, args): wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 2c29063662..e37d4d1951 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -34,6 +34,38 @@ final class SwapSetupInteractor: SwapBaseInteractor { ].compactMap { $0 } ) } + + private var canPayFeeInAssetCall = CancellableCallStore() + + deinit { + canPayFeeInAssetCall.cancel() + } + + private func provideCanPayFee(for asset: ChainAsset) { + canPayFeeInAssetCall.cancel() + + guard let utilityAssetId = asset.chain.utilityChainAssetId() else { + presenter?.didReceiveCanPayFeeInPayAsset(false, chainAssetId: asset.chainAssetId) + return + } + + let wrapper = assetConversionAggregator.createAvailableDirectionsWrapper(for: asset) + + executeCancellable( + wrapper: wrapper, + inOperationQueue: operationQueue, + backingCallIn: canPayFeeInAssetCall, + runningCallbackIn: .main + ) { [weak self] result in + switch result { + case let .success(chainAssetIds): + let canPayFee = chainAssetIds.contains(utilityAssetId) + self?.presenter?.didReceiveCanPayFeeInPayAsset(canPayFee, chainAssetId: asset.chainAssetId) + case let .failure(error): + self?.presenter?.didReceive(setupError: .payAssetSetFailed(error)) + } + } + } } extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { @@ -47,8 +79,9 @@ extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { func update(payChainAsset: ChainAsset?) { self.payChainAsset = payChainAsset - payChainAsset.map { - set(payChainAsset: $0) + if let payChainAsset = payChainAsset { + set(payChainAsset: payChainAsset) + provideCanPayFee(for: payChainAsset) } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index c54a153db8..c08c2190a1 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -566,8 +566,8 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { - func didReceive(baseError: SwapSetupError) { - logger.error("Did receive error: \(baseError)") + func didReceive(baseError: SwapBaseError) { + logger.error("Did receive base error: \(baseError)") switch baseError { case let .quote(_, args): @@ -592,6 +592,19 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { } } + func didReceive(setupError: SwapSetupError) { + logger.error("Did receive setup error: \(setupError)") + + switch setupError { + case let .payAssetSetFailed(error): + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + if let payChainAsset = self?.payChainAsset { + self?.interactor.update(payChainAsset: payChainAsset) + } + } + } + } + func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) { guard quoteArgs == self.quoteArgs else { return @@ -677,6 +690,14 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { } } } + + func didReceiveCanPayFeeInPayAsset(_ value: Bool, chainAssetId: ChainAssetId) { + if payChainAsset?.chainAssetId == chainAssetId { + canPayFeeInPayAsset = value + + provideFeeViewModel() + } + } } extension SwapSetupPresenter: Localizable { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 55d3f5491f..1839eed77e 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -39,7 +39,10 @@ protocol SwapSetupInteractorInputProtocol: SwapBaseInteractorInputProtocol { func update(feeChainAsset: ChainAsset?) } -protocol SwapSetupInteractorOutputProtocol: SwapBaseInteractorOutputProtocol {} +protocol SwapSetupInteractorOutputProtocol: SwapBaseInteractorOutputProtocol { + func didReceiveCanPayFeeInPayAsset(_ value: Bool, chainAssetId: ChainAssetId) + func didReceive(setupError: SwapSetupError) +} protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, ErrorPresentable, SwapErrorPresentable, ShortTextInfoPresentable { @@ -75,8 +78,5 @@ protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryabl } enum SwapSetupError: Error { - case quote(Error, AssetConversion.QuoteArgs) - case fetchFeeFailed(Error, TransactionFeeId, FeeChainAssetId?) - case price(Error, AssetModel.PriceId) - case assetBalance(Error, ChainAssetId, AccountId) + case payAssetSetFailed(Error) } From 75a507c064d0869b2bf1e174b15338abe5ad1463 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 2 Nov 2023 06:21:02 +0300 Subject: [PATCH 108/204] add operations --- novawallet/Common/Model/TokenOperation.swift | 16 ++++ .../Cell/SecretTypeTableViewCell.swift | 87 +++++++++++++++++++ .../ModalPicker/ModalPickerFactory.swift | 86 ++++++++++++++++++ .../Swaps/Setup/SwapSetupPresenter.swift | 64 +++++++++++++- .../Swaps/Setup/SwapSetupProtocols.swift | 12 +++ .../Swaps/Setup/SwapSetupViewController.swift | 19 ++++ .../Swaps/Setup/SwapSetupViewFactory.swift | 5 +- .../Swaps/Setup/SwapSetupWireframe.swift | 18 ++++ .../Setup/View/SwapSetupViewLayout.swift | 22 +++-- 9 files changed, 321 insertions(+), 8 deletions(-) diff --git a/novawallet/Common/Model/TokenOperation.swift b/novawallet/Common/Model/TokenOperation.swift index 2c73cd9fce..ea3fc5abea 100644 --- a/novawallet/Common/Model/TokenOperation.swift +++ b/novawallet/Common/Model/TokenOperation.swift @@ -56,11 +56,27 @@ typealias TransferAvailableCheckResult = Bool enum ReceiveAvailableCheckResult { case common(OperationCheckCommonResult) + + var available: Bool { + switch self { + case let .common(operationCheckCommonResult): + return operationCheckCommonResult == .available + } + } } enum BuyAvailableCheckResult { case common(OperationCheckCommonResult) case noBuyOptions + + var available: Bool { + switch self { + case let .common(operationCheckCommonResult): + return operationCheckCommonResult == .available + case .noBuyOptions: + return false + } + } } enum OperationCheckCommonResult { diff --git a/novawallet/Common/ViewController/ModalPicker/Cell/SecretTypeTableViewCell.swift b/novawallet/Common/ViewController/ModalPicker/Cell/SecretTypeTableViewCell.swift index a1a1bfeee1..00ae5fabe2 100644 --- a/novawallet/Common/ViewController/ModalPicker/Cell/SecretTypeTableViewCell.swift +++ b/novawallet/Common/ViewController/ModalPicker/Cell/SecretTypeTableViewCell.swift @@ -21,3 +21,90 @@ final class SecretTypeTableViewCell: IconWithTitleSubtitleTableViewCell { subtitleLabel.font = UIFont.p2Paragraph } } + +final class TokenOperationTableViewCell: UITableViewCell, ModalPickerCellProtocol { + struct Model { + let content: IconWithTitleSubtitleViewModel + let isActive: Bool + } + + private(set) var titleLabel = UILabel() + private(set) var subtitleLabel = UILabel() + private(set) var iconImageView = UIImageView() + let arrowIcon = R.image.iconSmallArrow()?.tinted(with: R.color.colorIconSecondary()!) + + var checkmarked: Bool = false + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupStyle() + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func bind(model: Model) { + titleLabel.text = model.content.title + subtitleLabel.text = model.content.subtitle + iconImageView.image = model.content.icon + + if model.isActive { + titleLabel.textColor = R.color.colorTextPrimary() + subtitleLabel.textColor = R.color.colorTextSecondary() + iconImageView.tintColor = R.color.colorIconPrimary() + accessoryView = UIImageView(image: arrowIcon) + selectionStyle = .default + } else { + titleLabel.textColor = R.color.colorButtonTextInactive() + subtitleLabel.textColor = R.color.colorButtonTextInactive() + iconImageView.tintColor = R.color.colorIconInactive() + accessoryView = nil + selectionStyle = .none + } + } + + private func setupStyle() { + backgroundColor = .clear + + selectedBackgroundView = UIView() + selectedBackgroundView?.backgroundColor = R.color.colorCellBackgroundPressed() + + separatorInset = UIEdgeInsets( + top: 0.0, + left: UIConstants.horizontalInset, + bottom: 0.0, + right: UIConstants.horizontalInset + ) + + titleLabel.apply(style: .regularSubhedlinePrimary) + subtitleLabel.apply(style: .footnoteSecondary) + } + + private func setupLayout() { + contentView.addSubview(iconImageView) + iconImageView.snp.makeConstraints { make in + make.leading.equalToSuperview().inset(UIConstants.horizontalInset) + make.centerY.equalToSuperview() + } + + iconImageView.setContentHuggingPriority(.defaultHigh, for: .horizontal) + iconImageView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + + contentView.addSubview(titleLabel) + titleLabel.snp.makeConstraints { make in + make.top.equalToSuperview().inset(7.0) + make.leading.equalTo(iconImageView.snp.trailing).offset(12.0) + make.trailing.equalToSuperview().inset(8.0) + } + + contentView.addSubview(subtitleLabel) + subtitleLabel.snp.makeConstraints { make in + make.bottom.equalToSuperview().inset(7.0) + make.leading.equalTo(iconImageView.snp.trailing).offset(12.0) + make.trailing.equalToSuperview().inset(8.0) + } + } +} diff --git a/novawallet/Common/ViewController/ModalPicker/ModalPickerFactory.swift b/novawallet/Common/ViewController/ModalPicker/ModalPickerFactory.swift index f63b42f454..4ed42223b7 100644 --- a/novawallet/Common/ViewController/ModalPicker/ModalPickerFactory.swift +++ b/novawallet/Common/ViewController/ModalPicker/ModalPickerFactory.swift @@ -508,3 +508,89 @@ extension ModalPickerFactory { return viewController } } + +extension ModalPickerFactory { + static func createPickerListForOperations( + operations: [(token: TokenOperation, active: Bool)], + delegate: ModalPickerViewControllerDelegate?, + token: String, + context: AnyObject? + ) -> UIViewController? { + guard !operations.isEmpty else { + return nil + } + + let viewController: ModalPickerViewController + = ModalPickerViewController(nib: R.nib.modalPickerViewController) + + viewController.localizedTitle = .init { _ in "Get \(token) using" } + + viewController.selectedIndex = NSNotFound + viewController.delegate = delegate + viewController.modalPresentationStyle = .custom + viewController.context = context + viewController.headerBorderType = .none + viewController.separatorStyle = .none + viewController.separatorColor = R.color.colorDivider() + viewController.cellHeight = 48.0 + + viewController.viewModels = operations.map { operation in + LocalizableResource { locale in + TokenOperationTableViewCell.Model( + content: .init( + title: operation.token.titleForLocale(locale), + subtitle: operation.token.subtitleForLocale(locale, token: token), + icon: operation.token.icon + ), + isActive: operation.active + ) + } + } + + let factory = ModalSheetPresentationFactory(configuration: ModalSheetPresentationConfiguration.nova) + viewController.modalTransitioningFactory = factory + + let height = viewController.headerHeight + CGFloat(operations.count) * viewController.cellHeight + + viewController.footerHeight + viewController.preferredContentSize = CGSize(width: 0.0, height: height) + + viewController.localizationManager = LocalizationManager.shared + + return viewController + } +} + +extension TokenOperation { + func titleForLocale(_: Locale) -> String { + switch self { + case .send: + return "Cross-chain transfer" + case .receive: + return "Receive" + case .buy: + return "Buy" + } + } + + func subtitleForLocale(_: Locale, token: String) -> String { + switch self { + case .send: + return "Transfer \(token) from another network" + case .receive: + return "Receive \(token) with QR or your address" + case .buy: + return "Instantly buy \(token) with a credit card" + } + } + + var icon: UIImage? { + switch self { + case .send: + return R.image.iconSend() + case .receive: + return R.image.iconReceive() + case .buy: + return R.image.iconBuy() + } + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 44d288d182..0eecc823a0 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -8,6 +8,8 @@ final class SwapSetupPresenter { let interactor: SwapSetupInteractorInputProtocol let dataValidatingFactory: SwapDataValidatorFactoryProtocol let logger: LoggerProtocol + let selectedAccount: MetaAccountModel + let purchaseProvider: PurchaseProviderProtocol private(set) var viewModelFactory: SwapsSetupViewModelFactoryProtocol private(set) var payAssetBalance: AssetBalance? @@ -18,7 +20,6 @@ final class SwapSetupPresenter { private(set) var payAssetPriceData: PriceData? private(set) var receiveAssetPriceData: PriceData? private(set) var feeAssetPriceData: PriceData? - private(set) var payAmountInput: AmountInputResult? private(set) var receiveAmountInput: Decimal? private(set) var fee: AssetConversion.FeeModel? @@ -40,6 +41,8 @@ final class SwapSetupPresenter { viewModelFactory: SwapsSetupViewModelFactoryProtocol, dataValidatingFactory: SwapDataValidatorFactoryProtocol, localizationManager: LocalizationManagerProtocol, + selectedAccount: MetaAccountModel, + purchaseProvider: PurchaseProviderProtocol, logger: LoggerProtocol ) { self.interactor = interactor @@ -47,7 +50,8 @@ final class SwapSetupPresenter { self.viewModelFactory = viewModelFactory self.dataValidatingFactory = dataValidatingFactory self.logger = logger - + self.selectedAccount = selectedAccount + self.purchaseProvider = purchaseProvider self.localizationManager = localizationManager } @@ -227,6 +231,21 @@ final class SwapSetupPresenter { view?.didReceiveNetworkFee(viewModel: .loaded(value: viewModel)) } + private func provideErrors() { + guard let payAmount = getPayAmount(for: payAmountInput) else { + view?.didReceive(errors: []) + return + } + var errors: [SwapSetupViewError] = [] + let balanceMinusFee = balanceMinusFee() ?? 0 + + if payAmount > balanceMinusFee { + errors.append(.insufficientToken) + } + + view?.didReceive(errors: errors) + } + func estimateFee() { guard let quote = quote, let accountId = accountId, @@ -397,6 +416,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideSettingsState() // TODO: get from settings slippage = .fraction(from: AssetConversionConstants.defaultSlippage)?.fromPercents() + provideErrors() interactor.setup() } @@ -416,6 +436,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { self?.interactor.update(feeChainAsset: feeChainAsset) self?.refreshQuote(direction: .sell, forceUpdate: false) + self?.provideErrors() } } @@ -433,6 +454,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { payAmountInput = amount.map { .absolute($0) } refreshQuote(direction: .sell) provideButtonState() + provideErrors() } func updateReceiveAmount(_ amount: Decimal?) { @@ -459,6 +481,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { providePayAssetViews() refreshQuote(direction: .sell) provideButtonState() + provideErrors() } func showFeeActions() { @@ -552,6 +575,34 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { self?.estimateFee() } } + + func depositInsufficientToken() { + guard let payChainAsset = payChainAsset, let accountId = accountId else { + return + } + let purchaseActions = purchaseProvider.buildPurchaseActions(for: payChainAsset, accountId: accountId) + let sendAvailable = TokenOperation.checkTransferOperationAvailable() + let recieveAvailable = TokenOperation.checkReceiveOperationAvailable( + walletType: selectedAccount.type, + chainAsset: payChainAsset + ).available + let buyAvailable = TokenOperation.checkBuyOperationAvailable( + purchaseActions: purchaseActions, + walletType: selectedAccount.type, + chainAsset: payChainAsset + ).available + let operations: [(token: TokenOperation, active: Bool)] = [ + (token: .send, active: sendAvailable), + (token: .receive, active: recieveAvailable), + (token: .buy, active: buyAvailable) + ] + wireframe.showTokenDepositOptions( + form: view, + operations: operations, + token: payChainAsset.asset.symbol, + delegate: self + ) + } } extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { @@ -629,6 +680,7 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { self.fee = fee provideFeeViewModel() provideButtonState() + provideErrors() } func didReceive(price: PriceData?, priceId: AssetModel.PriceId) { @@ -648,12 +700,14 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { func didReceive(payAccountId: AccountId?) { accountId = payAccountId + provideErrors() } func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId _: AccountId) { if chainAsset == payChainAsset?.chainAssetId { payAssetBalance = balance providePayTitle() + provideErrors() } if chainAsset == feeChainAsset?.chainAssetId { feeAssetBalance = balance @@ -674,3 +728,9 @@ extension SwapSetupPresenter: Localizable { } } } + +extension SwapSetupPresenter: ModalPickerViewControllerDelegate { + func modalPickerDidSelectModelAtIndex(_: Int, context _: AnyObject?) {} + + func modalPickerDidSelectModel(at _: Int, section _: Int, context _: AnyObject?) {} +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 55d3f5491f..51df7081f4 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -15,6 +15,7 @@ protocol SwapSetupViewProtocol: ControllerBackedProtocol { func didReceiveNetworkFee(viewModel: LoadableViewModelState) func didReceiveDetailsState(isAvailable: Bool) func didReceiveSettingsState(isAvailable: Bool) + func didReceive(errors: [SwapSetupViewError]) } protocol SwapSetupPresenterProtocol: AnyObject { @@ -30,6 +31,7 @@ protocol SwapSetupPresenterProtocol: AnyObject { func showRateInfo() func showSettings() func selectMaxPayAmount() + func depositInsufficientToken() } protocol SwapSetupInteractorInputProtocol: SwapBaseInteractorInputProtocol { @@ -72,6 +74,12 @@ protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryabl form view: ControllerBackedProtocol?, viewModel: SwapNetworkFeeSheetViewModel ) + func showTokenDepositOptions( + form view: ControllerBackedProtocol?, + operations: [(token: TokenOperation, active: Bool)], + token: String, + delegate: ModalPickerViewControllerDelegate? + ) } enum SwapSetupError: Error { @@ -80,3 +88,7 @@ enum SwapSetupError: Error { case price(Error, AssetModel.PriceId) case assetBalance(Error, ChainAssetId, AccountId) } + +enum SwapSetupViewError { + case insufficientToken +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 2cb0e77297..dfe04245a2 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -84,6 +84,11 @@ final class SwapSetupViewController: UIViewController, ViewHolder { action: #selector(networkFeeInfoAction), for: .touchUpInside ) + rootView.depositTokenButton.addTarget( + self, + action: #selector(depositTokenAction), + for: .touchUpInside + ) } private func setupLocalization() { @@ -164,6 +169,10 @@ final class SwapSetupViewController: UIViewController, ViewHolder { @objc private func settingsAction() { presenter.showSettings() } + + @objc private func depositTokenAction() { + presenter.depositInsufficientToken() + } } extension SwapSetupViewController: SwapSetupViewProtocol { @@ -179,8 +188,10 @@ extension SwapSetupViewController: SwapSetupViewProtocol { switch viewModel { case let .asset(assetViewModel): rootView.payAmountInputView.bind(assetViewModel: assetViewModel) + rootView.depositTokenButton.imageWithTitleView?.title = "Get \(assetViewModel.symbol)" case let .empty(emptySwapsAssetViewModel): rootView.payAmountInputView.bind(emptyViewModel: emptySwapsAssetViewModel) + rootView.depositTokenButton.imageWithTitleView?.title = nil } } @@ -228,6 +239,14 @@ extension SwapSetupViewController: SwapSetupViewProtocol { func didReceiveSettingsState(isAvailable: Bool) { navigationItem.rightBarButtonItem?.isEnabled = isAvailable } + + func didReceive(errors: [SwapSetupViewError]) { + if errors.contains(.insufficientToken) { + rootView.changeDepositTokenButtonVisibility(hidden: false) + } else { + rootView.changeDepositTokenButtonVisibility(hidden: true) + } + } } extension SwapSetupViewController: Localizable { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index 5e1f360e7b..fded1063b1 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -5,7 +5,8 @@ import RobinHood struct SwapSetupViewFactory { static func createView(assetListObservable: AssetListModelObservable) -> SwapSetupViewProtocol? { guard - let currencyManager = CurrencyManager.shared else { + let currencyManager = CurrencyManager.shared, + let selectedWallet = SelectedWalletSettings.shared.value else { return nil } @@ -34,6 +35,8 @@ struct SwapSetupViewFactory { viewModelFactory: viewModelFactory, dataValidatingFactory: dataValidatingFactory, localizationManager: LocalizationManager.shared, + selectedAccount: selectedWallet, + purchaseProvider: PurchaseAggregator.defaultAggregator(), logger: Logger.shared ) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index 1e91ad7af5..bb1dd38319 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -98,4 +98,22 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { view?.controller.present(bottomSheet.controller, animated: true) } + + func showTokenDepositOptions( + form view: ControllerBackedProtocol?, + operations: [(token: TokenOperation, active: Bool)], + token: String, + delegate: ModalPickerViewControllerDelegate? + ) { + guard let bottomSheet = ModalPickerFactory.createPickerListForOperations( + operations: operations, + delegate: delegate, + token: token, + context: nil + ) else { + return + } + + view?.controller.present(bottomSheet, animated: true) + } } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index d230d35f81..67dcb383fc 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -5,11 +5,10 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { let payAmountView = SwapSetupTitleView(frame: .zero) let payAmountInputView = SwapAmountInputView() - + let depositTokenButton: TriangularedButton = .create { - $0.applySecondaryStyle() + $0.applySecondaryDefaultStyle() $0.imageWithTitleView?.titleColor = R.color.colorButtonTextAccent() - $0.isHidden = true } let receiveAmountView: TitleHorizontalMultiValueView = .create { @@ -61,10 +60,14 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { payAmountView.snp.makeConstraints { $0.height.equalTo(18) } - addArrangedSubview(payAmountInputView, spacingAfter: 24) + addArrangedSubview(payAmountInputView, spacingAfter: 12) payAmountInputView.snp.makeConstraints { $0.height.equalTo(64) } + addArrangedSubview(depositTokenButton, spacingAfter: 24) + depositTokenButton.snp.makeConstraints { + $0.height.equalTo(44) + } addArrangedSubview(receiveAmountView, spacingAfter: 8) receiveAmountView.snp.makeConstraints { $0.height.equalTo(18) @@ -79,7 +82,6 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { addSubview(switchButton) switchButton.snp.makeConstraints { $0.height.equalTo(switchButton.snp.width) - $0.top.equalTo(payAmountInputView.snp.bottom).offset(4) $0.bottom.equalTo(receiveAmountInputView.snp.top).offset(-4) $0.centerX.equalTo(payAmountInputView.snp.centerX) } @@ -96,4 +98,14 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { rateCell.titleButton.invalidateLayout() networkFeeCell.titleButton.invalidateLayout() } + + func changeDepositTokenButtonVisibility(hidden: Bool) { + if hidden { + stackView.setCustomSpacing(24, after: payAmountInputView) + } else { + stackView.setCustomSpacing(12, after: payAmountInputView) + } + depositTokenButton.isHidden = hidden + setNeedsLayout() + } } From f57bae8048ee9104bc357bd5418d8052d3f1f383 Mon Sep 17 00:00:00 2001 From: ERussel Date: Thu, 2 Nov 2023 05:34:20 +0100 Subject: [PATCH 109/204] fix typos --- .../SwapNetworkFeeSheetViewController.swift | 4 +++- .../SwapNetworkFeeSheetViewFactory.swift | 5 +---- novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift | 2 +- novawallet/ru.lproj/Localizable.strings | 6 +++--- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewController.swift b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewController.swift index 7fde440973..d661a3a7e9 100644 --- a/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewController.swift +++ b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewController.swift @@ -42,7 +42,9 @@ final class SwapNetworkFeeSheetViewController: UIViewController, ViewHolder { rootView.titleLabel.text = viewModel.title.value(for: selectedLocale) rootView.detailsLabel.text = viewModel.message.value(for: selectedLocale) rootView.hint.detailsLabel.text = viewModel.hint.value(for: selectedLocale) - rootView.feeTypeSwitch.titles = (0 ..< viewModel.count).map { viewModel.sectionTitle($0).value(for: selectedLocale) } + rootView.feeTypeSwitch.titles = (0 ..< viewModel.count).map { index in + viewModel.sectionTitle(index).value(for: selectedLocale) + } } private func setupSwitch() { diff --git a/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewFactory.swift b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewFactory.swift index 5bcbc63fc0..11780e5d84 100644 --- a/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetViewFactory.swift @@ -2,10 +2,7 @@ import Foundation import SoraFoundation struct SwapNetworkFeeSheetViewFactory { - static func createView( - from viewModel: SwapNetworkFeeSheetViewModel, - allowsSwipeDown _: Bool = true - ) -> MessageSheetViewProtocol { + static func createView(from viewModel: SwapNetworkFeeSheetViewModel) -> MessageSheetViewProtocol { let wireframe = MessageSheetWireframe() let presenter = MessageSheetPresenter(wireframe: wireframe) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index c08c2190a1..e7b2cfec78 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -596,7 +596,7 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { logger.error("Did receive setup error: \(setupError)") switch setupError { - case let .payAssetSetFailed(error): + case .payAssetSetFailed: wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in if let payChainAsset = self?.payChainAsset { self?.interactor.update(payChainAsset: payChainAsset) diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index cbe4848c2d..5bc5966fa7 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -473,7 +473,7 @@ "staking.story.validator.page.1" = "Валидатор обеспечивает работу ноды блокчейна 24/7 и обязан иметь необходимое количество стейка (общий стейк самого валидатора и его номинаторов), чтобы быть избранным сетью. Валидаторы должны поддерживать производительность и надежность своих нод, за что они получают вознаграждения. Валидатор — это полноценная работа, существуют профильные компании, которые специализируются на валидировании в блокчейн сетях."; "staking.story.validator.page.2" = "Любой может стать валидатором и запустить ноду блокчейна, однако это требует определённых технических знаний и ответственности. Сети Polkadot и Kusama запустили программу Thousand Validators Programme (Программа Тысячи Валидаторов), чтобы помочь начинающим. Более того, сеть всегда будет стремиться вознаграждать тех валидаторов, чей суммарный стейк меньше (но достаточен чтобы быть избранным в сети), для поддержки децентрализации."; "staking.story.reward.title" = "Получение наград"; -"staking.story.reward.page.1" = "Вознаграждения за стейкинг доступны для выплаты в конце каждой эры (6 часов в Kusama и 24 часа в Polkadot). Сеть хранит ожидаемые вознаграждения в течении 84 эр и в большинстве случаев валидаторы сами выплачивают вс��м награды. Однако, валидаторы могут забыть это сделать или с ними может что-то случиться, поэтому номинаторы могут выплатить свои награды самостоятельно."; +"staking.story.reward.page.1" = "Вознаграждения за стейкинг доступны для выплаты в конце каждой эры (6 часов в Kusama и 24 часа в Polkadot). Сеть хранит ожидаемые вознаграждения в течении 84 эр и в большинстве случаев валидаторы сами выплачивают всем награды. Однако, валидаторы могут забыть это сделать или с ними может что-то случиться, поэтому номинаторы могут выплатить свои награды самостоятельно."; "staking.story.reward.page.2" = "Несмотря на то, что обычно вознаграждения выплачиваются валидаторами, Nova Wallet помогает узнать о вознаграждениях, срок выплаты которых близок к истечению, с помощью предупреждений. Предупреждения об этом и других важных событиях появятся на главном экране стейкинга."; "common.cancel.operation.message" = "Вы уверены, что хотите отменить операцию?"; "common.cancel.operation.action" = "Отменить операцию"; @@ -621,7 +621,7 @@ "staking.hint.rewards.format_v2_2_0" = "Токены в стейке приносят награду каждую эру (%@)"; "staking.hint.unstake.format_v2_2_0" = "Период вывода токенов из стейкинга занимает %@"; "staking.set.separate.account.controller_v2_2_0" = "Установите отдельный аккаунт как контроллер, чтобы увеличить безопасность стейкинга делегируя ему управление стейком"; -"staking.controller.can.hint_v2_2_0" = "Контроллер аккаунт может: вывести, забрать, ве��нуть в стейк, сменить валидаторов и установить назначение вознаграждений"; +"staking.controller.can.hint_v2_2_0" = "Контроллер аккаунт может: вывести, забрать, вернуть в стейк, сменить валидаторов и установить назначение вознаграждений"; "staking.stash.can.hint_v2_2_0" = "Стэш аккаунт может: застейкать больше и установить контроллер аккаунт"; "staking.network.info.title" = "Информация о стейкинге"; "staking.network.info.staking.period.value" = "Неограниченно"; @@ -791,7 +791,7 @@ "parastk.manage.collators" = "Управление коллаторами"; "parastk.pending.revoke.message" = "Вы не можете добавить стейк в коллатора, для которого вы разблокируете все токены."; "parastk.cant.bond.more.title" = "Невозможно добавить стейк в выбранного коллатора"; -"parastk.not.active.collator.message" = "Выбранный кол��атор намерен прекратить участие в стейкинге."; +"parastk.not.active.collator.message" = "Выбранный коллатор намерен прекратить участие в стейкинге."; "parastk.not.active.collator.title" = "Невозможно застейкать с выбранным коллатором"; "parachain.staking.delegator.exists.title" = "Стекинг активирован ранее"; "parachain.staking.delegator.exists.message" = "Стекинг был активирован ранее. Для того чтобы застейкать еще токенов использовать соответствующее действие."; From a34680f84f9b5c9767feb87a0ae59ea98b48926a Mon Sep 17 00:00:00 2001 From: ERussel Date: Thu, 2 Nov 2023 06:34:37 +0100 Subject: [PATCH 110/204] fix fee calculation on confirm --- novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift | 7 ++++++- .../Modules/Swaps/Confirm/SwapConfirmPresenter.swift | 4 ++-- novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift | 3 ++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift index b92dea68b7..1aff798ff9 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift @@ -57,6 +57,8 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB feeChainAssetId: feeChainAssetId ) } + + assetBalanceProviders[utilityAsset.chainAssetId] = assetBalanceSubscription(chainAsset: utilityAsset) } func updateSubscriptions(activeChainAssets: Set) { @@ -138,7 +140,9 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB case let .success(feeModel): self?.feeModelBuilder?.apply(feeModel: feeModel, args: args) case let .failure(error): - self?.basePresenter?.didReceive(baseError: .fetchFeeFailed(error, args.identifier, feeAsset.chainAssetId)) + self?.basePresenter?.didReceive( + baseError: .fetchFeeFailed(error, args.identifier, feeAsset.chainAssetId) + ) } } } @@ -154,6 +158,7 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB updateFeeModelBuilder(for: chainAsset.chain) priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) + assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) } func set(payChainAsset chainAsset: ChainAsset) { diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 96f9d82abe..c560b18f0a 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -408,7 +408,7 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in self?.interactor.calculateQuote(for: args) } - case let .fetchFeeFailed: + case .fetchFeeFailed: wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in self?.estimateFee() } @@ -422,7 +422,7 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { .filter { $0.asset.priceId == priceId } .forEach(self.interactor.remakePriceSubscription) } - case let .assetBalance: + case .assetBalance: wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in self?.interactor.setup() } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index e37d4d1951..75f380737b 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -30,7 +30,8 @@ final class SwapSetupInteractor: SwapBaseInteractor { [ receiveChainAsset?.chainAssetId, payChainAsset?.chainAssetId, - feeChainAsset?.chainAssetId + feeChainAsset?.chainAssetId, + feeChainAsset?.chain.utilityChainAssetId() ].compactMap { $0 } ) } From 366d5a58e7786d73aba0e451593cbf28d2e27b4d Mon Sep 17 00:00:00 2001 From: ERussel Date: Thu, 2 Nov 2023 08:53:28 +0100 Subject: [PATCH 111/204] pay fee in custom token --- novawallet.xcodeproj/project.pbxproj | 4 ++ .../Substrate/ExtrinsicServiceFactory.swift | 4 +- .../Extension/AssetConversionTxPayment.swift | 15 +++++++ .../Extension/ExtrinsicExtension.swift | 4 +- .../Swaps/Confirm/SwapConfirmInteractor.swift | 40 +++++++++++++++---- .../Swaps/Confirm/SwapConfirmProtocols.swift | 2 +- .../Confirm/SwapConfirmViewFactory.swift | 6 +-- 7 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 novawallet/Common/Substrate/Extension/AssetConversionTxPayment.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index b8a895c7d4..b9e2f06be8 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -278,6 +278,7 @@ 0CEB4ED12AF14B8B0048FD84 /* SubmoduleNavigationStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEB4ED02AF14B8B0048FD84 /* SubmoduleNavigationStrategy.swift */; }; 0CEB4ED32AF1689D0048FD84 /* AssetConversionAggregationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEB4ED22AF1689D0048FD84 /* AssetConversionAggregationFactory.swift */; }; 0CEB4ED52AF20EB90048FD84 /* CancellableCallHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEB4ED42AF20EB90048FD84 /* CancellableCallHelper.swift */; }; + 0CEB4ED92AF371EF0048FD84 /* AssetConversionTxPayment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEB4ED82AF371EF0048FD84 /* AssetConversionTxPayment.swift */; }; 0CF193D12A843DA9003F12F6 /* StakingTypeBalanceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D02A843DA9003F12F6 /* StakingTypeBalanceFactory.swift */; }; 0CF193D32A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D22A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift */; }; 0CF193D52A861926003F12F6 /* PredefinedTimeShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D42A861925003F12F6 /* PredefinedTimeShortcut.swift */; }; @@ -4336,6 +4337,7 @@ 0CEB4ED02AF14B8B0048FD84 /* SubmoduleNavigationStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmoduleNavigationStrategy.swift; sourceTree = ""; }; 0CEB4ED22AF1689D0048FD84 /* AssetConversionAggregationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionAggregationFactory.swift; sourceTree = ""; }; 0CEB4ED42AF20EB90048FD84 /* CancellableCallHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellableCallHelper.swift; sourceTree = ""; }; + 0CEB4ED82AF371EF0048FD84 /* AssetConversionTxPayment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionTxPayment.swift; sourceTree = ""; }; 0CF193D02A843DA9003F12F6 /* StakingTypeBalanceFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StakingTypeBalanceFactory.swift; sourceTree = ""; }; 0CF193D22A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedStakingViewModelFactory.swift; sourceTree = ""; }; 0CF193D42A861925003F12F6 /* PredefinedTimeShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PredefinedTimeShortcut.swift; sourceTree = ""; }; @@ -16123,6 +16125,7 @@ isa = PBXGroup; children = ( 84DEE7032797FBF800B9A39E /* ExtrinsicExtension.swift */, + 0CEB4ED82AF371EF0048FD84 /* AssetConversionTxPayment.swift */, ); path = Extension; sourceTree = ""; @@ -21128,6 +21131,7 @@ 84757E19299A2E2F00616C6C /* Gov2SubscriptionFactory+Referendum.swift in Sources */, 8473F4BA282C012B007CC55A /* StakingRelaychainInteractor+InputProtocol.swift in Sources */, 84E0EE0C292D402C008B2953 /* GovernanceAssetSelectionViewFactory.swift in Sources */, + 0CEB4ED92AF371EF0048FD84 /* AssetConversionTxPayment.swift in Sources */, 848DAEFE282293DB00D56F55 /* ParachainStaking+Types.swift in Sources */, 8489A6E227FEFC730040C066 /* StakingRebondOption.swift in Sources */, 84CFF1E626526FBC00DB7CF7 /* StakingBondMoreInteractor.swift in Sources */, diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceFactory.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceFactory.swift index b59b704f97..08bb8a2d46 100644 --- a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceFactory.swift +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceFactory.swift @@ -31,12 +31,12 @@ extension ExtrinsicServiceFactoryProtocol { func createService( account: ChainAccountResponse, chain: ChainModel, - feeAssetId: UInt32 + feeAssetConversionId: AssetConversionPallet.AssetId ) -> ExtrinsicServiceProtocol { createService( account: account, chain: chain, - extensions: DefaultExtrinsicExtension.extensions(payingFeeIn: feeAssetId) + extensions: DefaultExtrinsicExtension.extensions(payingFeeIn: feeAssetConversionId) ) } diff --git a/novawallet/Common/Substrate/Extension/AssetConversionTxPayment.swift b/novawallet/Common/Substrate/Extension/AssetConversionTxPayment.swift new file mode 100644 index 0000000000..94082b9dfb --- /dev/null +++ b/novawallet/Common/Substrate/Extension/AssetConversionTxPayment.swift @@ -0,0 +1,15 @@ +import Foundation +import BigInt +import SubstrateSdk + +class AssetConversionTxPayment: Codable, ExtrinsicExtension { + public static let name: String = "ChargeAssetTxPayment" + + @StringCodable public var tip: BigUInt + let assetId: AssetConversionPallet.AssetId? + + init(tip: BigUInt = 0, assetId: AssetConversionPallet.AssetId? = nil) { + self.tip = tip + self.assetId = assetId + } +} diff --git a/novawallet/Common/Substrate/Extension/ExtrinsicExtension.swift b/novawallet/Common/Substrate/Extension/ExtrinsicExtension.swift index 8fb97e4ff1..632ea73109 100644 --- a/novawallet/Common/Substrate/Extension/ExtrinsicExtension.swift +++ b/novawallet/Common/Substrate/Extension/ExtrinsicExtension.swift @@ -8,9 +8,9 @@ enum DefaultExtrinsicExtension { ] } - static func extensions(payingFeeIn assetId: UInt32) -> [ExtrinsicExtension] { + static func extensions(payingFeeIn assetId: AssetConversionPallet.AssetId) -> [ExtrinsicExtension] { [ - ChargeAssetTxPayment(assetId: assetId) + AssetConversionTxPayment(assetId: assetId) ] } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift index e0438b072d..eb1bd17148 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift @@ -8,7 +8,7 @@ final class SwapConfirmInteractor: SwapBaseInteractor { let initState: SwapConfirmInitState let runtimeService: RuntimeProviderProtocol - let extrinsicService: ExtrinsicServiceProtocol + let extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol let assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol let signer: SigningWrapperProtocol @@ -18,7 +18,7 @@ final class SwapConfirmInteractor: SwapBaseInteractor { assetConversionAggregator: AssetConversionAggregationFactoryProtocol, assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol, runtimeService: RuntimeProviderProtocol, - extrinsicService: ExtrinsicServiceProtocol, + extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, currencyManager: CurrencyManagerProtocol, @@ -29,7 +29,7 @@ final class SwapConfirmInteractor: SwapBaseInteractor { self.initState = initState self.signer = signer self.runtimeService = runtimeService - self.extrinsicService = extrinsicService + self.extrinsicServiceFactory = extrinsicServiceFactory self.assetConversionExtrinsicService = assetConversionExtrinsicService super.init( @@ -65,7 +65,7 @@ final class SwapConfirmInteractor: SwapBaseInteractor { for: args, codingFactory: runtimeCoderFactory ) - self.submitClosure(extrinsicService: self.extrinsicService, builder: builder) + try self.submitClosure(builder: builder, runtimeCoderFactory: runtimeCoderFactory) } catch { self.presenter?.didReceive(error: .submit(error)) } @@ -76,9 +76,35 @@ final class SwapConfirmInteractor: SwapBaseInteractor { } private func submitClosure( - extrinsicService: ExtrinsicServiceProtocol, - builder: @escaping ExtrinsicBuilderClosure - ) { + builder: @escaping ExtrinsicBuilderClosure, + runtimeCoderFactory: RuntimeCoderFactoryProtocol + ) throws { + let extrinsicService: ExtrinsicServiceProtocol + + guard let account = chainAccountResponse(for: initState.feeChainAsset) else { + throw ChainAccountFetchingError.accountNotExists + } + + if initState.feeChainAsset.isUtilityAsset { + extrinsicService = extrinsicServiceFactory.createService( + account: account, + chain: initState.feeChainAsset.chain + ) + } else { + guard let assetId = AssetHubTokensConverter.convertToMultilocation( + chainAsset: initState.feeChainAsset, + codingFactory: runtimeCoderFactory + ) else { + throw SwapConfirmError.submit(CommonError.dataCorruption) + } + + extrinsicService = extrinsicServiceFactory.createService( + account: account, + chain: initState.feeChainAsset.chain, + feeAssetConversionId: assetId + ) + } + extrinsicService.submit( builder, signer: signer, diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift index 8945309686..36ccaa85b6 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift @@ -37,6 +37,6 @@ protocol SwapConfirmWireframeProtocol: AnyObject, AlertPresentable, CommonRetrya func complete(on view: ControllerBackedProtocol?, locale: Locale) } -enum SwapConfirmError { +enum SwapConfirmError: Error { case submit(Error) } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index cd4d1657cb..aac228b127 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -82,11 +82,11 @@ struct SwapConfirmViewFactory { operationQueue: operationQueue ) - let extrinsicService = ExtrinsicServiceFactory( + let extrinsicServiceFactory = ExtrinsicServiceFactory( runtimeRegistry: runtimeService, engine: connection, operationManager: OperationManager(operationQueue: operationQueue) - ).createService(account: selectedAccount.chainAccount, chain: chain) + ) let feeService = AssetHubFeeService( wallet: wallet, @@ -105,7 +105,7 @@ struct SwapConfirmViewFactory { assetConversionAggregator: assetConversionAggregator, assetConversionExtrinsicService: AssetHubExtrinsicService(chain: chain), runtimeService: runtimeService, - extrinsicService: extrinsicService, + extrinsicServiceFactory: extrinsicServiceFactory, priceLocalSubscriptionFactory: PriceProviderFactory.shared, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, currencyManager: currencyManager, From 278bfb4e2b99e90338aeb734ec0795b29837540d Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 2 Nov 2023 12:18:26 +0300 Subject: [PATCH 112/204] add xcm --- novawallet.xcodeproj/project.pbxproj | 28 ++++-- .../Contents.json | 12 +++ .../cross-chain.pdf | Bin 0 -> 2023 bytes .../Common/Model/Xcm/XcmTransfers.swift | 13 +++ .../Cell/SecretTypeTableViewCell.swift | 87 ----------------- .../Cell/TokenOperationTableViewCell.swift | 88 ++++++++++++++++++ .../ModalPicker/ModalPickerFactory.swift | 47 ++-------- .../Swaps/Base/SwapBaseProtocols.swift | 9 +- .../Swaps/Confirm/SwapConfirmPresenter.swift | 2 +- .../Swaps/Setup/Model/SwapModels.swift | 61 ++++++++++++ ...{ViewModels.swift => SwapViewModels.swift} | 5 - .../Swaps/Setup/SwapSetupInteractor.swift | 86 +++++++++++++++++ .../Swaps/Setup/SwapSetupPresenter.swift | 85 ++++++++++++++--- .../Swaps/Setup/SwapSetupProtocols.swift | 27 ++++-- .../Swaps/Setup/SwapSetupViewFactory.swift | 7 ++ .../Swaps/Setup/SwapSetupWireframe.swift | 36 ++++++- .../TransferSetupViewFactory.swift | 35 ++++++- novawallet/en.lproj/Localizable.strings | 5 + novawallet/ru.lproj/Localizable.strings | 5 + 19 files changed, 469 insertions(+), 169 deletions(-) create mode 100644 novawallet/Assets.xcassets/iconCrossChainTransfer.imageset/Contents.json create mode 100644 novawallet/Assets.xcassets/iconCrossChainTransfer.imageset/cross-chain.pdf create mode 100644 novawallet/Common/ViewController/ModalPicker/Cell/TokenOperationTableViewCell.swift create mode 100644 novawallet/Modules/Swaps/Setup/Model/SwapModels.swift rename novawallet/Modules/Swaps/Setup/Model/{ViewModels.swift => SwapViewModels.swift} (91%) diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index cbc47efaf3..2390243217 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -797,7 +797,9 @@ 77AB55592AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */; }; 77AB555B2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */; }; 77AB555D2AA24BA90058814E /* OperationDetailsPoolRewardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */; }; - 77C9BCBC2ACD1AF500022EA2 /* ViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */; }; + 77C976202AF36A170049272C /* SwapModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9761F2AF36A170049272C /* SwapModels.swift */; }; + 77C976222AF39F180049272C /* TokenOperationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C976212AF39F180049272C /* TokenOperationTableViewCell.swift */; }; + 77C9BCBC2ACD1AF500022EA2 /* SwapViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBB2ACD1AF500022EA2 /* SwapViewModels.swift */; }; 77C9BCBE2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */; }; 77C9BCC42ACD570100022EA2 /* SwapAssetsOperationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */; }; 77C9BCC62ACD571400022EA2 /* SwapAssetOperationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCC52ACD571400022EA2 /* SwapAssetOperationPresenter.swift */; }; @@ -818,13 +820,13 @@ 77E0DC9E2A6940C400D03724 /* Calendar+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E0DC9D2A6940C400D03724 /* Calendar+Helpers.swift */; }; 77E255672A16145500B644C3 /* StakingRewardsFilterMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E255662A16145500B644C3 /* StakingRewardsFilterMapper.swift */; }; 77E255692A16148000B644C3 /* StakingRewardsFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E255682A16148000B644C3 /* StakingRewardsFilter.swift */; }; + 77E304A92AEB9F76006FD6F0 /* SwapConfirmInitState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304A82AEB9F76006FD6F0 /* SwapConfirmInitState.swift */; }; + 77E304AB2AEBB214006FD6F0 /* SlippageBounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304AA2AEBB214006FD6F0 /* SlippageBounds.swift */; }; + 77E304AD2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304AC2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift */; }; 77E304B02AEFA261006FD6F0 /* SwapNetworkFeeSheetLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304AF2AEFA261006FD6F0 /* SwapNetworkFeeSheetLayout.swift */; }; 77E304B22AEFC0F3006FD6F0 /* SwapNetworkFeeSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304B12AEFC0F3006FD6F0 /* SwapNetworkFeeSheetViewController.swift */; }; 77E304B52AEFC303006FD6F0 /* SwapNetworkFeeSheetViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304B42AEFC303006FD6F0 /* SwapNetworkFeeSheetViewFactory.swift */; }; 77E304B72AEFC348006FD6F0 /* SwapNetworkFeeSheetViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304B62AEFC348006FD6F0 /* SwapNetworkFeeSheetViewModel.swift */; }; - 77E304A92AEB9F76006FD6F0 /* SwapConfirmInitState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304A82AEB9F76006FD6F0 /* SwapConfirmInitState.swift */; }; - 77E304AB2AEBB214006FD6F0 /* SlippageBounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304AA2AEBB214006FD6F0 /* SlippageBounds.swift */; }; - 77E304AD2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E304AC2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift */; }; 77EA2A232A333C1500B0670B /* french_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A182A333C1500B0670B /* french_output.json */; }; 77EA2A242A333C1500B0670B /* arrays_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A192A333C1500B0670B /* arrays_output.json */; }; 77EA2A252A333C1500B0670B /* weird_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77EA2A1A2A333C1500B0670B /* weird_output.json */; }; @@ -4850,7 +4852,9 @@ 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashModel.swift; sourceTree = ""; }; 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashViewModel.swift; sourceTree = ""; }; 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsPoolRewardView.swift; sourceTree = ""; }; - 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModels.swift; sourceTree = ""; }; + 77C9761F2AF36A170049272C /* SwapModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapModels.swift; sourceTree = ""; }; + 77C976212AF39F180049272C /* TokenOperationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenOperationTableViewCell.swift; sourceTree = ""; }; + 77C9BCBB2ACD1AF500022EA2 /* SwapViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapViewModels.swift; sourceTree = ""; }; 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapsSetupViewModelFactory.swift; sourceTree = ""; }; 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationInteractor.swift; sourceTree = ""; }; 77C9BCC52ACD571400022EA2 /* SwapAssetOperationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetOperationPresenter.swift; sourceTree = ""; }; @@ -4872,13 +4876,13 @@ 77E255652A16059A00B644C3 /* MultiassetUserDataModel9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel9.xcdatamodel; sourceTree = ""; }; 77E255662A16145500B644C3 /* StakingRewardsFilterMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardsFilterMapper.swift; sourceTree = ""; }; 77E255682A16148000B644C3 /* StakingRewardsFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardsFilter.swift; sourceTree = ""; }; + 77E304A82AEB9F76006FD6F0 /* SwapConfirmInitState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConfirmInitState.swift; sourceTree = ""; }; + 77E304AA2AEBB214006FD6F0 /* SlippageBounds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlippageBounds.swift; sourceTree = ""; }; + 77E304AC2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPriceDifferenceViewModelFactoryProtocol.swift; sourceTree = ""; }; 77E304AF2AEFA261006FD6F0 /* SwapNetworkFeeSheetLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeSheetLayout.swift; sourceTree = ""; }; 77E304B12AEFC0F3006FD6F0 /* SwapNetworkFeeSheetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeSheetViewController.swift; sourceTree = ""; }; 77E304B42AEFC303006FD6F0 /* SwapNetworkFeeSheetViewFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeSheetViewFactory.swift; sourceTree = ""; }; 77E304B62AEFC348006FD6F0 /* SwapNetworkFeeSheetViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapNetworkFeeSheetViewModel.swift; sourceTree = ""; }; - 77E304A82AEB9F76006FD6F0 /* SwapConfirmInitState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConfirmInitState.swift; sourceTree = ""; }; - 77E304AA2AEBB214006FD6F0 /* SlippageBounds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlippageBounds.swift; sourceTree = ""; }; - 77E304AC2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPriceDifferenceViewModelFactoryProtocol.swift; sourceTree = ""; }; 77EA2A182A333C1500B0670B /* french_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = french_output.json; sourceTree = ""; }; 77EA2A192A333C1500B0670B /* arrays_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = arrays_output.json; sourceTree = ""; }; 77EA2A1A2A333C1500B0670B /* weird_output.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = weird_output.json; sourceTree = ""; }; @@ -9910,7 +9914,8 @@ 77C9BCBA2ACD1AE800022EA2 /* Model */ = { isa = PBXGroup; children = ( - 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */, + 77C9BCBB2ACD1AF500022EA2 /* SwapViewModels.swift */, + 77C9761F2AF36A170049272C /* SwapModels.swift */, 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */, ); path = Model; @@ -15956,6 +15961,7 @@ 8436B6D728480D2F00F24360 /* ModalPickerActionTableViewCell.swift */, 842B17FC2864980B0014CC57 /* NetworkSelectionTableViewCell.swift */, 8489198D2A0529DA008D57A3 /* NetworkTableViewCell.swift */, + 77C976212AF39F180049272C /* TokenOperationTableViewCell.swift */, ); path = Cell; sourceTree = ""; @@ -20521,6 +20527,7 @@ 84B1318C29ED70BF004EA1FF /* EvmFallbackGasLimit.swift in Sources */, 84DB9E902640A48E00F23DD3 /* StakingRedeemViewModel.swift in Sources */, 84D8F16124D8193200AF43E9 /* IconWithTitleTableViewCell.swift in Sources */, + 77C976202AF36A170049272C /* SwapModels.swift in Sources */, 844ADE7E28CB351500EE29F7 /* AutomationTimeAutocompound.swift in Sources */, 84C41F3628EDADE000DB1CD3 /* ReferendaTrackInfo.swift in Sources */, AEF507F226259DF00098574D /* ValidationViewModel.swift in Sources */, @@ -21428,6 +21435,7 @@ F4D0546B2729949100210294 /* MoonbeamMakeSignatureResponse.swift in Sources */, D9046DBA27451ED700C29F2E /* ParallelContributionSource.swift in Sources */, 0CE629DE2AA9B6BF00E250BD /* RewardDestinationViewModelFactory.swift in Sources */, + 77C976222AF39F180049272C /* TokenOperationTableViewCell.swift in Sources */, 77C9BCBE2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift in Sources */, 844DB624262D9C710025A8F0 /* ErasRewardDistribution.swift in Sources */, 84FAB0632542C8D600319F74 /* ContactItem.swift in Sources */, @@ -21878,7 +21886,7 @@ 845B821926EF808D00D25C72 /* MetaAccountMapper.swift in Sources */, 19A29027666EB5388CBFAD61 /* StakingRewardDetailsInteractor.swift in Sources */, 846AC7EF2638D9200075F7DA /* YourValidatorTableCell.swift in Sources */, - 77C9BCBC2ACD1AF500022EA2 /* ViewModels.swift in Sources */, + 77C9BCBC2ACD1AF500022EA2 /* SwapViewModels.swift in Sources */, C937154FA9021AECD72A871B /* StakingRewardDetailsViewController.swift in Sources */, 84770F27291F7CD400852A33 /* ReferendumDetailsInitData.swift in Sources */, 84B8AA8529F910AD00347A37 /* WalletConnectStateError.swift in Sources */, diff --git a/novawallet/Assets.xcassets/iconCrossChainTransfer.imageset/Contents.json b/novawallet/Assets.xcassets/iconCrossChainTransfer.imageset/Contents.json new file mode 100644 index 0000000000..531f8ab844 --- /dev/null +++ b/novawallet/Assets.xcassets/iconCrossChainTransfer.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "cross-chain.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconCrossChainTransfer.imageset/cross-chain.pdf b/novawallet/Assets.xcassets/iconCrossChainTransfer.imageset/cross-chain.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a0293ea0843388b1e53c1bf07352b477f4d59ffe GIT binary patch literal 2023 zcmb7_PjAyO6u|HP6z?)=J2Z*YIB}$@5^K8&0b+FACJw>%x}r3JBtwHw&vRlwI}8cP zVRL`>`~UoWcDuP=@Tg#nLjvvRFAU)F65{RiaC0|QgPwk+<3CkF5=oih2dfpHeDTFw z{x2t2Dj~o@ps*O;~8EMr)c46jX}EzJ@tw3c4o|a zq=J-D9&4(jnV5DQy4h*QYImcQ`7M-phl{t7gi0DCfsZV`2KE)&kq&bIHbe*p(Xp^^Xq literal 0 HcmV?d00001 diff --git a/novawallet/Common/Model/Xcm/XcmTransfers.swift b/novawallet/Common/Model/Xcm/XcmTransfers.swift index fe8329453f..d4aede988a 100644 --- a/novawallet/Common/Model/Xcm/XcmTransfers.swift +++ b/novawallet/Common/Model/Xcm/XcmTransfers.swift @@ -83,6 +83,19 @@ struct XcmTransfers: Decodable { return xcmTransfers } + func transferChainAssets(to chainAssetId: ChainAssetId) -> [ChainAssetId] { + chains.flatMap { chain in + chain.assets.filter { asset in + asset.xcmTransfers.contains(where: { transfer in + transfer.destination.chainId == chainAssetId.chainId && + transfer.destination.assetId == chainAssetId.assetId + }) + }.map { + ChainAssetId(chainId: chain.chainId, assetId: $0.assetId) + } + } + } + func transfer( from chainAssetId: ChainAssetId, destinationChainId: ChainModel.Id diff --git a/novawallet/Common/ViewController/ModalPicker/Cell/SecretTypeTableViewCell.swift b/novawallet/Common/ViewController/ModalPicker/Cell/SecretTypeTableViewCell.swift index 00ae5fabe2..a1a1bfeee1 100644 --- a/novawallet/Common/ViewController/ModalPicker/Cell/SecretTypeTableViewCell.swift +++ b/novawallet/Common/ViewController/ModalPicker/Cell/SecretTypeTableViewCell.swift @@ -21,90 +21,3 @@ final class SecretTypeTableViewCell: IconWithTitleSubtitleTableViewCell { subtitleLabel.font = UIFont.p2Paragraph } } - -final class TokenOperationTableViewCell: UITableViewCell, ModalPickerCellProtocol { - struct Model { - let content: IconWithTitleSubtitleViewModel - let isActive: Bool - } - - private(set) var titleLabel = UILabel() - private(set) var subtitleLabel = UILabel() - private(set) var iconImageView = UIImageView() - let arrowIcon = R.image.iconSmallArrow()?.tinted(with: R.color.colorIconSecondary()!) - - var checkmarked: Bool = false - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - setupStyle() - setupLayout() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func bind(model: Model) { - titleLabel.text = model.content.title - subtitleLabel.text = model.content.subtitle - iconImageView.image = model.content.icon - - if model.isActive { - titleLabel.textColor = R.color.colorTextPrimary() - subtitleLabel.textColor = R.color.colorTextSecondary() - iconImageView.tintColor = R.color.colorIconPrimary() - accessoryView = UIImageView(image: arrowIcon) - selectionStyle = .default - } else { - titleLabel.textColor = R.color.colorButtonTextInactive() - subtitleLabel.textColor = R.color.colorButtonTextInactive() - iconImageView.tintColor = R.color.colorIconInactive() - accessoryView = nil - selectionStyle = .none - } - } - - private func setupStyle() { - backgroundColor = .clear - - selectedBackgroundView = UIView() - selectedBackgroundView?.backgroundColor = R.color.colorCellBackgroundPressed() - - separatorInset = UIEdgeInsets( - top: 0.0, - left: UIConstants.horizontalInset, - bottom: 0.0, - right: UIConstants.horizontalInset - ) - - titleLabel.apply(style: .regularSubhedlinePrimary) - subtitleLabel.apply(style: .footnoteSecondary) - } - - private func setupLayout() { - contentView.addSubview(iconImageView) - iconImageView.snp.makeConstraints { make in - make.leading.equalToSuperview().inset(UIConstants.horizontalInset) - make.centerY.equalToSuperview() - } - - iconImageView.setContentHuggingPriority(.defaultHigh, for: .horizontal) - iconImageView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - - contentView.addSubview(titleLabel) - titleLabel.snp.makeConstraints { make in - make.top.equalToSuperview().inset(7.0) - make.leading.equalTo(iconImageView.snp.trailing).offset(12.0) - make.trailing.equalToSuperview().inset(8.0) - } - - contentView.addSubview(subtitleLabel) - subtitleLabel.snp.makeConstraints { make in - make.bottom.equalToSuperview().inset(7.0) - make.leading.equalTo(iconImageView.snp.trailing).offset(12.0) - make.trailing.equalToSuperview().inset(8.0) - } - } -} diff --git a/novawallet/Common/ViewController/ModalPicker/Cell/TokenOperationTableViewCell.swift b/novawallet/Common/ViewController/ModalPicker/Cell/TokenOperationTableViewCell.swift new file mode 100644 index 0000000000..99495d0351 --- /dev/null +++ b/novawallet/Common/ViewController/ModalPicker/Cell/TokenOperationTableViewCell.swift @@ -0,0 +1,88 @@ +import UIKit + +final class TokenOperationTableViewCell: UITableViewCell, ModalPickerCellProtocol { + struct Model { + let content: IconWithTitleSubtitleViewModel + let isActive: Bool + } + + private(set) var titleLabel = UILabel() + private(set) var subtitleLabel = UILabel() + private(set) var iconImageView = UIImageView() + let arrowIcon = R.image.iconSmallArrow()?.tinted(with: R.color.colorIconSecondary()!) + + var checkmarked: Bool = false + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupStyle() + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func bind(model: Model) { + titleLabel.text = model.content.title + subtitleLabel.text = model.content.subtitle + iconImageView.image = model.content.icon + + if model.isActive { + titleLabel.textColor = R.color.colorTextPrimary() + subtitleLabel.textColor = R.color.colorTextSecondary() + iconImageView.tintColor = R.color.colorIconPrimary() + accessoryView = UIImageView(image: arrowIcon) + selectionStyle = .default + } else { + titleLabel.textColor = R.color.colorButtonTextInactive() + subtitleLabel.textColor = R.color.colorButtonTextInactive() + iconImageView.tintColor = R.color.colorIconInactive() + accessoryView = nil + selectionStyle = .none + } + } + + private func setupStyle() { + backgroundColor = .clear + + selectedBackgroundView = UIView() + selectedBackgroundView?.backgroundColor = R.color.colorCellBackgroundPressed() + + separatorInset = UIEdgeInsets( + top: 0, + left: UIConstants.horizontalInset, + bottom: 0, + right: UIConstants.horizontalInset + ) + + titleLabel.apply(style: .regularSubhedlinePrimary) + subtitleLabel.apply(style: .footnoteSecondary) + } + + private func setupLayout() { + contentView.addSubview(iconImageView) + iconImageView.snp.makeConstraints { make in + make.leading.equalToSuperview().inset(UIConstants.horizontalInset) + make.centerY.equalToSuperview() + } + + iconImageView.setContentHuggingPriority(.defaultHigh, for: .horizontal) + iconImageView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + + contentView.addSubview(titleLabel) + titleLabel.snp.makeConstraints { make in + make.top.equalToSuperview().inset(7) + make.leading.equalTo(iconImageView.snp.trailing).offset(12) + make.trailing.equalToSuperview().inset(8) + } + + contentView.addSubview(subtitleLabel) + subtitleLabel.snp.makeConstraints { make in + make.bottom.equalToSuperview().inset(7) + make.leading.equalTo(iconImageView.snp.trailing).offset(12) + make.trailing.equalToSuperview().inset(8) + } + } +} diff --git a/novawallet/Common/ViewController/ModalPicker/ModalPickerFactory.swift b/novawallet/Common/ViewController/ModalPicker/ModalPickerFactory.swift index 4ed42223b7..1f1567af9d 100644 --- a/novawallet/Common/ViewController/ModalPicker/ModalPickerFactory.swift +++ b/novawallet/Common/ViewController/ModalPicker/ModalPickerFactory.swift @@ -511,7 +511,7 @@ extension ModalPickerFactory { extension ModalPickerFactory { static func createPickerListForOperations( - operations: [(token: TokenOperation, active: Bool)], + operations: [DepositOperationModel], delegate: ModalPickerViewControllerDelegate?, token: String, context: AnyObject? @@ -523,7 +523,7 @@ extension ModalPickerFactory { let viewController: ModalPickerViewController = ModalPickerViewController(nib: R.nib.modalPickerViewController) - viewController.localizedTitle = .init { _ in "Get \(token) using" } + viewController.localizedTitle = .init { R.string.localizable.swapsSetupDepositTitle(token, preferredLanguages: $0.rLanguages) } viewController.selectedIndex = NSNotFound viewController.delegate = delegate @@ -532,15 +532,15 @@ extension ModalPickerFactory { viewController.headerBorderType = .none viewController.separatorStyle = .none viewController.separatorColor = R.color.colorDivider() - viewController.cellHeight = 48.0 + viewController.cellHeight = 48 viewController.viewModels = operations.map { operation in LocalizableResource { locale in TokenOperationTableViewCell.Model( content: .init( - title: operation.token.titleForLocale(locale), - subtitle: operation.token.subtitleForLocale(locale, token: token), - icon: operation.token.icon + title: operation.titleForLocale(locale), + subtitle: operation.subtitleForLocale(locale, token: token), + icon: operation.icon ), isActive: operation.active ) @@ -559,38 +559,3 @@ extension ModalPickerFactory { return viewController } } - -extension TokenOperation { - func titleForLocale(_: Locale) -> String { - switch self { - case .send: - return "Cross-chain transfer" - case .receive: - return "Receive" - case .buy: - return "Buy" - } - } - - func subtitleForLocale(_: Locale, token: String) -> String { - switch self { - case .send: - return "Transfer \(token) from another network" - case .receive: - return "Receive \(token) with QR or your address" - case .buy: - return "Instantly buy \(token) with a credit card" - } - } - - var icon: UIImage? { - switch self { - case .send: - return R.image.iconSend() - case .receive: - return R.image.iconReceive() - case .buy: - return R.image.iconBuy() - } - } -} diff --git a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift index 53d568b1f8..28fe14c842 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift @@ -10,8 +10,15 @@ protocol SwapBaseInteractorInputProtocol: AnyObject { protocol SwapBaseInteractorOutputProtocol: AnyObject { func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) func didReceive(fee: AssetConversion.FeeModel?, transactionId: TransactionFeeId, feeChainAssetId: ChainAssetId?) - func didReceive(baseError: SwapSetupError) + func didReceive(baseError: SwapSetupBaseError) func didReceive(price: PriceData?, priceId: AssetModel.PriceId) func didReceive(payAccountId: AccountId?) func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId: AccountId) } + +enum SwapSetupBaseError: Error { + case quote(Error, AssetConversion.QuoteArgs) + case fetchFeeFailed(Error, TransactionFeeId, FeeChainAssetId?) + case price(Error, AssetModel.PriceId) + case assetBalance(Error, ChainAssetId, AccountId) +} diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 7ebc9ca88b..e0de99d003 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -398,7 +398,7 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { balances[chainAsset] = balance } - func didReceive(baseError: SwapSetupError) { + func didReceive(baseError: SwapSetupBaseError) { switch baseError { case let .quote(_, args): wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapModels.swift b/novawallet/Modules/Swaps/Setup/Model/SwapModels.swift new file mode 100644 index 0000000000..6caa5d8908 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/Model/SwapModels.swift @@ -0,0 +1,61 @@ +import SoraFoundation + +struct SwapSetupFeeIdentifier: Equatable { + let transactionId: String + let feeChainAssetId: ChainAssetId? +} + +struct DepositOperationModel { + let operation: TokenOperation + let active: Bool +} + +extension DepositOperationModel { + func titleForLocale(_ locale: Locale) -> String { + switch operation { + case .send: + return R.string.localizable.swapsSetupDepositByCrossChainTransferTitle( + preferredLanguages: locale.rLanguages + ) + case .receive: + return R.string.localizable.walletAssetReceive( + preferredLanguages: locale.rLanguages + ) + case .buy: + return R.string.localizable.walletAssetBuy( + preferredLanguages: locale.rLanguages + ) + } + } + + func subtitleForLocale(_ locale: Locale, token: String) -> String { + switch operation { + case .send: + return R.string.localizable.swapsSetupDepositByCrossChainTransferSubtitle( + token, + preferredLanguages: locale.rLanguages + ) + case .receive: + return R.string.localizable.swapsSetupDepositByReceiveSubtitle( + token, + preferredLanguages: locale.rLanguages + ) + case .buy: + return R.string.localizable.swapsSetupDepositByBuySubtitle( + token, + preferredLanguages: locale.rLanguages + ) + } + } + + var icon: UIImage? { + switch operation { + case .send: + return R.image.iconCrossChainTransfer() + case .receive: + return R.image.iconReceive() + case .buy: + return R.image.iconBuy() + } + } +} diff --git a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift b/novawallet/Modules/Swaps/Setup/Model/SwapViewModels.swift similarity index 91% rename from novawallet/Modules/Swaps/Setup/Model/ViewModels.swift rename to novawallet/Modules/Swaps/Setup/Model/SwapViewModels.swift index 330d5fee19..e670428660 100644 --- a/novawallet/Modules/Swaps/Setup/Model/ViewModels.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapViewModels.swift @@ -27,11 +27,6 @@ struct SwapPriceDifferenceViewModel { let difference: DifferenceViewModel? } -struct SwapSetupFeeIdentifier: Equatable { - let transactionId: String - let feeChainAssetId: ChainAssetId? -} - enum FeeSelectionViewModel: Int, CaseIterable { case payAsset case utilityAsset diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 2c29063662..8539639ccb 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -3,6 +3,35 @@ import RobinHood import BigInt final class SwapSetupInteractor: SwapBaseInteractor { + let xcmTransfersSyncService: XcmTransfersSyncServiceProtocol + let chainRegistry: ChainRegistryProtocol + private var xcmTransfers: XcmTransfers? + + init( + xcmTransfersSyncService: XcmTransfersSyncServiceProtocol, + chainRegistry: ChainRegistryProtocol, + assetConversionOperationFactory: AssetConversionOperationFactoryProtocol, + assetConversionFeeService: AssetConversionFeeServiceProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + currencyManager: CurrencyManagerProtocol, + selectedWallet: MetaAccountModel, + operationQueue: OperationQueue + ) { + self.xcmTransfersSyncService = xcmTransfersSyncService + self.chainRegistry = chainRegistry + + super.init( + assetConversionOperationFactory: assetConversionOperationFactory, + assetConversionFeeService: assetConversionFeeService, + priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, + walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, + currencyManager: currencyManager, + selectedWallet: selectedWallet, + operationQueue: operationQueue + ) + } + weak var presenter: SwapSetupInteractorOutputProtocol? { basePresenter as? SwapSetupInteractorOutputProtocol } @@ -34,9 +63,64 @@ final class SwapSetupInteractor: SwapBaseInteractor { ].compactMap { $0 } ) } + + deinit { + xcmTransfersSyncService.throttle() + } + + private func setupXcmTransfersSyncService() { + xcmTransfersSyncService.notificationCallback = { [weak self] result in + switch result { + case let .success(xcmTransfers): + self?.xcmTransfers = xcmTransfers + self?.provideAvailableTransfers() + case let .failure(error): + self?.presenter?.didReceive(error: .xcm(error)) + } + } + + xcmTransfersSyncService.setup() + } + + private func provideAvailableTransfers() { + guard let xcmTransfers = xcmTransfers, let payChainAsset = payChainAsset else { + presenter?.didReceiveAvailableXcm(origins: [], xcmTransfers: nil) + return + } + + let chainAssets = xcmTransfers.transferChainAssets(to: payChainAsset.chainAssetId) + + guard !chainAssets.isEmpty else { + presenter?.didReceiveAvailableXcm(origins: [], xcmTransfers: xcmTransfers) + return + } + + let origins: [ChainAsset] = chainAssets.compactMap { chainAsset in + guard + chainAsset != payChainAsset.chainAssetId, + let chain = chainRegistry.getChain(for: chainAsset.chainId), + let asset = chain.asset(for: chainAsset.assetId) + else { + return nil + } + + return ChainAsset(chain: chain, asset: asset) + } + + presenter?.didReceiveAvailableXcm(origins: origins, xcmTransfers: xcmTransfers) + } + + override func setup() { + super.setup() + setupXcmTransfersSyncService() + } } extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { + func setupXcm() { + setupXcmTransfersSyncService() + } + func update(receiveChainAsset: ChainAsset?) { self.receiveChainAsset = receiveChainAsset receiveChainAsset.map { @@ -50,6 +134,8 @@ extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { payChainAsset.map { set(payChainAsset: $0) } + + provideAvailableTransfers() } func update(feeChainAsset: ChainAsset?) { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 0eecc823a0..a4ab4d810d 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -2,7 +2,7 @@ import Foundation import SoraFoundation import BigInt -final class SwapSetupPresenter { +final class SwapSetupPresenter: PurchaseFlowManaging { weak var view: SwapSetupViewProtocol? let wireframe: SwapSetupWireframeProtocol let interactor: SwapSetupInteractorInputProtocol @@ -31,9 +31,12 @@ final class SwapSetupPresenter { } private var slippage: BigRational? - private var feeIdentifier: SwapSetupFeeIdentifier? private var accountId: AccountId? + private var depositOperations: [DepositOperationModel] = [] + private var purchaseActions: [PurchaseAction] = [] + private var depositCrossChainAssets: [ChainAsset] = [] + private var xcmTransfers: XcmTransfers? init( interactor: SwapSetupInteractorInputProtocol, @@ -580,8 +583,11 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { guard let payChainAsset = payChainAsset, let accountId = accountId else { return } - let purchaseActions = purchaseProvider.buildPurchaseActions(for: payChainAsset, accountId: accountId) + + purchaseActions = purchaseProvider.buildPurchaseActions(for: payChainAsset, accountId: accountId) let sendAvailable = TokenOperation.checkTransferOperationAvailable() + let crossChainSendAvailable = depositCrossChainAssets.first != nil && sendAvailable + let recieveAvailable = TokenOperation.checkReceiveOperationAvailable( walletType: selectedAccount.type, chainAsset: payChainAsset @@ -591,14 +597,14 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { walletType: selectedAccount.type, chainAsset: payChainAsset ).available - let operations: [(token: TokenOperation, active: Bool)] = [ - (token: .send, active: sendAvailable), - (token: .receive, active: recieveAvailable), - (token: .buy, active: buyAvailable) + depositOperations = [ + .init(operation: .send, active: crossChainSendAvailable), + .init(operation: .receive, active: recieveAvailable), + .init(operation: .buy, active: buyAvailable) ] wireframe.showTokenDepositOptions( form: view, - operations: operations, + operations: depositOperations, token: payChainAsset.asset.symbol, delegate: self ) @@ -606,7 +612,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { - func didReceive(baseError: SwapSetupError) { + func didReceive(baseError: SwapSetupBaseError) { switch baseError { case let .quote(_, args): guard args == quoteArgs else { @@ -630,6 +636,15 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { } } + func didReceive(error: SwapSetupError) { + switch error { + case .xcm: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.setupXcm() + } + } + } + func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) { guard quoteArgs == self.quoteArgs else { return @@ -718,6 +733,11 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { } } } + + func didReceiveAvailableXcm(origins: [ChainAsset], xcmTransfers: XcmTransfers?) { + depositCrossChainAssets = origins + self.xcmTransfers = xcmTransfers + } } extension SwapSetupPresenter: Localizable { @@ -730,7 +750,50 @@ extension SwapSetupPresenter: Localizable { } extension SwapSetupPresenter: ModalPickerViewControllerDelegate { - func modalPickerDidSelectModelAtIndex(_: Int, context _: AnyObject?) {} + func modalPickerDidSelectModelAtIndex(_ index: Int, context _: AnyObject?) { + guard let operation = depositOperations[safe: index], operation.active else { + return + } - func modalPickerDidSelectModel(at _: Int, section _: Int, context _: AnyObject?) {} + switch operation.operation { + case .buy: + startPuchaseFlow( + from: view, + purchaseActions: purchaseActions, + wireframe: wireframe, + locale: selectedLocale + ) + case .receive: + guard let payChainAsset = payChainAsset, + let metaChainAccountResponse = selectedAccount.fetchMetaChainAccount(for: payChainAsset.chain.accountRequest()) else { + return + } + wireframe.showDepositTokensByReceive( + from: view, + chainAsset: payChainAsset, + metaChainAccountResponse: metaChainAccountResponse + ) + case .send: + guard let payChainAsset = payChainAsset, + let accountId = accountId, + let address = try? accountId.toAddress(using: payChainAsset.chain.chainFormat), + let origin = depositCrossChainAssets.first, + let xcmTransfers = xcmTransfers else { + return + } + wireframe.showDepositTokensBySend( + from: view, + origin: origin, + destination: payChainAsset, + recepient: .init(address: address, username: ""), + xcmTransfers: xcmTransfers + ) + } + } +} + +extension SwapSetupPresenter: PurchaseDelegate { + func purchaseDidComplete() { + wireframe.presentPurchaseDidComplete(view: view, locale: selectedLocale) + } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 51df7081f4..2e41c44726 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -39,12 +39,16 @@ protocol SwapSetupInteractorInputProtocol: SwapBaseInteractorInputProtocol { func update(receiveChainAsset: ChainAsset?) func update(payChainAsset: ChainAsset?) func update(feeChainAsset: ChainAsset?) + func setupXcm() } -protocol SwapSetupInteractorOutputProtocol: SwapBaseInteractorOutputProtocol {} +protocol SwapSetupInteractorOutputProtocol: SwapBaseInteractorOutputProtocol { + func didReceiveAvailableXcm(origins: [ChainAsset], xcmTransfers: XcmTransfers?) + func didReceive(error: SwapSetupError) +} protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, - ErrorPresentable, SwapErrorPresentable, ShortTextInfoPresentable { + ErrorPresentable, SwapErrorPresentable, ShortTextInfoPresentable, PurchasePresentable { func showPayTokenSelection( from view: ControllerBackedProtocol?, chainAsset: ChainAsset?, @@ -76,17 +80,26 @@ protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryabl ) func showTokenDepositOptions( form view: ControllerBackedProtocol?, - operations: [(token: TokenOperation, active: Bool)], + operations: [DepositOperationModel], token: String, delegate: ModalPickerViewControllerDelegate? ) + func showDepositTokensByReceive( + from view: ControllerBackedProtocol?, + chainAsset: ChainAsset, + metaChainAccountResponse: MetaChainAccountResponse + ) + func showDepositTokensBySend( + from view: ControllerBackedProtocol?, + origin: ChainAsset, + destination: ChainAsset, + recepient: DisplayAddress?, + xcmTransfers: XcmTransfers + ) } enum SwapSetupError: Error { - case quote(Error, AssetConversion.QuoteArgs) - case fetchFeeFailed(Error, TransactionFeeId, FeeChainAssetId?) - case price(Error, AssetModel.PriceId) - case assetBalance(Error, ChainAssetId, AccountId) + case xcm(Error) } enum SwapSetupViewError { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index fded1063b1..99fa5f4400 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -79,7 +79,14 @@ struct SwapSetupViewFactory { operationQueue: operationQueue ) + let xcmTransfersSyncService = XcmTransfersSyncService( + remoteUrl: ApplicationConfig.shared.xcmTransfersURL, + operationQueue: operationQueue + ) + let interactor = SwapSetupInteractor( + xcmTransfersSyncService: xcmTransfersSyncService, + chainRegistry: ChainRegistryFacade.sharedRegistry, assetConversionOperationFactory: assetConversionOperationFactory, assetConversionFeeService: feeService, priceLocalSubscriptionFactory: PriceProviderFactory.shared, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index bb1dd38319..30c1a2bd41 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -101,7 +101,7 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { func showTokenDepositOptions( form view: ControllerBackedProtocol?, - operations: [(token: TokenOperation, active: Bool)], + operations: [DepositOperationModel], token: String, delegate: ModalPickerViewControllerDelegate? ) { @@ -116,4 +116,38 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { view?.controller.present(bottomSheet, animated: true) } + + func showDepositTokensBySend( + from view: ControllerBackedProtocol?, + origin: ChainAsset, + destination: ChainAsset, + recepient: DisplayAddress?, + xcmTransfers: XcmTransfers + ) { + guard let transferSetupView = TransferSetupViewFactory.createCrossChainView( + from: origin, + to: destination, + xcmTransfers: xcmTransfers, + recepient: recepient + ) else { + return + } + + view?.controller.navigationController?.pushViewController(transferSetupView.controller, animated: true) + } + + func showDepositTokensByReceive( + from view: ControllerBackedProtocol?, + chainAsset: ChainAsset, + metaChainAccountResponse: MetaChainAccountResponse + ) { + guard let receiveTokensView = AssetReceiveViewFactory.createView( + chainAsset: chainAsset, + metaChainAccountResponse: metaChainAccountResponse + ) else { + return + } + + view?.controller.navigationController?.pushViewController(receiveTokensView.controller, animated: true) + } } diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift index 76f641f87a..a82502e478 100644 --- a/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift @@ -8,6 +8,35 @@ struct TransferSetupViewFactory { from chainAsset: ChainAsset, recepient: DisplayAddress?, transferCompletion: TransferCompletionClosure? = nil + ) -> TransferSetupViewProtocol? { + createView(from: chainAsset, recepient: recepient, transferCompletion: transferCompletion) { factory, state, view in + factory.createOnChainPresenter(for: chainAsset, initialState: state, view: view) + } + } + + static func createCrossChainView( + from chainAsset: ChainAsset, + to destinationChainAsset: ChainAsset, + xcmTransfers: XcmTransfers, + recepient: DisplayAddress?, + transferCompletion: TransferCompletionClosure? = nil + ) -> TransferSetupViewProtocol? { + createView(from: chainAsset, recepient: recepient, transferCompletion: transferCompletion) { factory, state, view in + factory.createCrossChainPresenter( + for: chainAsset, + destinationChainAsset: destinationChainAsset, + xcmTransfers: xcmTransfers, + initialState: state, + view: view + ) + } + } + + static func createView( + from chainAsset: ChainAsset, + recepient: DisplayAddress?, + transferCompletion: TransferCompletionClosure?, + createChildPresenterClosure: (TransferSetupPresenterFactoryProtocol, TransferSetupInputState, TransferSetupChildViewProtocol) -> TransferSetupChildPresenterProtocol? ) -> TransferSetupViewProtocol? { guard let wallet = SelectedWalletSettings.shared.value else { return nil @@ -48,11 +77,7 @@ struct TransferSetupViewFactory { localizationManager: localizationManager ) - presenter.childPresenter = presenterFactory.createOnChainPresenter( - for: chainAsset, - initialState: initPresenterState, - view: view - ) + presenter.childPresenter = createChildPresenterClosure(presenterFactory, initPresenterState, view) presenter.view = view interactor.presenter = presenter diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 31b5818549..599cc5bfe0 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1411,3 +1411,8 @@ "swaps.setup.price.difference.description" = "Price difference refers to the difference in price between two different assets. When making a swap in crypto, the price difference is usually the difference between the price of the asset you are swapping for and the price of the asset you are swapping with."; "swaps.setup.error.rate.was.updated.title" = "Swap rate was updated"; "swaps.setup.error.rate.was.updated.message" = "Old rate: %@.\nNew rate:%@"; +"swaps.setup.deposit.by.cross.chain.transfer.title" = "Cross-chain transfer"; +"swaps.setup.deposit.by.cross.chain.transfer.subtitle" = "Transfer %@ from another network"; +"swaps.setup.deposit.by.receive.subtitle" = "Receive %@ with QR or your address"; +"swaps.setup.deposit.by.buy.subtitle" = "Instantly buy %@ with a credit card"; +"swaps.setup.deposit.title" = "Get %@ using"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index cbe4848c2d..6900a1755e 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1411,3 +1411,8 @@ "swaps.setup.price.difference.description" = "Разница в цене относится к разнице в цене между двумя различными активами. При совершении обмена в криптовалюте разница в цене обычно представляет собой разницу между ценой актива, на который вы меняете, и ценой актива, на который вы меняетесь."; "swaps.setup.error.rate.was.updated.title" = "Обменный курс был обновлен"; "swaps.setup.error.rate.was.updated.message" = "Было: %@.\nСтало:%@"; +"swaps.setup.deposit.by.cross.chain.transfer.title" = "Перевод между сетями"; +"swaps.setup.deposit.by.cross.chain.transfer.subtitle" = "Перевести %@ из другой сети"; +"swaps.setup.deposit.by.receive.subtitle" = "Получить %@ используя QR-код или адрес"; +"swaps.setup.deposit.by.buy.subtitle" = "Купить %@ используя банковскую карту"; +"swaps.setup.deposit.title" = "Пополнить %@"; From 2249c8ea4c80cbf46fe341ec4be2f10c332e8cd8 Mon Sep 17 00:00:00 2001 From: ERussel Date: Thu, 2 Nov 2023 10:23:21 +0100 Subject: [PATCH 113/204] fix swap logic --- .../Model/SwapConfirmViewModelFactory.swift | 4 +- .../Confirm/View/SwapConfirmViewLayout.swift | 2 +- .../Model/SwapsSetupViewModelFactory.swift | 2 +- .../Swaps/Setup/SwapSetupPresenter.swift | 90 +++++++++++++------ .../Setup/View/SwapSetupViewLayout.swift | 2 +- 5 files changed, 70 insertions(+), 30 deletions(-) diff --git a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift index 2b7c10e294..d01d1d16b3 100644 --- a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift @@ -97,10 +97,10 @@ extension SwapConfirmViewModelFactory: SwapConfirmViewModelFactoryProtocol { ).value(for: locale) let amountOut = balanceViewModelFactoryFacade.amountFromValue( targetAssetInfo: params.assetDisplayInfoOut, - value: difference ?? 0 + value: difference ).value(for: locale) - return "\(amountIn) = \(amountOut)" + return "\(amountIn) ≈ \(amountOut)" } func slippageViewModel(slippage: BigRational) -> String { diff --git a/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift b/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift index d0cd473c61..55d737520c 100644 --- a/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift +++ b/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift @@ -83,7 +83,7 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { ) rateCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupDetailsRate( preferredLanguages: locale.rLanguages) - networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetwork( + networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetworkFee( preferredLanguages: locale.rLanguages) rateCell.titleButton.invalidateLayout() networkFeeCell.titleButton.invalidateLayout() diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift index a2b64c7cd0..00639ee2c3 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift @@ -231,7 +231,7 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { ).value(for: locale) let amountOut = balanceViewModelFactoryFacade.amountFromValue( targetAssetInfo: params.assetDisplayInfoOut, - value: difference ?? 0 + value: difference ).value(for: locale) return "\(amountIn) ≈ \(amountOut)" diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index d1e27632a4..6337da0d56 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -10,16 +10,39 @@ final class SwapSetupPresenter { let logger: LoggerProtocol private(set) var viewModelFactory: SwapsSetupViewModelFactoryProtocol - private(set) var payAssetBalance: AssetBalance? - private(set) var feeAssetBalance: AssetBalance? - private(set) var receiveAssetBalance: AssetBalance? + + private(set) var balances: [ChainAssetId: AssetBalance] = [:] + + var payAssetBalance: AssetBalance? { + payChainAsset.flatMap { balances[$0.chainAssetId] } + } + + var feeAssetBalance: AssetBalance? { + feeChainAsset.flatMap { balances[$0.chainAssetId] } + } + + var receiveAssetBalance: AssetBalance? { + receiveChainAsset.flatMap { balances[$0.chainAssetId] } + } + + private(set) var prices: [ChainAssetId: PriceData] = [:] + + var payAssetPriceData: PriceData? { + payChainAsset.flatMap { prices[$0.chainAssetId] } + } + + var receiveAssetPriceData: PriceData? { + receiveChainAsset.flatMap { prices[$0.chainAssetId] } + } + + var feeAssetPriceData: PriceData? { + feeChainAsset.flatMap { prices[$0.chainAssetId] } + } + private(set) var payChainAsset: ChainAsset? private(set) var canPayFeeInPayAsset: Bool = false private(set) var receiveChainAsset: ChainAsset? private(set) var feeChainAsset: ChainAsset? - private(set) var payAssetPriceData: PriceData? - private(set) var receiveAssetPriceData: PriceData? - private(set) var feeAssetPriceData: PriceData? private(set) var payAmountInput: AmountInputResult? private(set) var receiveAmountInput: Decimal? private(set) var fee: AssetConversion.FeeModel? @@ -388,8 +411,11 @@ final class SwapSetupPresenter { feeChainAsset = chainAsset providePayAssetViews() interactor.update(feeChainAsset: chainAsset) + + fee = nil + provideFeeViewModel() + estimateFee() - refreshQuote(direction: .sell) } } @@ -417,25 +443,42 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } self?.feeChainAsset = feeChainAsset + self?.fee = nil self?.canPayFeeInPayAsset = false + self?.providePayAssetViews() self?.provideButtonState() self?.provideSettingsState() + self?.provideFeeViewModel() + self?.interactor.update(payChainAsset: chainAsset) self?.interactor.update(feeChainAsset: feeChainAsset) - self?.refreshQuote(direction: .sell, forceUpdate: false) + if let direction = self?.quoteArgs?.direction { + self?.refreshQuote(direction: direction, forceUpdate: false) + } else if self?.payAmountInput != nil { + self?.refreshQuote(direction: .sell, forceUpdate: false) + } else { + self?.refreshQuote(direction: .buy, forceUpdate: false) + } } } func selectReceiveToken() { wireframe.showReceiveTokenSelection(from: view, chainAsset: payChainAsset) { [weak self] chainAsset in - let chainAsset = chainAsset self?.receiveChainAsset = chainAsset self?.provideReceiveAssetViews() self?.provideButtonState() - self?.refreshQuote(direction: .buy, forceUpdate: false) + self?.interactor.update(receiveChainAsset: chainAsset) + + if let direction = self?.quoteArgs?.direction { + self?.refreshQuote(direction: direction, forceUpdate: false) + } else if self?.receiveAmountInput != nil { + self?.refreshQuote(direction: .buy, forceUpdate: false) + } else { + self?.refreshQuote(direction: .sell, forceUpdate: false) + } } } @@ -457,8 +500,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { Swift.swap(&payChainAsset, &receiveChainAsset) canPayFeeInPayAsset = false - Swift.swap(&payAssetBalance, &receiveAssetBalance) - Swift.swap(&payAssetPriceData, &receiveAssetPriceData) + interactor.update(payChainAsset: payChainAsset) interactor.update(receiveChainAsset: receiveChainAsset) let newFocus: TextFieldFocus? @@ -687,16 +729,18 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { } func didReceive(price: PriceData?, priceId: AssetModel.PriceId) { - if priceId == payChainAsset?.asset.priceId { - payAssetPriceData = price + if let payChainAsset = payChainAsset, priceId == payChainAsset.asset.priceId { + prices[payChainAsset.chainAssetId] = price providePayInputPriceViewModel() } - if priceId == receiveChainAsset?.asset.priceId { - receiveAssetPriceData = price + + if let receiveChainAsset = receiveChainAsset, priceId == receiveChainAsset.asset.priceId { + prices[receiveChainAsset.chainAssetId] = price provideReceiveInputPriceViewModel() } - if priceId == feeChainAsset?.asset.priceId { - feeAssetPriceData = price + + if let feeChainAsset = feeChainAsset, priceId == feeChainAsset.asset.priceId { + prices[feeChainAsset.chainAssetId] = price provideFeeViewModel() } } @@ -706,21 +750,17 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { } func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId _: AccountId) { + balances[chainAsset] = balance + if chainAsset == payChainAsset?.chainAssetId { - payAssetBalance = balance providePayTitle() - } - if chainAsset == feeChainAsset?.chainAssetId { - feeAssetBalance = balance + if case .rate = payAmountInput { providePayInputPriceViewModel() providePayAmountInputViewModel() provideButtonState() } } - if chainAsset == receiveChainAsset?.chainAssetId { - receiveAssetBalance = balance - } } func didReceiveCanPayFeeInPayAsset(_ value: Bool, chainAssetId: ChainAssetId) { diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index d269a200c0..63df8dec57 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -86,7 +86,7 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { ) rateCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupDetailsRate( preferredLanguages: locale.rLanguages) - networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetwork( + networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetworkFee( preferredLanguages: locale.rLanguages) rateCell.titleButton.invalidateLayout() networkFeeCell.titleButton.invalidateLayout() From 823928e147ded1779bef9abeb6c9f4ef245029a7 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 2 Nov 2023 12:29:21 +0300 Subject: [PATCH 114/204] fix colors --- .../ModalPicker/Cell/TokenOperationTableViewCell.swift | 2 +- .../ViewController/ModalPicker/ModalPickerFactory.swift | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/novawallet/Common/ViewController/ModalPicker/Cell/TokenOperationTableViewCell.swift b/novawallet/Common/ViewController/ModalPicker/Cell/TokenOperationTableViewCell.swift index 99495d0351..a9e0283fdc 100644 --- a/novawallet/Common/ViewController/ModalPicker/Cell/TokenOperationTableViewCell.swift +++ b/novawallet/Common/ViewController/ModalPicker/Cell/TokenOperationTableViewCell.swift @@ -27,7 +27,7 @@ final class TokenOperationTableViewCell: UITableViewCell, ModalPickerCellProtoco func bind(model: Model) { titleLabel.text = model.content.title subtitleLabel.text = model.content.subtitle - iconImageView.image = model.content.icon + iconImageView.image = model.content.icon?.withRenderingMode(.alwaysTemplate) if model.isActive { titleLabel.textColor = R.color.colorTextPrimary() diff --git a/novawallet/Common/ViewController/ModalPicker/ModalPickerFactory.swift b/novawallet/Common/ViewController/ModalPicker/ModalPickerFactory.swift index 1f1567af9d..cafa73d18f 100644 --- a/novawallet/Common/ViewController/ModalPicker/ModalPickerFactory.swift +++ b/novawallet/Common/ViewController/ModalPicker/ModalPickerFactory.swift @@ -523,7 +523,12 @@ extension ModalPickerFactory { let viewController: ModalPickerViewController = ModalPickerViewController(nib: R.nib.modalPickerViewController) - viewController.localizedTitle = .init { R.string.localizable.swapsSetupDepositTitle(token, preferredLanguages: $0.rLanguages) } + viewController.localizedTitle = .init { + R.string.localizable.swapsSetupDepositTitle( + token, + preferredLanguages: $0.rLanguages + ) + } viewController.selectedIndex = NSNotFound viewController.delegate = delegate From 64563a18dea9c77d8121a709365b5f021df8dfae Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 2 Nov 2023 12:44:23 +0300 Subject: [PATCH 115/204] fixes after merging --- novawallet.xcodeproj/project.pbxproj | 14 ++++++-------- .../Modules/Swaps/Setup/SwapSetupPresenter.swift | 1 + .../Swaps/Setup/SwapSetupViewController.swift | 3 ++- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 43856e302c..203cfe2049 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -797,11 +797,10 @@ 77AB55592AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */; }; 77AB555B2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */; }; 77AB555D2AA24BA90058814E /* OperationDetailsPoolRewardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */; }; + 77C9761E2AF220130049272C /* SwapSelectedChainAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9761D2AF220130049272C /* SwapSelectedChainAsset.swift */; }; 77C976202AF36A170049272C /* SwapModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9761F2AF36A170049272C /* SwapModels.swift */; }; 77C976222AF39F180049272C /* TokenOperationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C976212AF39F180049272C /* TokenOperationTableViewCell.swift */; }; - 77C9BCBC2ACD1AF500022EA2 /* SwapViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBB2ACD1AF500022EA2 /* SwapViewModels.swift */; }; - 77C9761E2AF220130049272C /* SwapSelectedChainAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9761D2AF220130049272C /* SwapSelectedChainAsset.swift */; }; - 77C9BCBC2ACD1AF500022EA2 /* ViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */; }; + 77C976242AF3A5280049272C /* SwapViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C976232AF3A5280049272C /* SwapViewModels.swift */; }; 77C9BCBE2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */; }; 77C9BCC42ACD570100022EA2 /* SwapAssetsOperationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */; }; 77C9BCC62ACD571400022EA2 /* SwapAssetOperationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCC52ACD571400022EA2 /* SwapAssetOperationPresenter.swift */; }; @@ -4854,11 +4853,10 @@ 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashModel.swift; sourceTree = ""; }; 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashViewModel.swift; sourceTree = ""; }; 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsPoolRewardView.swift; sourceTree = ""; }; + 77C9761D2AF220130049272C /* SwapSelectedChainAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapSelectedChainAsset.swift; sourceTree = ""; }; 77C9761F2AF36A170049272C /* SwapModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapModels.swift; sourceTree = ""; }; 77C976212AF39F180049272C /* TokenOperationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenOperationTableViewCell.swift; sourceTree = ""; }; - 77C9BCBB2ACD1AF500022EA2 /* SwapViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapViewModels.swift; sourceTree = ""; }; - 77C9761D2AF220130049272C /* SwapSelectedChainAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapSelectedChainAsset.swift; sourceTree = ""; }; - 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModels.swift; sourceTree = ""; }; + 77C976232AF3A5280049272C /* SwapViewModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapViewModels.swift; sourceTree = ""; }; 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapsSetupViewModelFactory.swift; sourceTree = ""; }; 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationInteractor.swift; sourceTree = ""; }; 77C9BCC52ACD571400022EA2 /* SwapAssetOperationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetOperationPresenter.swift; sourceTree = ""; }; @@ -9918,7 +9916,7 @@ 77C9BCBA2ACD1AE800022EA2 /* Model */ = { isa = PBXGroup; children = ( - 77C9BCBB2ACD1AF500022EA2 /* SwapViewModels.swift */, + 77C976232AF3A5280049272C /* SwapViewModels.swift */, 77C9761F2AF36A170049272C /* SwapModels.swift */, 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */, ); @@ -21649,6 +21647,7 @@ 84AEEE6A27A9204C005EBA77 /* AssetListSettingsCell.swift in Sources */, F466AA85273D0D4200D14021 /* SettingsSectionHeaderView.swift in Sources */, 842BDB25278C3ACB00AB4B5A /* DAppBrowserMetadataState.swift in Sources */, + 77C976242AF3A5280049272C /* SwapViewModels.swift in Sources */, 64B508A1A3D820AA8DBCFAA3 /* AccountExportPasswordWireframe.swift in Sources */, F462B351260C7DBE0005AB01 /* StakingRewardHistoryTableCell.swift in Sources */, 840302E8292D00380013F356 /* GovernanceAssetSelectionPresenter.swift in Sources */, @@ -21892,7 +21891,6 @@ 845B821926EF808D00D25C72 /* MetaAccountMapper.swift in Sources */, 19A29027666EB5388CBFAD61 /* StakingRewardDetailsInteractor.swift in Sources */, 846AC7EF2638D9200075F7DA /* YourValidatorTableCell.swift in Sources */, - 77C9BCBC2ACD1AF500022EA2 /* SwapViewModels.swift in Sources */, C937154FA9021AECD72A871B /* StakingRewardDetailsViewController.swift in Sources */, 84770F27291F7CD400852A33 /* ReferendumDetailsInitData.swift in Sources */, 84B8AA8529F910AD00347A37 /* WalletConnectStateError.swift in Sources */, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 2a8b707eec..bf91e9d5b7 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -504,6 +504,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideButtonState() provideSettingsState() provideFeeViewModel() + provideErrors() refreshQuote(direction: .sell, forceUpdate: false) view?.didReceive(focus: newFocus) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index d209ff510f..e33e400f11 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -259,7 +259,8 @@ extension SwapSetupViewController: SwapSetupViewProtocol { rootView.receiveAmountInputView.set(focused: true) } } - func didReceive(errors: [SwapSetupViewError]) { + + func didReceive(errors: [SwapSetupViewError]) { if errors.contains(.insufficientToken) { rootView.changeDepositTokenButtonVisibility(hidden: false) } else { From 3156b4f5fdc4df1a728d6cc4f47e4a1339e372d9 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 2 Nov 2023 13:40:59 +0300 Subject: [PATCH 116/204] bugfix after merging --- novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index bf91e9d5b7..4009b503de 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -480,8 +480,8 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { Swift.swap(&payChainAsset, &receiveChainAsset) Swift.swap(&payAssetBalance, &receiveAssetBalance) Swift.swap(&payAssetPriceData, &receiveAssetPriceData) - Swift.swap(&payAssetSelfSufficient, &receiveAssetSelfSufficient) + interactor.update(payChainAsset: payChainAsset) interactor.update(receiveChainAsset: receiveChainAsset) let newFocus: TextFieldFocus? @@ -505,7 +505,6 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideSettingsState() provideFeeViewModel() provideErrors() - refreshQuote(direction: .sell, forceUpdate: false) view?.didReceive(focus: newFocus) } From 9e1c4ffbe90444ede4f31042513d056dcd2647b4 Mon Sep 17 00:00:00 2001 From: ERussel Date: Thu, 2 Nov 2023 17:16:27 +0100 Subject: [PATCH 117/204] fix merge conflicts --- .../Modules/Swaps/Setup/SwapSetupPresenter.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 71d3381069..633472fc46 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -787,7 +787,14 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { } self.fee = fee + provideFeeViewModel() + + if case .rate = payAmountInput { + providePayInputPriceViewModel() + providePayAmountInputViewModel() + } + provideButtonState() provideErrors() } @@ -868,7 +875,9 @@ extension SwapSetupPresenter: ModalPickerViewControllerDelegate { ) case .receive: guard let payChainAsset = payChainAsset, - let metaChainAccountResponse = selectedAccount.fetchMetaChainAccount(for: payChainAsset.chain.accountRequest()) else { + let metaChainAccountResponse = selectedAccount.fetchMetaChainAccount( + for: payChainAsset.chain.accountRequest() + ) else { return } wireframe.showDepositTokensByReceive( From 86ebe6c4ca2ea814b5567f8cc9f515a69ee2d10c Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 2 Nov 2023 22:34:57 +0300 Subject: [PATCH 118/204] add view --- novawallet.xcodeproj/project.pbxproj | 8 ++ .../OperationDetailsPresenter.swift | 12 ++ .../OperationDetailsProtocols.swift | 5 +- .../OperationDetailsViewController.swift | 52 +++++++++ .../View/OperationDetailsSwapView.swift | 104 ++++++++++++++++++ .../ViewModel/OperationDetailsViewModel.swift | 1 + .../ViewModel/OperationSwapViewModel.swift | 11 ++ novawallet/en.lproj/Localizable.strings | 1 + novawallet/ru.lproj/Localizable.strings | 1 + 9 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 novawallet/Modules/OperationDetails/View/OperationDetailsSwapView.swift create mode 100644 novawallet/Modules/OperationDetails/ViewModel/OperationSwapViewModel.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index a92e36b21e..ab346a8746 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -798,6 +798,8 @@ 77AB555B2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */; }; 77AB555D2AA24BA90058814E /* OperationDetailsPoolRewardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */; }; 77C9761E2AF220130049272C /* SwapSelectedChainAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9761D2AF220130049272C /* SwapSelectedChainAsset.swift */; }; + 77C976262AF421AE0049272C /* OperationDetailsSwapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C976252AF421AE0049272C /* OperationDetailsSwapView.swift */; }; + 77C976282AF426100049272C /* OperationSwapViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C976272AF426100049272C /* OperationSwapViewModel.swift */; }; 77C9BCBC2ACD1AF500022EA2 /* ViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */; }; 77C9BCBE2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */; }; 77C9BCC42ACD570100022EA2 /* SwapAssetsOperationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */; }; @@ -4852,6 +4854,8 @@ 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashViewModel.swift; sourceTree = ""; }; 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsPoolRewardView.swift; sourceTree = ""; }; 77C9761D2AF220130049272C /* SwapSelectedChainAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapSelectedChainAsset.swift; sourceTree = ""; }; + 77C976252AF421AE0049272C /* OperationDetailsSwapView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsSwapView.swift; sourceTree = ""; }; + 77C976272AF426100049272C /* OperationSwapViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationSwapViewModel.swift; sourceTree = ""; }; 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModels.swift; sourceTree = ""; }; 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapsSetupViewModelFactory.swift; sourceTree = ""; }; 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationInteractor.swift; sourceTree = ""; }; @@ -10740,6 +10744,7 @@ children = ( 842A736A27DB7A2E006EE1EA /* OperationDetailsViewModel.swift */, 842A736C27DB7B5E006EE1EA /* OperationTransferViewModel.swift */, + 77C976272AF426100049272C /* OperationSwapViewModel.swift */, 842A736E27DB7E57006EE1EA /* OperationExtrinsicViewModel.swift */, 842A737227DB7F75006EE1EA /* OperationRewardOrSlashViewModel.swift */, 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */, @@ -10753,6 +10758,7 @@ isa = PBXGroup; children = ( 842A737B27DCC488006EE1EA /* OperationDetailsTransferView.swift */, + 77C976252AF421AE0049272C /* OperationDetailsSwapView.swift */, 842A737D27DCD1A0006EE1EA /* OperationDetailsExtrinsicView.swift */, 842A737F27DCD427006EE1EA /* OperationDetailsRewardView.swift */, 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */, @@ -22109,6 +22115,7 @@ 0E6C2939AFB3D125C760D5A0 /* CrowdloanContributionSetupProtocols.swift in Sources */, 8410562C27AF1C15004F5CA3 /* Ethereum+Checksum.swift in Sources */, 4E5CD7B8821FA5298EA1598E /* CrowdloanContributionSetupWireframe.swift in Sources */, + 77C976262AF421AE0049272C /* OperationDetailsSwapView.swift in Sources */, 0CC2E5622A6E5C43004092E7 /* NominationPoolsAccountUpdatingService.swift in Sources */, 8846F72029D56A0700B8B776 /* Web3NameAddressListPresentable.swift in Sources */, 77A6F5C12A2F1724004AFD1A /* MessageSheetPresentable+presentOperationResult.swift in Sources */, @@ -22356,6 +22363,7 @@ 0C6F0C9E2A69723B007170C6 /* StartStakingStateProtocol.swift in Sources */, 84AE7AAD27D3839D00495267 /* StackCellViewModel.swift in Sources */, 848CC94628D9FC46009EB4B0 /* ConvictionVotingTally.swift in Sources */, + 77C976282AF426100049272C /* OperationSwapViewModel.swift in Sources */, 84953F6A2934C9E20033F47D /* EtherscanERC20OperationFactory.swift in Sources */, 0CE629D72AA9B5E200E250BD /* BalanceViewModelFactory.swift in Sources */, CD9359A2720F2EE1D4E09DF6 /* DAppTxDetailsWireframe.swift in Sources */, diff --git a/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift b/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift index ba36da0d06..9cef0aa98e 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift @@ -151,6 +151,18 @@ extension OperationDetailsPresenter: OperationDetailsPresenterProtocol { ) } } + + func showRateInfo() { + wireframe.showRateInfo(from: view) + } + + func showNetworkFeeInfo() { + wireframe.showFeeInfo(from: view) + } + + func repeatOperation() { + // TODO: Show swap + } } extension OperationDetailsPresenter: OperationDetailsInteractorOutputProtocol { diff --git a/novawallet/Modules/OperationDetails/OperationDetailsProtocols.swift b/novawallet/Modules/OperationDetails/OperationDetailsProtocols.swift index e94ed998f0..4b557e26bf 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsProtocols.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsProtocols.swift @@ -9,6 +9,9 @@ protocol OperationDetailsPresenterProtocol: AnyObject { func showRecepientActions() func showOperationActions() func send() + func showRateInfo() + func showNetworkFeeInfo() + func repeatOperation() } protocol OperationDetailsInteractorInputProtocol: AnyObject { @@ -20,7 +23,7 @@ protocol OperationDetailsInteractorOutputProtocol: AnyObject { } protocol OperationDetailsWireframeProtocol: AlertPresentable, ErrorPresentable, - AddressOptionsPresentable, OperationIdOptionsPresentable { + AddressOptionsPresentable, OperationIdOptionsPresentable, ShortTextInfoPresentable { func showSend( from view: OperationDetailsViewProtocol?, displayAddress: DisplayAddress, diff --git a/novawallet/Modules/OperationDetails/OperationDetailsViewController.swift b/novawallet/Modules/OperationDetails/OperationDetailsViewController.swift index a86aa7855d..4994453a09 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsViewController.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsViewController.swift @@ -264,6 +264,44 @@ final class OperationDetailsViewController: UIViewController, ViewHolder { return view } + private func applySwap( + viewModel: OperationSwapViewModel + ) { + let swapView: OperationDetailsSwapView = rootView.setupLocalizableView() + swapView.locale = selectedLocale + swapView.bind(viewModel: viewModel) + + let repeatOperationButton = rootView.setupActionButton() + repeatOperationButton.imageWithTitleView?.title = R.string.localizable.commonActionRepeatOperation( + preferredLanguages: selectedLocale.rLanguages + ) + swapView.rateCell.addTarget( + self, + action: #selector(actionRate), + for: .touchUpInside + ) + swapView.networkFeeCell.addTarget( + self, + action: #selector(actionNetworkFee), + for: .touchUpInside + ) + swapView.accountCell.addTarget( + self, + action: #selector(actionSender), + for: .touchUpInside + ) + swapView.transactionHashView.addTarget( + self, + action: #selector(actionOperationId), + for: .touchUpInside + ) + repeatOperationButton.addTarget( + self, + action: #selector(actionRepeatSwapOperation), + for: .touchUpInside + ) + } + @objc func actionSender() { presenter.showSenderActions() } @@ -279,6 +317,18 @@ final class OperationDetailsViewController: UIViewController, ViewHolder { @objc func actionSend() { presenter.send() } + + @objc func actionRate() { + presenter.showRateInfo() + } + + @objc func actionNetworkFee() { + presenter.showNetworkFeeInfo() + } + + @objc func actionRepeatSwapOperation() { + presenter.repeatOperation() + } } extension OperationDetailsViewController: OperationDetailsViewProtocol { @@ -305,6 +355,8 @@ extension OperationDetailsViewController: OperationDetailsViewProtocol { applyPoolReward(viewModel: poolRewardViewModel, networkViewModel: networkViewModel) case let .poolSlash(poolSlashViewModel): applyPoolSlash(viewModel: poolSlashViewModel, networkViewModel: networkViewModel) + case let .swap(swapViewModel): + applySwap(viewModel: swapViewModel) } } } diff --git a/novawallet/Modules/OperationDetails/View/OperationDetailsSwapView.swift b/novawallet/Modules/OperationDetails/View/OperationDetailsSwapView.swift new file mode 100644 index 0000000000..88f58257c4 --- /dev/null +++ b/novawallet/Modules/OperationDetails/View/OperationDetailsSwapView.swift @@ -0,0 +1,104 @@ +import UIKit + +final class OperationDetailsSwapView: ScrollableContainerLayoutView, LocalizableViewProtocol { + let senderTableView = StackTableView() + + let pairsView = SwapPairView() + let detailsTableView = StackTableView() + let walletTableView = StackTableView() + let transactionTableView = StackTableView() + + let rateCell: SwapInfoViewCell = .create { + $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() + $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote + $0.titleButton.imageWithTitleView?.iconImage = R.image.iconInfoFilledAccent() + } + + let networkFeeCell = SwapNetworkFeeViewCell() + + let walletCell = StackTableCell() + + let accountCell: StackInfoTableCell = .create { + $0.detailsLabel.lineBreakMode = .byTruncatingMiddle + $0.infoIcon = R.image.iconInfoFilledAccent() + } + + let transactionHashView: StackInfoTableCell = .create { + $0.detailsLabel.lineBreakMode = .byTruncatingMiddle + } + + let actionButton: TriangularedButton = .create { + $0.applyDefaultStyle() + } + + var locale: Locale = .current { + didSet { + if locale != oldValue { + setup(locale: locale) + } + } + } + + func bind(viewModel: OperationSwapViewModel) { + pairsView.leftAssetView.bind(viewModel: viewModel.assetIn) + pairsView.rigthAssetView.bind(viewModel: viewModel.assetOut) + rateCell.bind(loadableViewModel: .loaded(value: viewModel.rate)) + networkFeeCell.bind(loadableViewModel: .loaded(value: .init( + isEditable: false, + balanceViewModel: viewModel.fee + ))) + walletCell.bind(viewModel: .init( + details: viewModel.wallet.walletName ?? "", + imageViewModel: viewModel.wallet.walletIcon + )) + accountCell.bind(viewModel: .init( + details: viewModel.wallet.address, + imageViewModel: viewModel.wallet.addressIcon + )) + transactionHashView.bind(details: viewModel.transactionHash) + } + + private func setup(locale: Locale) { + rateCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupDetailsRate( + preferredLanguages: locale.rLanguages) + networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetwork( + preferredLanguages: locale.rLanguages) + rateCell.titleButton.invalidateLayout() + networkFeeCell.titleButton.invalidateLayout() + + walletCell.titleLabel.text = R.string.localizable.commonWallet( + preferredLanguages: locale.rLanguages) + accountCell.titleLabel.text = R.string.localizable.commonAccount( + preferredLanguages: locale.rLanguages) + transactionHashView.titleLabel.text = R.string.localizable.commonTxId( + preferredLanguages: locale.rLanguages + ) + } + + override func setupStyle() { + backgroundColor = .clear + } + + override func setupLayout() { + super.setupLayout() + + stackView.layoutMargins = UIEdgeInsets(top: 12, left: 16, bottom: 0, right: 16) + addArrangedSubview(pairsView, spacingAfter: 8) + addArrangedSubview(detailsTableView, spacingAfter: 8) + addArrangedSubview(walletTableView, spacingAfter: 8) + addArrangedSubview(transactionTableView) + + detailsTableView.addArrangedSubview(rateCell) + detailsTableView.addArrangedSubview(networkFeeCell) + walletTableView.addArrangedSubview(walletCell) + walletTableView.addArrangedSubview(accountCell) + transactionTableView.addArrangedSubview(transactionHashView) + + addSubview(actionButton) + actionButton.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) + make.bottom.equalTo(safeAreaLayoutGuide).inset(UIConstants.actionBottomInset) + make.height.equalTo(UIConstants.actionHeight) + } + } +} diff --git a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModel.swift b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModel.swift index 32f65641f3..12bd933c52 100644 --- a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModel.swift +++ b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModel.swift @@ -9,6 +9,7 @@ struct OperationDetailsViewModel { case contract(_ viewModel: OperationContractCallViewModel) case poolReward(_ viewModel: OperationPoolRewardOrSlashViewModel) case poolSlash(_ viewModel: OperationPoolRewardOrSlashViewModel) + case swap(_ viewModel: OperationSwapViewModel) } let time: String diff --git a/novawallet/Modules/OperationDetails/ViewModel/OperationSwapViewModel.swift b/novawallet/Modules/OperationDetails/ViewModel/OperationSwapViewModel.swift new file mode 100644 index 0000000000..cb762fd75e --- /dev/null +++ b/novawallet/Modules/OperationDetails/ViewModel/OperationSwapViewModel.swift @@ -0,0 +1,11 @@ +import Foundation + +struct OperationSwapViewModel { + let isOutgoing: Bool + let assetIn: SwapAssetAmountViewModel + let assetOut: SwapAssetAmountViewModel + let rate: String + let fee: BalanceViewModelProtocol + let wallet: WalletAccountViewModel + let transactionHash: String +} diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 31b5818549..2344fe6c9a 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1411,3 +1411,4 @@ "swaps.setup.price.difference.description" = "Price difference refers to the difference in price between two different assets. When making a swap in crypto, the price difference is usually the difference between the price of the asset you are swapping for and the price of the asset you are swapping with."; "swaps.setup.error.rate.was.updated.title" = "Swap rate was updated"; "swaps.setup.error.rate.was.updated.message" = "Old rate: %@.\nNew rate:%@"; +"common.action.repeat.operation" = "Repeat the operation"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index cbe4848c2d..30011a23fe 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1411,3 +1411,4 @@ "swaps.setup.price.difference.description" = "Разница в цене относится к разнице в цене между двумя различными активами. При совершении обмена в криптовалюте разница в цене обычно представляет собой разницу между ценой актива, на который вы меняете, и ценой актива, на который вы меняетесь."; "swaps.setup.error.rate.was.updated.title" = "Обменный курс был обновлен"; "swaps.setup.error.rate.was.updated.message" = "Было: %@.\nСтало:%@"; +"common.action.repeat.operation" = "Повторить операцию"; From 4c9cd62e77eb13ab7cd7515c8adfcc3d6dd0e8b6 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sat, 4 Nov 2023 09:10:49 +0100 Subject: [PATCH 119/204] add validation logic --- novawallet.xcodeproj/project.pbxproj | 20 +- .../Helpers/CancellableCallHelper.swift | 20 ++ .../Common/Substrate/Types/AccountInfo.swift | 1 + .../Swaps/Base/SwapBaseInteractor.swift | 53 +++++ .../Swaps/Base/SwapBaseProtocols.swift | 3 + .../Swaps/Confirm/SwapConfirmInteractor.swift | 4 + .../Swaps/Confirm/SwapConfirmPresenter.swift | 9 + .../Confirm/SwapConfirmViewFactory.swift | 7 + .../Swaps/Setup/SwapSetupInteractor.swift | 7 +- .../Setup/SwapSetupPresenter+Validating.swift | 17 +- .../Swaps/Setup/SwapSetupPresenter.swift | 38 ++++ .../Swaps/Setup/SwapSetupViewFactory.swift | 8 +- .../Validation/SwapDataValidatorFactory.swift | 75 ------ .../Swaps/Validation/SwapFeeParams.swift | 74 ------ .../Swaps/Validation/SwapMaxErrorParams.swift | 10 +- .../Modules/Swaps/Validation/SwapModel.swift | 215 ++++++++++++++++++ 16 files changed, 385 insertions(+), 176 deletions(-) delete mode 100644 novawallet/Modules/Swaps/Validation/SwapFeeParams.swift create mode 100644 novawallet/Modules/Swaps/Validation/SwapModel.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 8eef071d8a..78493bc417 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -68,6 +68,8 @@ 0C13D3212A822B220054BB6F /* StartStakingDirectConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3202A822B220054BB6F /* StartStakingDirectConfirmWireframe.swift */; }; 0C13D3242A823D810054BB6F /* StartStakingExtrinsicProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3232A823D810054BB6F /* StartStakingExtrinsicProxy.swift */; }; 0C13D3262A8275400054BB6F /* StartStakingFeeIdFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3252A82753F0054BB6F /* StartStakingFeeIdFactory.swift */; }; + 0C13DFC92AF4FFC200E5F355 /* SwapMaxErrorParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFC82AF4FFC200E5F355 /* SwapMaxErrorParams.swift */; }; + 0C13DFCB2AF6182500E5F355 /* SwapModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFCA2AF6182500E5F355 /* SwapModel.swift */; }; 0C154A0E2A45995500932C3F /* CompoundComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C154A0D2A45995500932C3F /* CompoundComparator.swift */; }; 0C17BD972A42F162004AF9E7 /* WalletViewModelObserverContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */; }; 0C17BD992A42F1BE004AF9E7 /* MoneyPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */; }; @@ -695,8 +697,6 @@ 7719018C2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719018B2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift */; }; 7719018E2AE0E71F00D9C918 /* SwapErrorPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719018D2AE0E71F00D9C918 /* SwapErrorPresentable.swift */; }; 771901902AE2424B00D9C918 /* SwapsValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719018F2AE2424B00D9C918 /* SwapsValidationTests.swift */; }; - 771901932AE2736E00D9C918 /* SwapFeeParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901922AE2736E00D9C918 /* SwapFeeParams.swift */; }; - 771901952AE2739800D9C918 /* SwapMaxErrorParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901942AE2739800D9C918 /* SwapMaxErrorParams.swift */; }; 771901972AE2897F00D9C918 /* SwapSetupPresenter+Validating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901962AE2897F00D9C918 /* SwapSetupPresenter+Validating.swift */; }; 7719019B2AE670AE00D9C918 /* ShortTextInfoPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719019A2AE670AE00D9C918 /* ShortTextInfoPresentable.swift */; }; 7719019D2AE6996600D9C918 /* SwapPairView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719019C2AE6996600D9C918 /* SwapPairView.swift */; }; @@ -801,11 +801,9 @@ 77AB55592AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */; }; 77AB555B2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */; }; 77AB555D2AA24BA90058814E /* OperationDetailsPoolRewardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */; }; - 77C9761E2AF220130049272C /* SwapSelectedChainAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9761D2AF220130049272C /* SwapSelectedChainAsset.swift */; }; 77C976202AF36A170049272C /* SwapModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9761F2AF36A170049272C /* SwapModels.swift */; }; 77C976222AF39F180049272C /* TokenOperationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C976212AF39F180049272C /* TokenOperationTableViewCell.swift */; }; 77C976242AF3A5280049272C /* SwapViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C976232AF3A5280049272C /* SwapViewModels.swift */; }; - 77C9BCBC2ACD1AF500022EA2 /* ViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */; }; 77C9BCBE2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */; }; 77C9BCC42ACD570100022EA2 /* SwapAssetsOperationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */; }; 77C9BCC62ACD571400022EA2 /* SwapAssetOperationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCC52ACD571400022EA2 /* SwapAssetOperationPresenter.swift */; }; @@ -4125,6 +4123,8 @@ 0C13D3202A822B220054BB6F /* StartStakingDirectConfirmWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingDirectConfirmWireframe.swift; sourceTree = ""; }; 0C13D3232A823D810054BB6F /* StartStakingExtrinsicProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingExtrinsicProxy.swift; sourceTree = ""; }; 0C13D3252A82753F0054BB6F /* StartStakingFeeIdFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingFeeIdFactory.swift; sourceTree = ""; }; + 0C13DFC82AF4FFC200E5F355 /* SwapMaxErrorParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapMaxErrorParams.swift; sourceTree = ""; }; + 0C13DFCA2AF6182500E5F355 /* SwapModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapModel.swift; sourceTree = ""; }; 0C154A0D2A45995500932C3F /* CompoundComparator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompoundComparator.swift; sourceTree = ""; }; 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletViewModelObserverContainer.swift; sourceTree = ""; }; 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyPresentable.swift; sourceTree = ""; }; @@ -4756,8 +4756,6 @@ 7719018B2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapDataValidatorFactory.swift; sourceTree = ""; }; 7719018D2AE0E71F00D9C918 /* SwapErrorPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapErrorPresentable.swift; sourceTree = ""; }; 7719018F2AE2424B00D9C918 /* SwapsValidationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapsValidationTests.swift; sourceTree = ""; }; - 771901922AE2736E00D9C918 /* SwapFeeParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapFeeParams.swift; sourceTree = ""; }; - 771901942AE2739800D9C918 /* SwapMaxErrorParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapMaxErrorParams.swift; sourceTree = ""; }; 771901962AE2897F00D9C918 /* SwapSetupPresenter+Validating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwapSetupPresenter+Validating.swift"; sourceTree = ""; }; 7719019A2AE670AE00D9C918 /* ShortTextInfoPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortTextInfoPresentable.swift; sourceTree = ""; }; 7719019C2AE6996600D9C918 /* SwapPairView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPairView.swift; sourceTree = ""; }; @@ -4863,11 +4861,9 @@ 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashModel.swift; sourceTree = ""; }; 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashViewModel.swift; sourceTree = ""; }; 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsPoolRewardView.swift; sourceTree = ""; }; - 77C9761D2AF220130049272C /* SwapSelectedChainAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapSelectedChainAsset.swift; sourceTree = ""; }; 77C9761F2AF36A170049272C /* SwapModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapModels.swift; sourceTree = ""; }; 77C976212AF39F180049272C /* TokenOperationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenOperationTableViewCell.swift; sourceTree = ""; }; 77C976232AF3A5280049272C /* SwapViewModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapViewModels.swift; sourceTree = ""; }; - 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModels.swift; sourceTree = ""; }; 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapsSetupViewModelFactory.swift; sourceTree = ""; }; 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationInteractor.swift; sourceTree = ""; }; 77C9BCC52ACD571400022EA2 /* SwapAssetOperationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetOperationPresenter.swift; sourceTree = ""; }; @@ -9655,8 +9651,8 @@ children = ( 7719018B2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift */, 7719018D2AE0E71F00D9C918 /* SwapErrorPresentable.swift */, - 771901922AE2736E00D9C918 /* SwapFeeParams.swift */, - 771901942AE2739800D9C918 /* SwapMaxErrorParams.swift */, + 0C13DFC82AF4FFC200E5F355 /* SwapMaxErrorParams.swift */, + 0C13DFCA2AF6182500E5F355 /* SwapModel.swift */, ); path = Validation; sourceTree = ""; @@ -20156,6 +20152,7 @@ AEF7404E25E6DC9400407D41 /* InflationCurveRewardEngine.swift in Sources */, 848CCB522833D29F00A1FD00 /* StakingParachainStatics.swift in Sources */, 84CA68D926BE9E7F003B9453 /* SpecVersionSubscription.swift in Sources */, + 0C13DFCB2AF6182500E5F355 /* SwapModel.swift in Sources */, 84C74363251E4C2F009576C6 /* DummySigner.swift in Sources */, 8454C26A2632B8CE00657DAD /* BalanceDepositEvent.swift in Sources */, 0C0CB38A2AC56A1600EAC516 /* AssetConversionPallet+Path.swift in Sources */, @@ -20260,6 +20257,7 @@ 84E2ABC82992724600A5D3C1 /* GovernanceDelegatorAction.swift in Sources */, 8463A70325E2FCD0003B8160 /* WeakWrapper.swift in Sources */, 8459A9CA2746A1BC000D6278 /* CrowdloanOffchainSubscriber.swift in Sources */, + 0C13DFC92AF4FFC200E5F355 /* SwapMaxErrorParams.swift in Sources */, 8401620B25E144D50087A5F3 /* AmountInputAccessoryView.swift in Sources */, 88F19DE028D8D0F600F6E459 /* LoadableViewModelState+Addition.swift in Sources */, 844C3E652A07627E00C4305F /* DAppWalletAuthViewModel.swift in Sources */, @@ -20870,7 +20868,6 @@ 84FBED0329279CF200FBEB83 /* ContractTransactionHistoryUpdater.swift in Sources */, 84893BFE24DA0000008F6A3F /* FieldStatus.swift in Sources */, 84F51053263AB440005D15AE /* StakingUnbondSetupLayout.swift in Sources */, - 771901952AE2739800D9C918 /* SwapMaxErrorParams.swift in Sources */, 77F033952A8142B0006BC67E /* StakingTypeValidatorView.swift in Sources */, 84BC704B289F1338008A9758 /* ExpirationTimeViewModel.swift in Sources */, 849014BF24AA87E4008F705E /* ScreenAuthorizationProtocol.swift in Sources */, @@ -21945,7 +21942,6 @@ 84FEF3E528089FFB0042CBE7 /* TextInputField.swift in Sources */, 0CEB4ED32AF1689D0048FD84 /* AssetConversionAggregationFactory.swift in Sources */, 84D17EE12805A62600F7BAFF /* DAppAlertPresentable.swift in Sources */, - 771901932AE2736E00D9C918 /* SwapFeeParams.swift in Sources */, A871B6ABACAE8A811010F792 /* StakingPayoutConfirmationWireframe.swift in Sources */, 1795E946F1E386442E96E2BC /* StakingPayoutConfirmationPresenter.swift in Sources */, AEFA82BC4285117096BCBB16 /* StakingPayoutConfirmationInteractor.swift in Sources */, diff --git a/novawallet/Common/Helpers/CancellableCallHelper.swift b/novawallet/Common/Helpers/CancellableCallHelper.swift index 87a72282de..722ca1fd1c 100644 --- a/novawallet/Common/Helpers/CancellableCallHelper.swift +++ b/novawallet/Common/Helpers/CancellableCallHelper.swift @@ -33,6 +33,26 @@ final class CancellableCallStore { } } +func execute( + wrapper: CompoundOperationWrapper, + inOperationQueue operationQueue: OperationQueue, + runningCallbackIn callbackQueue: DispatchQueue?, + callbackClosure: @escaping (Result) -> Void +) { + wrapper.targetOperation.completionBlock = { + dispatchInQueueWhenPossible(callbackQueue) { + do { + let value = try wrapper.targetOperation.extractNoCancellableResultData() + callbackClosure(.success(value)) + } catch { + callbackClosure(.failure(error)) + } + } + } + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) +} + func executeCancellable( wrapper: CompoundOperationWrapper, inOperationQueue operationQueue: OperationQueue, diff --git a/novawallet/Common/Substrate/Types/AccountInfo.swift b/novawallet/Common/Substrate/Types/AccountInfo.swift index 28f80eec87..9c8d789bd6 100644 --- a/novawallet/Common/Substrate/Types/AccountInfo.swift +++ b/novawallet/Common/Substrate/Types/AccountInfo.swift @@ -4,6 +4,7 @@ import BigInt struct AccountInfo: Codable, Equatable { @StringCodable var nonce: UInt32 + @StringCodable var consumers: UInt32 let data: AccountData } diff --git a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift index 1aff798ff9..108dd31212 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift @@ -6,6 +6,8 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB weak var basePresenter: SwapBaseInteractorOutputProtocol? let assetConversionAggregator: AssetConversionAggregationFactoryProtocol let assetConversionFeeService: AssetConversionFeeServiceProtocol + let chainRegistry: ChainRegistryProtocol + let assetStorageFactory: AssetStorageInfoOperationFactoryProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol let currencyManager: CurrencyManagerProtocol @@ -22,6 +24,8 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB init( assetConversionAggregator: AssetConversionAggregationFactoryProtocol, assetConversionFeeService: AssetConversionFeeServiceProtocol, + chainRegistry: ChainRegistryProtocol, + assetStorageFactory: AssetStorageInfoOperationFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, currencyManager: CurrencyManagerProtocol, @@ -30,6 +34,8 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB ) { self.assetConversionAggregator = assetConversionAggregator self.assetConversionFeeService = assetConversionFeeService + self.chainRegistry = chainRegistry + self.assetStorageFactory = assetStorageFactory self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory self.currencyManager = currencyManager @@ -41,6 +47,39 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB quoteCall.cancel() } + private func provideAssetBalanceExistense(for chainAsset: ChainAsset) { + guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { + let error = ChainRegistryError.runtimeMetadaUnavailable + basePresenter?.didReceive(baseError: .assetBalanceExistense(error, chainAsset)) + return + } + + let wrapper = assetStorageFactory.createAssetBalanceExistenceOperation( + chainId: chainAsset.chain.chainId, + asset: chainAsset.asset, + runtimeProvider: runtimeService, + operationQueue: operationQueue + ) + + execute( + wrapper: wrapper, + inOperationQueue: operationQueue, + runningCallbackIn: .main + ) { [weak self] result in + switch result { + case let .success(existense): + self?.basePresenter?.didReceiveAssetBalance( + existense: existense, + chainAssetId: chainAsset.chainAssetId + ) + case let .failure(error): + self?.basePresenter?.didReceive( + baseError: .assetBalanceExistense(error, chainAsset) + ) + } + } + } + func updateFeeModelBuilder(for chain: ChainModel) { guard let utilityAsset = chain.utilityChainAsset(), @@ -157,6 +196,8 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB updateFeeModelBuilder(for: chainAsset.chain) + provideAssetBalanceExistense(for: chainAsset) + priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) } @@ -170,6 +211,8 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB feeModelBuilder?.apply(feeAsset: utilityAsset) } + provideAssetBalanceExistense(for: chainAsset) + guard let chainAccount = chainAccountResponse(for: chainAsset) else { basePresenter?.didReceive(payAccountId: nil) return @@ -184,6 +227,12 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB updateFeeModelBuilder(for: chainAsset.chain) feeModelBuilder?.apply(feeAsset: chainAsset) + provideAssetBalanceExistense(for: chainAsset) + + if let utilityAsset = chainAsset.chain.utilityChainAsset(), !chainAsset.isUtilityAsset { + provideAssetBalanceExistense(for: utilityAsset) + } + priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) } @@ -205,6 +254,10 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB func remakePriceSubscription(for chainAsset: ChainAsset) { priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) } + + func retryAssetBalanceExistenseFetch(for chainAsset: ChainAsset) { + provideAssetBalanceExistense(for: chainAsset) + } } extension SwapBaseInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { diff --git a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift index 0e3381e892..ae13a17259 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift @@ -5,6 +5,7 @@ protocol SwapBaseInteractorInputProtocol: AnyObject { func calculateQuote(for args: AssetConversion.QuoteArgs) func calculateFee(args: AssetConversion.CallArgs) func remakePriceSubscription(for chainAsset: ChainAsset) + func retryAssetBalanceExistenseFetch(for chainAsset: ChainAsset) } protocol SwapBaseInteractorOutputProtocol: AnyObject { @@ -14,6 +15,7 @@ protocol SwapBaseInteractorOutputProtocol: AnyObject { func didReceive(price: PriceData?, priceId: AssetModel.PriceId) func didReceive(payAccountId: AccountId?) func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId: AccountId) + func didReceiveAssetBalance(existense: AssetBalanceExistence, chainAssetId: ChainAssetId) } enum SwapBaseError: Error { @@ -21,4 +23,5 @@ enum SwapBaseError: Error { case fetchFeeFailed(Error, TransactionFeeId, FeeChainAssetId?) case price(Error, AssetModel.PriceId) case assetBalance(Error, ChainAssetId, AccountId) + case assetBalanceExistense(Error, ChainAsset) } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift index eb1bd17148..ce51cc1a4b 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift @@ -17,6 +17,8 @@ final class SwapConfirmInteractor: SwapBaseInteractor { assetConversionFeeService: AssetConversionFeeServiceProtocol, assetConversionAggregator: AssetConversionAggregationFactoryProtocol, assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol, + chainRegistry: ChainRegistryProtocol, + assetStorageFactory: AssetStorageInfoOperationFactoryProtocol, runtimeService: RuntimeProviderProtocol, extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, @@ -35,6 +37,8 @@ final class SwapConfirmInteractor: SwapBaseInteractor { super.init( assetConversionAggregator: assetConversionAggregator, assetConversionFeeService: assetConversionFeeService, + chainRegistry: chainRegistry, + assetStorageFactory: assetStorageFactory, priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, currencyManager: currencyManager, diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index c560b18f0a..6c3474de69 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -19,6 +19,7 @@ final class SwapConfirmPresenter { private var payAccountId: AccountId? private var chainAccountResponse: MetaChainAccountResponse private var balances: [ChainAssetId: AssetBalance?] = [:] + private var assetBalanceExistences: [ChainAssetId: AssetBalanceExistence] = [:] init( interactor: SwapConfirmInteractorInputProtocol, @@ -402,6 +403,10 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { balances[chainAsset] = balance } + func didReceiveAssetBalance(existense: AssetBalanceExistence, chainAssetId: ChainAssetId) { + assetBalanceExistences[chainAssetId] = existense + } + func didReceive(baseError: SwapBaseError) { switch baseError { case let .quote(_, args): @@ -426,6 +431,10 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in self?.interactor.setup() } + case let .assetBalanceExistense(_, chainAsset): + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryAssetBalanceExistenseFetch(for: chainAsset) + } } } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index aac228b127..2c413e8ec8 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -99,11 +99,18 @@ struct SwapConfirmViewFactory { accountResponse: selectedAccount.chainAccount ) + let assetStorageFactory = AssetStorageInfoOperationFactory( + chainRegistry: chainRegistry, + operationQueue: operationQueue + ) + let interactor = SwapConfirmInteractor( initState: initState, assetConversionFeeService: feeService, assetConversionAggregator: assetConversionAggregator, assetConversionExtrinsicService: AssetHubExtrinsicService(chain: chain), + chainRegistry: chainRegistry, + assetStorageFactory: assetStorageFactory, runtimeService: runtimeService, extrinsicServiceFactory: extrinsicServiceFactory, priceLocalSubscriptionFactory: PriceProviderFactory.shared, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index d08f6dd85f..05e1e7845a 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -4,16 +4,16 @@ import BigInt final class SwapSetupInteractor: SwapBaseInteractor { let xcmTransfersSyncService: XcmTransfersSyncServiceProtocol - let chainRegistry: ChainRegistryProtocol private var xcmTransfers: XcmTransfers? private var canPayFeeInAssetCall = CancellableCallStore() init( xcmTransfersSyncService: XcmTransfersSyncServiceProtocol, - chainRegistry: ChainRegistryProtocol, assetConversionAggregatorFactory: AssetConversionAggregationFactoryProtocol, assetConversionFeeService: AssetConversionFeeServiceProtocol, + chainRegistry: ChainRegistryProtocol, + assetStorageFactory: AssetStorageInfoOperationFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, currencyManager: CurrencyManagerProtocol, @@ -21,11 +21,12 @@ final class SwapSetupInteractor: SwapBaseInteractor { operationQueue: OperationQueue ) { self.xcmTransfersSyncService = xcmTransfersSyncService - self.chainRegistry = chainRegistry super.init( assetConversionAggregator: assetConversionAggregatorFactory, assetConversionFeeService: assetConversionFeeService, + chainRegistry: chainRegistry, + assetStorageFactory: assetStorageFactory, priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, currencyManager: currencyManager, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift index 57b9217dc4..a8dd3a113f 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift @@ -7,13 +7,12 @@ extension SwapSetupPresenter { payChainAsset: ChainAsset, feeChainAsset: ChainAsset ) -> [DataValidating] { - let feeDecimal = fee.map { Decimal.fromSubstrateAmount( - $0.totalFee.targetAmount, - precision: Int16(feeChainAsset.asset.precision) - ) } ?? nil - let validators: [DataValidating] = [ - dataValidatingFactory.has(fee: feeDecimal, locale: selectedLocale) { [weak self] in + dataValidatingFactory.hasInPlank( + fee: fee?.totalFee.targetAmount, + locale: selectedLocale, + precision: feeChainAsset.assetDisplayInfo.assetPrecision + ) { [weak self] in self?.estimateFee() }, dataValidatingFactory.canSpendAmountInPlank( @@ -29,6 +28,12 @@ extension SwapSetupPresenter { asset: feeChainAsset.assetDisplayInfo, locale: selectedLocale ), + dataValidatingFactory.notViolatingMinBalancePaying( + fee: feeChainAsset.isUtilityAsset ? fee?.totalFee.targetAmount : 0, + total: utilityAssetBalance?.totalInPlank, + minBalance: utilityAssetMinBalance, + locale: selectedLocale + ), dataValidatingFactory.has( quote: quote, payChainAssetId: payChainAsset.chainAssetId, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 633472fc46..73aea4c83d 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -27,6 +27,14 @@ final class SwapSetupPresenter: PurchaseFlowManaging { receiveChainAsset.flatMap { balances[$0.chainAssetId] } } + var utilityAssetBalance: AssetBalance? { + guard let utilityAssetId = feeChainAsset?.chain.utilityChainAssetId() else { + return nil + } + + return balances[utilityAssetId] + } + private(set) var prices: [ChainAssetId: PriceData] = [:] var payAssetPriceData: PriceData? { @@ -55,6 +63,26 @@ final class SwapSetupPresenter: PurchaseFlowManaging { } } + private(set) var assetBalanceExistences: [ChainAssetId: AssetBalanceExistence] = [:] + + var payAssetMinBalance: BigUInt? { + payChainAsset.flatMap { assetBalanceExistences[$0.chainAssetId]?.minBalance } + } + + var receiveAssetMinBalance: BigUInt? { + receiveChainAsset.flatMap { assetBalanceExistences[$0.chainAssetId]?.minBalance } + } + + var feeAssetMinBalance: BigUInt? { + feeChainAsset.flatMap { assetBalanceExistences[$0.chainAssetId]?.minBalance } + } + + var utilityAssetMinBalance: BigUInt? { + feeChainAsset?.chain.utilityChainAsset().flatMap { + assetBalanceExistences[$0.chainAssetId]?.minBalance + } + } + private var slippage: BigRational? private var feeIdentifier: SwapSetupFeeIdentifier? private var accountId: AccountId? @@ -719,6 +747,10 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { handlePriceError(priceId: priceId) case let .assetBalance(_, chainAssetId, _): handleAssetBalanceError(chainAssetId: chainAssetId) + case let .assetBalanceExistense(_, chainAsset): + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryAssetBalanceExistenseFetch(for: chainAsset) + } } } @@ -836,6 +868,12 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { } } + func didReceiveAssetBalance(existense: AssetBalanceExistence, chainAssetId: ChainAssetId) { + logger.debug("Did receive existense for \(chainAssetId.stringValue): \(String(existense.minBalance))") + + assetBalanceExistences[chainAssetId] = existense + } + func didReceiveCanPayFeeInPayAsset(_ value: Bool, chainAssetId: ChainAssetId) { if payChainAsset?.chainAssetId == chainAssetId { canPayFeeInPayAsset = value diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index 283278d4ad..6c958b1e3b 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -81,11 +81,17 @@ struct SwapSetupViewFactory { operationQueue: operationQueue ) + let assetStorageFactory = AssetStorageInfoOperationFactory( + chainRegistry: chainRegistry, + operationQueue: operationQueue + ) + let interactor = SwapSetupInteractor( xcmTransfersSyncService: xcmTransfersSyncService, - chainRegistry: ChainRegistryFacade.sharedRegistry, assetConversionAggregatorFactory: assetConversionAggregator, assetConversionFeeService: feeService, + chainRegistry: ChainRegistryFacade.sharedRegistry, + assetStorageFactory: assetStorageFactory, priceLocalSubscriptionFactory: PriceProviderFactory.shared, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, currencyManager: currencyManager, diff --git a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift index 5a752085fc..7d20d6eb7c 100644 --- a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift +++ b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift @@ -10,11 +10,6 @@ protocol SwapDataValidatorFactoryProtocol: BaseDataValidatingFactoryProtocol { locale: Locale, onError: (() -> Void)? ) -> DataValidating - func canPayFeeSpendingAmount( - params: SwapFeeParams, - swapAmount: Decimal?, - locale: Locale - ) -> DataValidating } final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { @@ -56,74 +51,4 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { return quote.assetIn == payChainAssetId && quote.assetOut == receiveChainAssetId }) } - - func canPayFeeSpendingAmount( - params: SwapFeeParams, - swapAmount: Decimal?, - locale: Locale - ) -> DataValidating { - let preparedValues = params.prepare(swapAmount: swapAmount) - - return WarningConditionViolation(onWarning: { [weak self] delegate in - guard let self = self, let view = self.view else { - return - } - let availableToPayString = self.balanceViewModelFactoryFacade.amountFromValue( - targetAssetInfo: params.feeChainAsset.assetDisplayInfo, - value: preparedValues.availableToPay - ).value(for: locale) - let feeString = self.balanceViewModelFactoryFacade.amountFromValue( - targetAssetInfo: params.feeChainAsset.assetDisplayInfo, - value: preparedValues.feeDecimal - ).value(for: locale) - let errorParams: SwapMaxErrorParams - - if preparedValues.toBuyED != 0 { - let diffString = self.balanceViewModelFactoryFacade.amountFromValue( - targetAssetInfo: params.feeChainAsset.assetDisplayInfo, - value: preparedValues.diff - ).value(for: locale) - let edDepositInFeeTokenString = self.balanceViewModelFactoryFacade.amountFromValue( - targetAssetInfo: params.feeChainAsset.assetDisplayInfo, - value: preparedValues.edDepositInFeeTokenDecimal - ).value(for: locale) - let edString = self.balanceViewModelFactoryFacade.amountFromValue( - targetAssetInfo: params.edChainAsset.assetDisplayInfo, - value: preparedValues.edDecimal - ).value(for: locale) - let edToken = params.edChainAsset.asset.symbol - errorParams = .init( - maxSwap: availableToPayString, - fee: feeString, - existentialDeposit: SwapMaxErrorParams.ExistensialDepositErrorParams( - fee: diffString, - value: edString, - token: edToken - ) - ) - } else { - errorParams = .init( - maxSwap: availableToPayString, - fee: feeString, - existentialDeposit: nil - ) - } - - let action = { [preparedValues] in - if preparedValues.availableToPay > 0 { - params.amountUpdateClosure(preparedValues.availableToPay) - delegate.didCompleteWarningHandling() - } - } - - self.presentable.presentSwapAll( - from: view, - errorParams: errorParams, - action: action, - locale: locale - ) - }, preservesCondition: { - preparedValues.feeTokenBalanceDecimal >= preparedValues.swapAmountInFeeToken + preparedValues.feeDecimal + preparedValues.toBuyED - }) - } } diff --git a/novawallet/Modules/Swaps/Validation/SwapFeeParams.swift b/novawallet/Modules/Swaps/Validation/SwapFeeParams.swift deleted file mode 100644 index 4c54284b08..0000000000 --- a/novawallet/Modules/Swaps/Validation/SwapFeeParams.swift +++ /dev/null @@ -1,74 +0,0 @@ -import BigInt - -struct SwapFeeParams { - let fee: BigUInt? - let feeChainAsset: ChainAsset - let feeAssetBalance: AssetBalance? - let edAmount: BigUInt? - let edAmountInFeeToken: BigUInt? - let edChainAsset: ChainAsset - let edChainAssetBalance: AssetBalance? - let payChainAsset: ChainAsset - let amountUpdateClosure: (Decimal) -> Void -} - -extension SwapFeeParams { - func prepare(swapAmount: Decimal?) -> SwapFeeResult { - let params = self - let fee = params.fee ?? 0 - let feeDecimal = Decimal.fromSubstrateAmount( - fee, - precision: Int16(params.feeChainAsset.asset.precision) - ) ?? 0 - let feeTokenBalance = params.feeAssetBalance?.transferable ?? 0 - let feeTokenBalanceDecimal = Decimal.fromSubstrateAmount( - feeTokenBalance, - precision: Int16(params.feeChainAsset.asset.precision) - ) ?? 0 - - let edBalance = params.edAmount ?? 0 - let edDecimal = Decimal.fromSubstrateAmount( - edBalance, - precision: Int16(params.edChainAsset.asset.precision) - ) ?? 0 - let edBalanceTransferrable = params.edChainAssetBalance?.transferable ?? 0 - let edBalanceTransferrableDecimal = Decimal.fromSubstrateAmount( - edBalanceTransferrable, - precision: Int16(params.edChainAsset.asset.precision) - ) ?? 0 - let edDepositInFeeToken = params.edAmountInFeeToken ?? 0 - let edDepositInFeeTokenDecimal = Decimal.fromSubstrateAmount( - edDepositInFeeToken, - precision: Int16(params.feeChainAsset.asset.precision) - ) ?? 0 - - let toBuyED = params.edChainAsset != params.feeChainAsset && edBalanceTransferrableDecimal == 0 ? edDepositInFeeTokenDecimal : 0 - let swapAmount = swapAmount ?? 0 - let swapAmountInFeeToken = params.payChainAsset == params.feeChainAsset ? swapAmount : 0 - let needToPay = swapAmountInFeeToken + feeDecimal + toBuyED - let diff = needToPay - feeTokenBalanceDecimal - let availableToPay = feeTokenBalanceDecimal - diff - - return .init( - availableToPay: availableToPay, - feeDecimal: feeDecimal, - toBuyED: toBuyED, - edDepositInFeeTokenDecimal: edDepositInFeeTokenDecimal, - diff: diff, - edDecimal: edDecimal, - feeTokenBalanceDecimal: feeTokenBalanceDecimal, - swapAmountInFeeToken: swapAmountInFeeToken - ) - } - - struct SwapFeeResult { - let availableToPay: Decimal - let feeDecimal: Decimal - let toBuyED: Decimal - let edDepositInFeeTokenDecimal: Decimal - let diff: Decimal - let edDecimal: Decimal - let feeTokenBalanceDecimal: Decimal - let swapAmountInFeeToken: Decimal - } -} diff --git a/novawallet/Modules/Swaps/Validation/SwapMaxErrorParams.swift b/novawallet/Modules/Swaps/Validation/SwapMaxErrorParams.swift index 4a2fe1e4de..7c1269b75c 100644 --- a/novawallet/Modules/Swaps/Validation/SwapMaxErrorParams.swift +++ b/novawallet/Modules/Swaps/Validation/SwapMaxErrorParams.swift @@ -1,11 +1,11 @@ struct SwapMaxErrorParams { - let maxSwap: String - let fee: String - let existentialDeposit: ExistensialDepositErrorParams? - - struct ExistensialDepositErrorParams { + struct ExistensialDeposit { let fee: String let value: String let token: String } + + let maxSwap: String + let fee: String + let existentialDeposit: ExistensialDeposit? } diff --git a/novawallet/Modules/Swaps/Validation/SwapModel.swift b/novawallet/Modules/Swaps/Validation/SwapModel.swift new file mode 100644 index 0000000000..645aeaadfa --- /dev/null +++ b/novawallet/Modules/Swaps/Validation/SwapModel.swift @@ -0,0 +1,215 @@ +import Foundation +import BigInt + +struct SwapModel { + struct InsufficientDueBalance { + let available: Decimal + } + + struct InsufficientDueNativeFee { + let available: Decimal + let fee: Decimal + } + + struct InsufficientDuePayAssetFee { + let available: Decimal + let feeInPayAsset: Decimal + let minBalanceInPayAsset: Decimal + let minBalanceInNativeAsset: Decimal + } + + enum InsufficientBalanceReason { + case amountToHigh(InsufficientDueBalance) + case feeInNativeAsset(InsufficientDueNativeFee) + case feeInPayAsset(InsufficientDuePayAssetFee) + } + + struct DustAfterSwap { + let dust: Decimal + let minBalance: Decimal + } + + struct DustAfterSwapAndFee { + let dust: Decimal + let minBalance: Decimal + let fee: Decimal + let minBalanceInPayAsset: Decimal + let minBalanceInNativeAsset: Decimal + } + + enum DustReason { + case swap(DustAfterSwap) + case swapAndFee(DustAfterSwapAndFee) + } + + struct CannotReceiveDueExistense { + let minBalance: Decimal + } + + enum CannotReceiveReason { + case existense(CannotReceiveDueExistense) + case noProvider + } + + let payChainAsset: ChainAsset + let receiveChainAsset: ChainAsset + let feeChainAsset: ChainAsset + let spendingAmount: Decimal? + let payAssetBalance: AssetBalance? + let feeAssetBalance: AssetBalance? + let receiveAssetBalance: AssetBalance? + let utilityAssetBalance: AssetBalance? + let payAssetExistense: AssetBalanceExistence? + let receiveAssetExistense: AssetBalanceExistence? + let feeAssetExistense: AssetBalanceExistence? + let utilityAssetExistense: AssetBalanceExistence? + let feeModel: AssetConversion.FeeModel? + let quote: AssetConversion.Quote? + let accountInfo: AccountInfo? + + var spendingAmountInPlank: BigUInt? { + spendingAmount?.toSubstrateAmount(precision: payChainAsset.assetDisplayInfo.assetPrecision) + } + + var payAssetBalanceAfterSwap: BigUInt { + let balance = payAssetBalance?.transferable ?? 0 + let fee = isFeeInPayToken ? (feeModel?.totalFee.targetAmount ?? 0) : 0 + let spendingAmount = spendingAmountInPlank ?? 0 + + let totalSpending = spendingAmount + fee + + return balance > totalSpending ? balance - totalSpending : 0 + } + + var isFeeInPayToken: Bool { + payChainAsset.chainAssetId == feeChainAsset.chainAssetId + } + + func checkBalanceSufficiency() -> InsufficientBalanceReason? { + let balance = payAssetBalance?.transferable ?? 0 + let fee = isFeeInPayToken ? (feeModel?.totalFee.targetAmount ?? 0) : 0 + let swapAmount = spendingAmountInPlank ?? 0 + + let totalSpending = swapAmount + fee + + guard balance < totalSpending else { + return nil + } + + if balance < swapAmount { + return .amountToHigh(.init(available: balance.decimal(precision: payChainAsset.asset.precision))) + } else if payChainAsset.isUtilityAsset { + let available = balance > fee ? balance - fee : 0 + + return .feeInNativeAsset( + .init( + available: available.decimal(precision: payChainAsset.asset.precision), + fee: fee.decimal(precision: feeChainAsset.asset.precision) + ) + ) + } else { + let available = balance > fee ? balance - fee : 0 + + if let addition = feeModel?.networkFeeAddition, let utilityAsset = feeChainAsset.chain.utilityAsset() { + return .feeInPayAsset( + .init( + available: available.decimal(precision: payChainAsset.asset.precision), + feeInPayAsset: fee.decimal(precision: feeChainAsset.asset.precision), + minBalanceInPayAsset: addition.targetAmount.decimal(precision: payChainAsset.asset.precision), + minBalanceInNativeAsset: addition.nativeAmount.decimal(precision: utilityAsset.precision) + ) + ) + } else { + return .feeInNativeAsset( + .init( + available: available.decimal(precision: payChainAsset.asset.precision), + fee: fee.decimal(precision: feeChainAsset.asset.precision) + ) + ) + } + } + } + + var notViolatingExistenseAfterFee: Bool { + guard feeChainAsset.isUtilityAsset else { + return true + } + + let totalBalance = utilityAssetBalance?.totalInPlank ?? 0 + let minBalance = utilityAssetExistense?.minBalance ?? 0 + let fee = feeModel?.totalFee.targetAmount ?? 0 + + return totalBalance >= minBalance + fee + } + + var willKillAccount: Bool { + guard payChainAsset.isUtilityAsset else { + return false + } + + let balance = payAssetBalanceAfterSwap + let minBalance = utilityAssetExistense?.minBalance ?? 0 + + return balance < minBalance + } + + var notViolatingConsumers: Bool { + guard willKillAccount else { + return false + } + + return (accountInfo?.consumers ?? 0) > 0 + } + + func checkCanReceive() -> CannotReceiveReason? { + let isSelfSufficient = receiveAssetExistense?.isSelfSufficient ?? false + let amountAfterSwap = (receiveAssetBalance?.totalInPlank ?? 0) + (quote?.amountOut ?? 0) + let feeInReceiveAsset = feeChainAsset.chainAssetId == receiveChainAsset.chainAssetId ? + (feeModel?.totalFee.targetAmount ?? 0) : 0 + let minBalance = receiveAssetExistense?.minBalance ?? 0 + + if amountAfterSwap < minBalance + feeInReceiveAsset { + return .existense( + .init(minBalance: minBalance.decimal(precision: receiveChainAsset.asset.precision)) + ) + } else if !isSelfSufficient && willKillAccount { + return .noProvider + } else { + return nil + } + } + + func checkDustAfterSwap() -> DustReason? { + let balance = payAssetBalanceAfterSwap + let minBalance = payAssetExistense?.minBalance ?? 0 + + guard balance == 0 || balance >= minBalance else { + return nil + } + + let remaning = minBalance - balance + + if + !isFeeInPayToken, !payChainAsset.isUtilityAsset, + let networkFee = feeModel?.networkFee, + let feeAdditions = feeModel?.networkFeeAddition, + let utilityAsset = feeChainAsset.chain.utilityAsset() { + return .swapAndFee( + .init( + dust: remaning.decimal(precision: payChainAsset.asset.precision), + minBalance: minBalance.decimal(precision: payChainAsset.asset.precision), + fee: networkFee.targetAmount.decimal(precision: payChainAsset.asset.precision), + minBalanceInPayAsset: feeAdditions.targetAmount.decimal(precision: payChainAsset.asset.precision), + minBalanceInNativeAsset: feeAdditions.nativeAmount.decimal(precision: utilityAsset.precision) + ) + ) + } else { + return .swap( + .init( + dust: remaning.decimal(precision: payChainAsset.asset.precision), + minBalance: minBalance.decimal(precision: payChainAsset.asset.precision) + ) + ) + } + } +} From 05916b3e243dc0e506d63d4f392659b9717c11a2 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sun, 5 Nov 2023 08:06:29 +0100 Subject: [PATCH 120/204] add can receive validations --- novawallet.xcodeproj/project.pbxproj | 8 +- .../Validation/SwapDataValidatorFactory.swift | 205 ++++++++++++++++++ .../Validation/SwapErrorPresentable.swift | 138 ++++++++++-- .../SwapErrorPresentableParams.swift | 38 ++++ .../Swaps/Validation/SwapMaxErrorParams.swift | 11 - .../Modules/Swaps/Validation/SwapModel.swift | 51 +++-- novawallet/en.lproj/Localizable.strings | 13 +- novawallet/ru.lproj/Localizable.strings | 13 +- 8 files changed, 418 insertions(+), 59 deletions(-) create mode 100644 novawallet/Modules/Swaps/Validation/SwapErrorPresentableParams.swift delete mode 100644 novawallet/Modules/Swaps/Validation/SwapMaxErrorParams.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 78493bc417..39d05fdeaa 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -68,7 +68,7 @@ 0C13D3212A822B220054BB6F /* StartStakingDirectConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3202A822B220054BB6F /* StartStakingDirectConfirmWireframe.swift */; }; 0C13D3242A823D810054BB6F /* StartStakingExtrinsicProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3232A823D810054BB6F /* StartStakingExtrinsicProxy.swift */; }; 0C13D3262A8275400054BB6F /* StartStakingFeeIdFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3252A82753F0054BB6F /* StartStakingFeeIdFactory.swift */; }; - 0C13DFC92AF4FFC200E5F355 /* SwapMaxErrorParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFC82AF4FFC200E5F355 /* SwapMaxErrorParams.swift */; }; + 0C13DFC92AF4FFC200E5F355 /* SwapErrorPresentableParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFC82AF4FFC200E5F355 /* SwapErrorPresentableParams.swift */; }; 0C13DFCB2AF6182500E5F355 /* SwapModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFCA2AF6182500E5F355 /* SwapModel.swift */; }; 0C154A0E2A45995500932C3F /* CompoundComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C154A0D2A45995500932C3F /* CompoundComparator.swift */; }; 0C17BD972A42F162004AF9E7 /* WalletViewModelObserverContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */; }; @@ -4123,7 +4123,7 @@ 0C13D3202A822B220054BB6F /* StartStakingDirectConfirmWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingDirectConfirmWireframe.swift; sourceTree = ""; }; 0C13D3232A823D810054BB6F /* StartStakingExtrinsicProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingExtrinsicProxy.swift; sourceTree = ""; }; 0C13D3252A82753F0054BB6F /* StartStakingFeeIdFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingFeeIdFactory.swift; sourceTree = ""; }; - 0C13DFC82AF4FFC200E5F355 /* SwapMaxErrorParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapMaxErrorParams.swift; sourceTree = ""; }; + 0C13DFC82AF4FFC200E5F355 /* SwapErrorPresentableParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapErrorPresentableParams.swift; sourceTree = ""; }; 0C13DFCA2AF6182500E5F355 /* SwapModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapModel.swift; sourceTree = ""; }; 0C154A0D2A45995500932C3F /* CompoundComparator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompoundComparator.swift; sourceTree = ""; }; 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletViewModelObserverContainer.swift; sourceTree = ""; }; @@ -9651,7 +9651,7 @@ children = ( 7719018B2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift */, 7719018D2AE0E71F00D9C918 /* SwapErrorPresentable.swift */, - 0C13DFC82AF4FFC200E5F355 /* SwapMaxErrorParams.swift */, + 0C13DFC82AF4FFC200E5F355 /* SwapErrorPresentableParams.swift */, 0C13DFCA2AF6182500E5F355 /* SwapModel.swift */, ); path = Validation; @@ -20257,7 +20257,7 @@ 84E2ABC82992724600A5D3C1 /* GovernanceDelegatorAction.swift in Sources */, 8463A70325E2FCD0003B8160 /* WeakWrapper.swift in Sources */, 8459A9CA2746A1BC000D6278 /* CrowdloanOffchainSubscriber.swift in Sources */, - 0C13DFC92AF4FFC200E5F355 /* SwapMaxErrorParams.swift in Sources */, + 0C13DFC92AF4FFC200E5F355 /* SwapErrorPresentableParams.swift in Sources */, 8401620B25E144D50087A5F3 /* AmountInputAccessoryView.swift in Sources */, 88F19DE028D8D0F600F6E459 /* LoadableViewModelState+Addition.swift in Sources */, 844C3E652A07627E00C4305F /* DAppWalletAuthViewModel.swift in Sources */, diff --git a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift index 7d20d6eb7c..f31dc2846f 100644 --- a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift +++ b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift @@ -3,6 +3,20 @@ import BigInt import SoraFoundation protocol SwapDataValidatorFactoryProtocol: BaseDataValidatingFactoryProtocol { + func hasSufficientBalance( + params: SwapModel, + swapMaxAction: @escaping () -> Void, + locale: Locale + ) -> DataValidating + + func canReceive(params: SwapModel, locale: Locale) -> DataValidating + + func noDustRemains( + params: SwapModel, + swapMaxAction: @escaping () -> Void, + locale: Locale + ) -> DataValidating + func has( quote: AssetConversion.Quote?, payChainAssetId: ChainAssetId?, @@ -28,6 +42,197 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { self.balanceViewModelFactoryFacade = balanceViewModelFactoryFacade } + // swiftlint:disable:next function_body_length + func hasSufficientBalance( + params: SwapModel, + swapMaxAction: @escaping () -> Void, + locale: Locale + ) -> DataValidating { + let insufficientReason = params.checkBalanceSufficiency() + + return ErrorConditionViolation(onError: { [weak self] in + guard + let view = self?.view, + let reason = insufficientReason, + let viewModelFactory = self?.balanceViewModelFactoryFacade else { + return + } + + switch reason { + case .amountToHigh: + self?.presentable.presentAmountTooHigh(from: view, locale: locale) + case let .feeInNativeAsset(model): + let params = SwapDisplayError.InsufficientBalanceDueFeeNativeAsset( + available: viewModelFactory.amountFromValue( + targetAssetInfo: params.payChainAsset.assetDisplayInfo, + value: model.available + ).value(for: locale), + fee: viewModelFactory.amountFromValue( + targetAssetInfo: params.feeChainAsset.assetDisplayInfo, + value: model.fee + ).value(for: locale) + ) + + self?.presentable.presentInsufficientBalance( + from: view, + reason: .dueFeeNativeAsset(params), + action: swapMaxAction, + locale: locale + ) + case let .feeInPayAsset(model): + let utilityChainAsset = params.utilityChainAsset ?? params.feeChainAsset + + let params = SwapDisplayError.InsufficientBalanceDueFeePayAsset( + available: viewModelFactory.amountFromValue( + targetAssetInfo: params.payChainAsset.assetDisplayInfo, + value: model.available + ).value(for: locale), + fee: viewModelFactory.amountFromValue( + targetAssetInfo: params.feeChainAsset.assetDisplayInfo, + value: model.feeInPayAsset + ).value(for: locale), + minBalanceInPayAsset: viewModelFactory.amountFromValue( + targetAssetInfo: params.payChainAsset.assetDisplayInfo, + value: model.minBalanceInPayAsset + ).value(for: locale), + minBalanceInUtilityAsset: viewModelFactory.amountFromValue( + targetAssetInfo: utilityChainAsset.assetDisplayInfo, + value: model.minBalanceInNativeAsset + ).value(for: locale), + tokenSymbol: utilityChainAsset.asset.symbol + ) + + self?.presentable.presentInsufficientBalance( + from: view, + reason: .dueFeePayAsset(params), + action: swapMaxAction, + locale: locale + ) + } + + self?.presentable.presentNotEnoughLiquidity(from: view, locale: locale) + }, preservesCondition: { + insufficientReason == nil + }) + } + + func canReceive(params: SwapModel, locale: Locale) -> DataValidating { + let cantReceiveReason = params.checkCanReceive() + + return ErrorConditionViolation(onError: { [weak self] in + guard + let view = self?.view, + let reason = cantReceiveReason, + let viewModelFactory = self?.balanceViewModelFactoryFacade else { + return + } + + switch reason { + case let .existense(model): + self?.presentable.presentMinBalanceViolatedToReceive( + from: view, + minBalance: viewModelFactory.amountFromValue( + targetAssetInfo: params.receiveChainAsset.assetDisplayInfo, + value: model.minBalance + ).value(for: locale), + locale: locale + ) + case let .noProvider(model): + let utilityChainAsset = params.utilityChainAsset ?? params.feeChainAsset + + self?.presentable.presentNoProviderForNonSufficientToken( + from: view, + utilityMinBalance: viewModelFactory.amountFromValue( + targetAssetInfo: utilityChainAsset.assetDisplayInfo, + value: model.minBalance + ).value(for: locale), + token: params.receiveChainAsset.asset.symbol, + locale: locale + ) + } + + }, preservesCondition: { + cantReceiveReason == nil + }) + } + + // swiftlint:disable:next function_body_length + func noDustRemains( + params: SwapModel, + swapMaxAction: @escaping () -> Void, + locale: Locale + ) -> DataValidating { + let dustReason = params.checkDustAfterSwap() + + return WarningConditionViolation(onWarning: { [weak self] delegate in + guard + let view = self?.view, + let viewModelFactory = self?.balanceViewModelFactoryFacade, + let reason = dustReason else { + return + } + + let errorReason: SwapDisplayError.DustRemains + + switch reason { + case let .swap(model): + let params = SwapDisplayError.DustRemainsDueNativeSwap( + remaining: viewModelFactory.amountFromValue( + targetAssetInfo: params.payChainAsset.assetDisplayInfo, + value: model.dust + ).value(for: locale), + minBalance: viewModelFactory.amountFromValue( + targetAssetInfo: params.payChainAsset.assetDisplayInfo, + value: model.minBalance + ).value(for: locale) + ) + + errorReason = .dueNativeSwap(params) + case let .swapAndFee(model): + let utilityChainAsset = params.utilityChainAsset ?? params.feeChainAsset + + let params = SwapDisplayError.DustRemainsDueFeeSwap( + remaining: viewModelFactory.amountFromValue( + targetAssetInfo: params.payChainAsset.assetDisplayInfo, + value: model.dust + ).value(for: locale), + minBalanceOfPayAsset: viewModelFactory.amountFromValue( + targetAssetInfo: params.payChainAsset.assetDisplayInfo, + value: model.minBalance + ).value(for: locale), + fee: viewModelFactory.amountFromValue( + targetAssetInfo: params.feeChainAsset.assetDisplayInfo, + value: model.fee + ).value(for: locale), + minBalanceInPayAsset: viewModelFactory.amountFromValue( + targetAssetInfo: params.payChainAsset.assetDisplayInfo, + value: model.minBalanceInPayAsset + ).value(for: locale), + minBalanceInUtilityAsset: viewModelFactory.amountFromValue( + targetAssetInfo: utilityChainAsset.assetDisplayInfo, + value: model.minBalanceInNativeAsset + ).value(for: locale), + utilitySymbol: utilityChainAsset.asset.symbol + ) + + errorReason = .dueFeeSwap(params) + } + + self?.presentable.presentDustRemains( + from: view, + reason: errorReason, + swapMaxAction: swapMaxAction, + proceedAction: { + delegate.didCompleteWarningHandling() + }, + locale: locale + ) + + }, preservesCondition: { + dustReason == nil + }) + } + func has( quote: AssetConversion.Quote?, payChainAssetId: ChainAssetId?, diff --git a/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift b/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift index 7bb25a2073..e5f7706f40 100644 --- a/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift +++ b/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift @@ -2,12 +2,34 @@ import Foundation protocol SwapErrorPresentable: BaseErrorPresentable { func presentNotEnoughLiquidity(from view: ControllerBackedProtocol, locale: Locale?) - func presentSwapAll( + + func presentInsufficientBalance( from view: ControllerBackedProtocol?, - errorParams: SwapMaxErrorParams, + reason: SwapDisplayError.InsufficientBalance, action: @escaping () -> Void, locale: Locale ) + + func presentDustRemains( + from view: ControllerBackedProtocol?, + reason: SwapDisplayError.DustRemains, + swapMaxAction: @escaping () -> Void, + proceedAction: @escaping () -> Void, + locale: Locale + ) + + func presentNoProviderForNonSufficientToken( + from view: ControllerBackedProtocol, + utilityMinBalance: String, + token: String, + locale: Locale + ) + + func presentMinBalanceViolatedToReceive( + from view: ControllerBackedProtocol, + minBalance: String, + locale: Locale + ) } extension SwapErrorPresentable where Self: AlertPresentable & ErrorPresentable { @@ -20,28 +42,61 @@ extension SwapErrorPresentable where Self: AlertPresentable & ErrorPresentable { present(message: nil, title: title, closeAction: closeAction, from: view) } - func presentSwapAll( + func presentNoProviderForNonSufficientToken( + from view: ControllerBackedProtocol, + utilityMinBalance: String, + token: String, + locale: Locale + ) { + let title = R.string.localizable.commonErrorGeneralTitle(preferredLanguages: locale.rLanguages) + let message = R.string.localizable.commonReceiveNotSufficientNativeAssetError( + utilityMinBalance, + token, + preferredLanguages: locale.rLanguages + ) + let closeAction = R.string.localizable.commonClose(preferredLanguages: locale.rLanguages) + + present(message: message, title: title, closeAction: closeAction, from: view) + } + + func presentMinBalanceViolatedToReceive( + from view: ControllerBackedProtocol, + minBalance: String, + locale: Locale + ) { + let title = R.string.localizable.commonErrorGeneralTitle(preferredLanguages: locale.rLanguages) + let message = R.string.localizable.commonReceiveAtLeastEdError( + minBalance, + preferredLanguages: locale.rLanguages + ) + let closeAction = R.string.localizable.commonClose(preferredLanguages: locale.rLanguages) + + present(message: message, title: title, closeAction: closeAction, from: view) + } + + func presentInsufficientBalance( from view: ControllerBackedProtocol?, - errorParams: SwapMaxErrorParams, + reason: SwapDisplayError.InsufficientBalance, action: @escaping () -> Void, locale: Locale ) { let title = R.string.localizable.commonInsufficientBalance(preferredLanguages: locale.rLanguages) let message: String - if let edError = errorParams.existentialDeposit { - message = R.string.localizable.swapsSetupErrorInsufficientBalanceEdMessage( - errorParams.maxSwap, - errorParams.fee, - edError.fee, - edError.value, - edError.token, + switch reason { + case let .dueFeePayAsset(value): + message = R.string.localizable.swapsSetupErrorInsufficientBalanceFeeSwapMessage( + value.available, + value.fee, + value.minBalanceInPayAsset, + value.minBalanceInUtilityAsset, + value.tokenSymbol, preferredLanguages: locale.rLanguages ) - } else { - message = R.string.localizable.swapsSetupErrorInsufficientBalanceMessage( - errorParams.maxSwap, - errorParams.fee, + case let .dueFeeNativeAsset(value): + message = R.string.localizable.swapsSetupErrorInsufficientBalanceFeeNativeMessage( + value.available, + value.fee, preferredLanguages: locale.rLanguages ) } @@ -51,9 +106,7 @@ extension SwapErrorPresentable where Self: AlertPresentable & ErrorPresentable { ) let swapAllAction = AlertPresentableAction( - title: R.string.localizable.swapsSetupErrorInsufficientBalanceAction( - preferredLanguages: locale.rLanguages - ), + title: R.string.localizable.commonSwapMax(preferredLanguages: locale.rLanguages), handler: action ) @@ -66,4 +119,53 @@ extension SwapErrorPresentable where Self: AlertPresentable & ErrorPresentable { present(viewModel: viewModel, style: .alert, from: view) } + + func presentDustRemains( + from view: ControllerBackedProtocol?, + reason: SwapDisplayError.DustRemains, + swapMaxAction: @escaping () -> Void, + proceedAction: @escaping () -> Void, + locale: Locale + ) { + let title = R.string.localizable.commonDustRemainsTitle(preferredLanguages: locale.rLanguages) + let message: String + + switch reason { + case let .dueFeeSwap(value): + message = R.string.localizable.swapsDustRemainsFeePayAssetMessage( + value.minBalanceOfPayAsset, + value.fee, + value.minBalanceInPayAsset, + value.minBalanceInUtilityAsset, + value.utilitySymbol, + value.remaining, + preferredLanguages: locale.rLanguages + ) + case let .dueNativeSwap(value): + message = R.string.localizable.swapsDustRemainsFeeNativeAssetMessage( + value.minBalance, + value.remaining, + preferredLanguages: locale.rLanguages + ) + } + + let proceedAction = AlertPresentableAction( + title: R.string.localizable.commonProceed(preferredLanguages: locale.rLanguages), + handler: proceedAction + ) + + let swapAllAction = AlertPresentableAction( + title: R.string.localizable.commonSwapMax(preferredLanguages: locale.rLanguages), + handler: swapMaxAction + ) + + let viewModel = AlertPresentableViewModel( + title: title, + message: message, + actions: [proceedAction, swapAllAction], + closeAction: nil + ) + + present(viewModel: viewModel, style: .alert, from: view) + } } diff --git a/novawallet/Modules/Swaps/Validation/SwapErrorPresentableParams.swift b/novawallet/Modules/Swaps/Validation/SwapErrorPresentableParams.swift new file mode 100644 index 0000000000..a5702e7e96 --- /dev/null +++ b/novawallet/Modules/Swaps/Validation/SwapErrorPresentableParams.swift @@ -0,0 +1,38 @@ +enum SwapDisplayError { + struct InsufficientBalanceDueFeePayAsset { + let available: String + let fee: String + let minBalanceInPayAsset: String + let minBalanceInUtilityAsset: String + let tokenSymbol: String + } + + struct InsufficientBalanceDueFeeNativeAsset { + let available: String + let fee: String + } + + enum InsufficientBalance { + case dueFeePayAsset(InsufficientBalanceDueFeePayAsset) + case dueFeeNativeAsset(InsufficientBalanceDueFeeNativeAsset) + } + + struct DustRemainsDueNativeSwap { + let remaining: String + let minBalance: String + } + + struct DustRemainsDueFeeSwap { + let remaining: String + let minBalanceOfPayAsset: String + let fee: String + let minBalanceInPayAsset: String + let minBalanceInUtilityAsset: String + let utilitySymbol: String + } + + enum DustRemains { + case dueNativeSwap(DustRemainsDueNativeSwap) + case dueFeeSwap(DustRemainsDueFeeSwap) + } +} diff --git a/novawallet/Modules/Swaps/Validation/SwapMaxErrorParams.swift b/novawallet/Modules/Swaps/Validation/SwapMaxErrorParams.swift deleted file mode 100644 index 7c1269b75c..0000000000 --- a/novawallet/Modules/Swaps/Validation/SwapMaxErrorParams.swift +++ /dev/null @@ -1,11 +0,0 @@ -struct SwapMaxErrorParams { - struct ExistensialDeposit { - let fee: String - let value: String - let token: String - } - - let maxSwap: String - let fee: String - let existentialDeposit: ExistensialDeposit? -} diff --git a/novawallet/Modules/Swaps/Validation/SwapModel.swift b/novawallet/Modules/Swaps/Validation/SwapModel.swift index 645aeaadfa..6347ecfa4b 100644 --- a/novawallet/Modules/Swaps/Validation/SwapModel.swift +++ b/novawallet/Modules/Swaps/Validation/SwapModel.swift @@ -23,12 +23,12 @@ struct SwapModel { case feeInNativeAsset(InsufficientDueNativeFee) case feeInPayAsset(InsufficientDuePayAssetFee) } - + struct DustAfterSwap { let dust: Decimal let minBalance: Decimal } - + struct DustAfterSwapAndFee { let dust: Decimal let minBalance: Decimal @@ -36,19 +36,23 @@ struct SwapModel { let minBalanceInPayAsset: Decimal let minBalanceInNativeAsset: Decimal } - + enum DustReason { case swap(DustAfterSwap) case swapAndFee(DustAfterSwapAndFee) } - + struct CannotReceiveDueExistense { let minBalance: Decimal } - + + struct CannotReceiveDueNoProviders { + let minBalance: Decimal + } + enum CannotReceiveReason { case existense(CannotReceiveDueExistense) - case noProvider + case noProvider(CannotReceiveDueNoProviders) } let payChainAsset: ChainAsset @@ -67,6 +71,10 @@ struct SwapModel { let quote: AssetConversion.Quote? let accountInfo: AccountInfo? + var utilityChainAsset: ChainAsset? { + feeChainAsset.chain.utilityChainAsset() + } + var spendingAmountInPlank: BigUInt? { spendingAmount?.toSubstrateAmount(precision: payChainAsset.assetDisplayInfo.assetPrecision) } @@ -110,7 +118,10 @@ struct SwapModel { } else { let available = balance > fee ? balance - fee : 0 - if let addition = feeModel?.networkFeeAddition, let utilityAsset = feeChainAsset.chain.utilityAsset() { + if + isFeeInPayToken, + let addition = feeModel?.networkFeeAddition, + let utilityAsset = feeChainAsset.chain.utilityAsset() { return .feeInPayAsset( .init( available: available.decimal(precision: payChainAsset.asset.precision), @@ -141,15 +152,15 @@ struct SwapModel { return totalBalance >= minBalance + fee } - + var willKillAccount: Bool { guard payChainAsset.isUtilityAsset else { return false } - + let balance = payAssetBalanceAfterSwap let minBalance = utilityAssetExistense?.minBalance ?? 0 - + return balance < minBalance } @@ -157,10 +168,10 @@ struct SwapModel { guard willKillAccount else { return false } - + return (accountInfo?.consumers ?? 0) > 0 } - + func checkCanReceive() -> CannotReceiveReason? { let isSelfSufficient = receiveAssetExistense?.isSelfSufficient ?? false let amountAfterSwap = (receiveAssetBalance?.totalInPlank ?? 0) + (quote?.amountOut ?? 0) @@ -172,8 +183,12 @@ struct SwapModel { return .existense( .init(minBalance: minBalance.decimal(precision: receiveChainAsset.asset.precision)) ) - } else if !isSelfSufficient && willKillAccount { - return .noProvider + } else if !isSelfSufficient, willKillAccount { + let utilityMinBalance = utilityAssetExistense?.minBalance ?? 0 + let precision = (utilityChainAsset ?? feeChainAsset).asset.precision + return .noProvider( + .init(minBalance: utilityMinBalance.decimal(precision: precision)) + ) } else { return nil } @@ -183,14 +198,14 @@ struct SwapModel { let balance = payAssetBalanceAfterSwap let minBalance = payAssetExistense?.minBalance ?? 0 - guard balance == 0 || balance >= minBalance else { + guard balance > 0, balance < minBalance else { return nil } - + let remaning = minBalance - balance - + if - !isFeeInPayToken, !payChainAsset.isUtilityAsset, + isFeeInPayToken, !payChainAsset.isUtilityAsset, let networkFee = feeModel?.networkFee, let feeAdditions = feeModel?.networkFeeAddition, let utilityAsset = feeChainAsset.chain.utilityAsset() { diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 599cc5bfe0..0017a917fd 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1399,9 +1399,9 @@ "swaps.rate.description" = "Exchange rate between two different cryptocurrencies. It represents how much of one cryptocurrency you can get in exchange for a certain amount of another cryptocurrency."; "swaps.network.fee.description" = "A network fees charged by the blockchain to process and validate any transactions. May vary depending on network conditions or transaction speed."; "swaps.setup.error.not.enough.liquidity.title" = "Pool doesn’t have enough liquidity to swap"; -"swaps.setup.error.insufficient.balance.ed.message" = "You can swap up to %@ since you need to pay %@ for network fee and also convert %@ to %@ to meet %@ minimum balance."; -"swaps.setup.error.insufficient.balance.message" = "You can swap up to %@ since you need to pay %@ for network fee."; -"swaps.setup.error.insufficient.balance.action" = "Swap max"; +"swaps.setup.error.insufficient.balance.fee.swap.message" = "You can swap up to %@ since you need to pay %@ for network fee and also convert %@ to %@ to meet %@ minimum balance."; +"swaps.setup.error.insufficient.balance.fee.native.message" = "You can swap up to %@ since you need to pay %@ for network fee."; +"common.swap.max" = "Swap max"; "common.alert.external.link.disclaimer.title" = "Continue in browser?"; "common.alert.external.link.disclaimer.message" = "To continue the purchase you will be redirected from Nova Wallet app to %@"; "polkadot.staking.promotion.title" = "Boost your DOT 🚀"; @@ -1410,9 +1410,14 @@ "swaps.setup.network.fee.token.hint" = "Network fee is added on top of entered amount"; "swaps.setup.price.difference.description" = "Price difference refers to the difference in price between two different assets. When making a swap in crypto, the price difference is usually the difference between the price of the asset you are swapping for and the price of the asset you are swapping with."; "swaps.setup.error.rate.was.updated.title" = "Swap rate was updated"; -"swaps.setup.error.rate.was.updated.message" = "Old rate: %@.\nNew rate:%@"; +"swaps.setup.error.rate.was.updated.message" = "Old rate: %@.\nNew rate: %@"; "swaps.setup.deposit.by.cross.chain.transfer.title" = "Cross-chain transfer"; "swaps.setup.deposit.by.cross.chain.transfer.subtitle" = "Transfer %@ from another network"; "swaps.setup.deposit.by.receive.subtitle" = "Receive %@ with QR or your address"; "swaps.setup.deposit.by.buy.subtitle" = "Instantly buy %@ with a credit card"; "swaps.setup.deposit.title" = "Get %@ using"; +"common.dust.remains.title" = "Too small amount remains on your balance"; +"swaps.dust.remains.fee.native.asset.message" = "You should leave at least %@ on your balance. Do you want to perform full swap by adding remaining %@ as well?"; +"swaps.dust.remains.fee.pay.asset.message" = "You should keep at least %@ after paying %@ network fee and converting %@ to %@ to meet %@ minimum balance.\n\nDo you want to fully swap by adding remaining %@ as well?"; +"common.receive.at.least.ed.error" = "You can’t receive less than %@"; +"common.receive.not.sufficient.native.asset.error" = "You must keep at least %@ to receive %@ token"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index e8257a9f77..7ae5b1c4ad 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1399,9 +1399,9 @@ "swaps.rate.description" = "Обменный курс между двумя различными криптовалютами. Он представляет, сколько одной криптовалюты вы можете получить в обмен на определенное количество другой криптовалюты."; "swaps.network.fee.description" = "Это комиссия сети, взимаемая блокчейном за обработку и подтверждение любых транзакций. Она может изменяться в зависимости от условий в сети или скорости выполнения транзакции."; "swaps.setup.error.not.enough.liquidity.title" = "В пуле недостаточно ликвидности для обмена"; -"swaps.setup.error.insufficient.balance.ed.message" = "You can swap up to %@ since you need to pay %@ for network fee and also convert %@ to %@ to meet %@ minimum balance."; -"swaps.setup.error.insufficient.balance.message" = "You can swap up to %@ since you need to pay %@ for network fee."; -"swaps.setup.error.insufficient.balance.action" = "Swap max"; +"swaps.setup.error.insufficient.balance.fee.swap.message" = "You can swap up to %@ since you need to pay %@ for network fee and also convert %@ to %@ to meet %@ minimum balance."; +"swaps.setup.error.insufficient.balance.fee.native.message" = "You can swap up to %@ since you need to pay %@ for network fee."; +"common.swap.max" = "Swap max"; "common.alert.external.link.disclaimer.title" = "Продолжить в браузере?"; "common.alert.external.link.disclaimer.message" = "Для продолжения покупки вы будете перенаправлены из приложения Nova Wallet на сайт %@"; "polkadot.staking.promotion.title" = "Максимизируйте\nнаграды от DOT 🚀"; @@ -1410,9 +1410,14 @@ "swaps.setup.network.fee.token.hint" = "Комиссия сети добавляется к введенной сумме."; "swaps.setup.price.difference.description" = "Разница в цене относится к разнице в цене между двумя различными активами. При совершении обмена в криптовалюте разница в цене обычно представляет собой разницу между ценой актива, на который вы меняете, и ценой актива, на который вы меняетесь."; "swaps.setup.error.rate.was.updated.title" = "Обменный курс был обновлен"; -"swaps.setup.error.rate.was.updated.message" = "Было: %@.\nСтало:%@"; +"swaps.setup.error.rate.was.updated.message" = "Было: %@.\nСтало: %@"; "swaps.setup.deposit.by.cross.chain.transfer.title" = "Перевод между сетями"; "swaps.setup.deposit.by.cross.chain.transfer.subtitle" = "Перевести %@ из другой сети"; "swaps.setup.deposit.by.receive.subtitle" = "Получить %@ используя QR-код или адрес"; "swaps.setup.deposit.by.buy.subtitle" = "Купить %@ используя банковскую карту"; "swaps.setup.deposit.title" = "Пополнить %@"; +"common.dust.remains.title" = "Баланс ниже минимального"; +"swaps.dust.remains.fee.native.asset.message" = "Вам необходимо оставить минимум %@ на вашем балансе. Вы хотите добавить к обмену оставшиеся %@ тоже?"; +"swaps.dust.remains.fee.pay.asset.message" = "Вам необходимо оставить минимум %@ после оплаты %@ комиссии сети и обмена %@ на %@ для поддержания минимального баланса %@.\n\nВы хотите добавить к обмену оставшиеся %@ тоже?"; +"common.receive.at.least.ed.error" = "Вы не можете получить меньше чем %@"; +"common.receive.not.sufficient.native.asset.error" = "У вас должно быть минимум %@ для получения %@ токена"; From 744506fb3c60f24488e739ed41ca09ee1260c296 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 6 Nov 2023 07:18:14 +0100 Subject: [PATCH 121/204] swap max logic and base presenter --- novawallet.xcodeproj/project.pbxproj | 12 ++ .../Foundation/BigUInt+Operation.swift | 8 + .../Common/Substrate/Types/AccountInfo.swift | 6 +- .../Swaps/Base/Model/SwapMaxModel.swift | 67 +++++++++ .../Swaps/Base/SwapBaseInteractor.swift | 12 +- .../Swaps/Base/SwapBasePresenter.swift | 138 ++++++++++++++++++ .../Swaps/Base/SwapBaseProtocols.swift | 4 +- .../Swaps/Confirm/SwapConfirmProtocols.swift | 3 +- .../Swaps/Setup/SwapSetupProtocols.swift | 3 +- .../Validation/SwapDataValidatorFactory.swift | 20 +++ .../Validation/SwapErrorPresentable.swift | 6 + .../SwapErrorPresentableParams.swift | 6 + .../Modules/Swaps/Validation/SwapModel.swift | 48 ++++-- novawallet/en.lproj/Localizable.strings | 1 + novawallet/ru.lproj/Localizable.strings | 1 + 15 files changed, 311 insertions(+), 24 deletions(-) create mode 100644 novawallet/Common/Extension/Foundation/BigUInt+Operation.swift create mode 100644 novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift create mode 100644 novawallet/Modules/Swaps/Base/SwapBasePresenter.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 39d05fdeaa..8f0b9c6de8 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -70,6 +70,9 @@ 0C13D3262A8275400054BB6F /* StartStakingFeeIdFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3252A82753F0054BB6F /* StartStakingFeeIdFactory.swift */; }; 0C13DFC92AF4FFC200E5F355 /* SwapErrorPresentableParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFC82AF4FFC200E5F355 /* SwapErrorPresentableParams.swift */; }; 0C13DFCB2AF6182500E5F355 /* SwapModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFCA2AF6182500E5F355 /* SwapModel.swift */; }; + 0C13DFCD2AF8A5A300E5F355 /* SwapMaxModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFCC2AF8A5A300E5F355 /* SwapMaxModel.swift */; }; + 0C13DFCF2AF8ADB300E5F355 /* BigUInt+Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFCE2AF8ADB300E5F355 /* BigUInt+Operation.swift */; }; + 0C13DFD12AF8AE3E00E5F355 /* SwapBasePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFD02AF8AE3E00E5F355 /* SwapBasePresenter.swift */; }; 0C154A0E2A45995500932C3F /* CompoundComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C154A0D2A45995500932C3F /* CompoundComparator.swift */; }; 0C17BD972A42F162004AF9E7 /* WalletViewModelObserverContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */; }; 0C17BD992A42F1BE004AF9E7 /* MoneyPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */; }; @@ -4125,6 +4128,9 @@ 0C13D3252A82753F0054BB6F /* StartStakingFeeIdFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingFeeIdFactory.swift; sourceTree = ""; }; 0C13DFC82AF4FFC200E5F355 /* SwapErrorPresentableParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapErrorPresentableParams.swift; sourceTree = ""; }; 0C13DFCA2AF6182500E5F355 /* SwapModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapModel.swift; sourceTree = ""; }; + 0C13DFCC2AF8A5A300E5F355 /* SwapMaxModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapMaxModel.swift; sourceTree = ""; }; + 0C13DFCE2AF8ADB300E5F355 /* BigUInt+Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BigUInt+Operation.swift"; sourceTree = ""; }; + 0C13DFD02AF8AE3E00E5F355 /* SwapBasePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapBasePresenter.swift; sourceTree = ""; }; 0C154A0D2A45995500932C3F /* CompoundComparator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompoundComparator.swift; sourceTree = ""; }; 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletViewModelObserverContainer.swift; sourceTree = ""; }; 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyPresentable.swift; sourceTree = ""; }; @@ -8462,6 +8468,7 @@ isa = PBXGroup; children = ( 0C2FDF182AEB82AA006A6C59 /* AssetHubFeeModelBuilder.swift */, + 0C13DFCC2AF8A5A300E5F355 /* SwapMaxModel.swift */, ); path = Model; sourceTree = ""; @@ -9675,6 +9682,7 @@ 771901B12AEA3E5100D9C918 /* ShortTextInfoPresentableExtensions.swift */, 77E304AA2AEBB214006FD6F0 /* SlippageBounds.swift */, 77E304AC2AEC16B2006FD6F0 /* SwapPriceDifferenceViewModelFactoryProtocol.swift */, + 0C13DFD02AF8AE3E00E5F355 /* SwapBasePresenter.swift */, ); path = Base; sourceTree = ""; @@ -13648,6 +13656,7 @@ 77E0DC9D2A6940C400D03724 /* Calendar+Helpers.swift */, 0C7C9B982ABFF355009A0362 /* String+Html.swift */, 0C7C9B9A2AC16D7B009A0362 /* NSAttributedString+Helpers.swift */, + 0C13DFCE2AF8ADB300E5F355 /* BigUInt+Operation.swift */, ); path = Foundation; sourceTree = ""; @@ -19829,6 +19838,7 @@ 84715786291136B100D7D003 /* GovernanceUnlocksViewModel.swift in Sources */, 84DD5F64263DFAB700425ACF /* ErrorConditionViolation.swift in Sources */, 0C2FDF192AEB82AA006A6C59 /* AssetHubFeeModelBuilder.swift in Sources */, + 0C13DFCD2AF8A5A300E5F355 /* SwapMaxModel.swift in Sources */, 8428768324AE046300D91AD8 /* LanguageSelectionViewController.swift in Sources */, F452D8CA273E58D5008F7295 /* SettingsTableFooterView.swift in Sources */, 8470D6D2253E3382009E9A5D /* StorageSubscriptionContainer.swift in Sources */, @@ -21033,6 +21043,7 @@ 8472974D260A9CDF009B86D0 /* SelectValidatorsConfirmationModel.swift in Sources */, 840D627C29CC369100D5E894 /* CoingeckoStreamableSource.swift in Sources */, 84FF267C28494B13003EC78D /* ParaStkStakeSetupPresenter+StartStaking.swift in Sources */, + 0C13DFCF2AF8ADB300E5F355 /* BigUInt+Operation.swift in Sources */, 88D02FEA2942ED4F00E26390 /* RoundedButton+Style.swift in Sources */, 842BDB2A278C4F3C00AB4B5A /* DAppBrowserAuthorizingState.swift in Sources */, 84FEADF228783F55001DFC26 /* BaseParaStakingRewardCalculatoService.swift in Sources */, @@ -22040,6 +22051,7 @@ 843E9B2827C83985009C143A /* AssetListNftsCell.swift in Sources */, 3E1462D9E1C0D490E81FD288 /* StakingUnbondConfirmViewFactory.swift in Sources */, 9B4BE26140C63E07C256CC97 /* StakingRedeemProtocols.swift in Sources */, + 0C13DFD12AF8AE3E00E5F355 /* SwapBasePresenter.swift in Sources */, 88FB7DD12950720800784E08 /* ContainerProtocols.swift in Sources */, 840AE2E529C9AF9C008FF665 /* EtherscanWalletHistoryDecodable.swift in Sources */, 772B1C7D2A93837800A19061 /* StartStakingCustomValidatorListWireframe.swift in Sources */, diff --git a/novawallet/Common/Extension/Foundation/BigUInt+Operation.swift b/novawallet/Common/Extension/Foundation/BigUInt+Operation.swift new file mode 100644 index 0000000000..f91e651e1c --- /dev/null +++ b/novawallet/Common/Extension/Foundation/BigUInt+Operation.swift @@ -0,0 +1,8 @@ +import Foundation +import BigInt + +extension BigUInt { + func saturatingSub(_ value: BigUInt) -> BigUInt { + self > value ? self - value : 0 + } +} diff --git a/novawallet/Common/Substrate/Types/AccountInfo.swift b/novawallet/Common/Substrate/Types/AccountInfo.swift index 9c8d789bd6..c95078f31c 100644 --- a/novawallet/Common/Substrate/Types/AccountInfo.swift +++ b/novawallet/Common/Substrate/Types/AccountInfo.swift @@ -4,8 +4,12 @@ import BigInt struct AccountInfo: Codable, Equatable { @StringCodable var nonce: UInt32 - @StringCodable var consumers: UInt32 + @OptionStringCodable var consumers: UInt32? let data: AccountData + + var hasConsumers: Bool { + (consumers ?? 0) > 0 + } } struct AccountData: Codable, Equatable { diff --git a/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift b/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift new file mode 100644 index 0000000000..32f2c1d2ba --- /dev/null +++ b/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift @@ -0,0 +1,67 @@ +import Foundation + +struct SwapMaxModel { + let payChainAsset: ChainAsset? + let feeChainAsset: ChainAsset? + let balance: AssetBalance? + let feeModel: AssetConversion.FeeModel? + let payAssetExistense: AssetBalanceExistence? + let receiveAssetExistense: AssetBalanceExistence? + let accountInfo: AccountInfo? + + func minBalanceCoveredByFrozen(in balance: AssetBalance) { + let minBalance = payAssetExistense?.minBalance ?? 0 + + return balance.transferable + minBalance <= balance.freeInPlank + } + + var shouldKeepMinBalance: Bool { + guard payChainAsset?.isUtilityAsset == true else { + return false + } + + let receiveSelfSufficient = receiveAssetExistense?.isSelfSufficient ?? false + let hasConsumers = (accountInfo?.hasConsumers ?? false) + + return (!receiveSelfSufficient || hasConsumers) + } + + private func calculateForNativeAsset(_ payChainAsset: ChainAsset, balance: AssetBalance) -> Decimal { + var maxAmount = balance.transferable + + if shouldKeepMinBalance && !minBalanceCoveredByFrozen(in: balance) { + let minBalance = payAssetExistense?.minBalance ?? 0 + maxAmount = maxAmount.saturatingSub(minBalance) + } + + if let feeModel = feeModel { + let fee = feeModel.totalFee.targetAmount + maxAmount = maxAmount.saturatingSub(fee) + } + + return maxAmount.decimal(precision: payChainAsset.asset.precision) + } + + private func calculateForCustomAsset(_ payChainAsset: ChainAsset, balance: AssetBalance) -> Decimal { + guard let feeModel = feeModel, payChainAsset.chainAssetId == feeChainAsset?.chainAssetId else { + return balance.transferable.decimal(precision: payChainAsset.asset.precision) + } + + let fee = feeModel.totalFee.targetAmount + let maxAmount = balance.transferable.saturatingSub(fee) + + return maxAmount.decimal(precision: payChainAsset.asset.precision) + } + + func calculate() -> Decimal { + guard let payChainAsset = payChainAsset, let balance = balance else { + return 0 + } + + if payChainAsset.isUtilityAsset { + return calculateForNativeAsset(payChainAsset, balance: balance) + } else { + return calculateForCustomAsset(payChainAsset, balance: balance) + } + } +} diff --git a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift index 108dd31212..b1b5034c0a 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift @@ -213,14 +213,8 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB provideAssetBalanceExistense(for: chainAsset) - guard let chainAccount = chainAccountResponse(for: chainAsset) else { - basePresenter?.didReceive(payAccountId: nil) - return - } priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) - - basePresenter?.didReceive(payAccountId: chainAccount.accountId) } func set(feeChainAsset chainAsset: ChainAsset) { @@ -250,8 +244,14 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB ) { fee(args: args) } + + func retryAssetBalanceSubscription(for chainAsset: ChainAsset) { + clear(streamableProvider: &assetBalanceProviders[chainAsset.chainAssetId]) + assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) + } func remakePriceSubscription(for chainAsset: ChainAsset) { + clear(streamableProvider: &priceProviders[chainAsset.chainAssetId]) priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset) } diff --git a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift new file mode 100644 index 0000000000..9b3ac880bc --- /dev/null +++ b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift @@ -0,0 +1,138 @@ +import Foundation + +final class SwapBasePresenter { + let logger: LoggerProtocol + let selectedWallet: MetaAccountModel + + private(set) var balances: [ChainAssetId: AssetBalance] = [:] + + var payAssetBalance: AssetBalance? { + payChainAsset.flatMap { balances[$0.chainAssetId] } + } + + var feeAssetBalance: AssetBalance? { + feeChainAsset.flatMap { balances[$0.chainAssetId] } + } + + var receiveAssetBalance: AssetBalance? { + receiveChainAsset.flatMap { balances[$0.chainAssetId] } + } + + var utilityAssetBalance: AssetBalance? { + guard let utilityAssetId = feeChainAsset?.chain.utilityChainAssetId() else { + return nil + } + + return balances[utilityAssetId] + } + + private(set) var prices: [ChainAssetId: PriceData] = [:] + + var payAssetPriceData: PriceData? { + payChainAsset.flatMap { prices[$0.chainAssetId] } + } + + var receiveAssetPriceData: PriceData? { + receiveChainAsset.flatMap { prices[$0.chainAssetId] } + } + + var feeAssetPriceData: PriceData? { + feeChainAsset.flatMap { prices[$0.chainAssetId] } + } + + var assetBalanceExistences: [ChainAssetId: AssetBalanceExistence] = [:] + + var payAssetBalanceExistense: AssetBalanceExistence? { + payChainAsset.flatMap { assetBalanceExistences[$0.chainAssetId] } + } + + var receiveAssetBalanceExistense: AssetBalanceExistence? { + receiveChainAsset.flatMap { assetBalanceExistences[$0.chainAssetId] } + } + + var feeAssetBalanceExistense: AssetBalanceExistence? { + feeChainAsset.flatMap { assetBalanceExistences[$0.chainAssetId] } + } + + var utilityAssetBalanceExistense: AssetBalanceExistence? { + feeChainAsset?.chain.utilityChainAsset().flatMap { + assetBalanceExistences[$0.chainAssetId] + } + } + + var fee: AssetConversion.FeeModel? + var quote: AssetConversion.Quote? + + func getPayChainAsset() -> ChainAsset? { + nil + } + + func getReceiveChainAsset() -> ChainAsset? { + nil + } + + func getFeeChainAsset() -> ChainAsset? { + nil + } + + func shouldHandleQuote(for args: AssetConversion.QuoteArgs?) -> Bool { + true + } + + func shouldHandleFee(for feeIdentifier: TransactionFeeId, feeChainAssetId: ChainAssetId) -> Bool { + true + } + + func handleBaseError( + _ error: SwapBaseError, + view: ControllerBackedProtocol?, + interactor: SwapBaseInteractorInputProtocol, + wireframe: SwapBaseWireframeProtocol, + locale: Locale + ) { + logger.error("Did receive base error: \(baseError)") + + switch baseError { + case let .quote(_, args): + guard shouldHandleQuote(for: args) else { + return + } + + wireframe.presentRequestStatus(on: view, locale: locale) { [weak self] in + interactor.calculateQuote(for: args) + } + case let .fetchFeeFailed(_, id, feeChainAssetId): + guard shouldHandleFee(for: id, feeChainAssetId: feeChainAssetId) else { + return + } + + wireframe.presentRequestStatus(on: view, locale: locale) { [weak self] in + self?.estimateFee() + } + case let .price(_, priceId): + wireframe.presentRequestStatus(on: view, locale: locale) { [weak self] in + guard let self = self else { + return + } + [self.getPayChainAsset(), self.getReceiveChainAsset(), self.getFeeChainAsset()] + .compactMap { $0 } + .filter { $0.asset.priceId == priceId } + .forEach(interactor.remakePriceSubscription) + } + case let .assetBalance(_, chainAssetId, _): + wireframe.presentRequestStatus(on: view, locale: locale) { [weak self] in + guard let self = self else { + return + } + [self.getPayChainAsset(), self.getReceiveChainAsset(), self.getFeeChainAsset()] + .compactMap { $0 } + .filter { $0.chainAssetId == chainAssetId } + .forEach(interactor.retryAssetBalanceSubscription) + } + case let .assetBalanceExistense(_, chainAsset): + wireframe.presentRequestStatus(on: view, locale: locale) { [weak self] in + interactor.retryAssetBalanceExistenseFetch(for: chainAsset) + } + } + } +} diff --git a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift index ae13a17259..aead8908f2 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift @@ -5,6 +5,7 @@ protocol SwapBaseInteractorInputProtocol: AnyObject { func calculateQuote(for args: AssetConversion.QuoteArgs) func calculateFee(args: AssetConversion.CallArgs) func remakePriceSubscription(for chainAsset: ChainAsset) + func retryAssetBalanceSubscription(for chainAsset: ChainAsset) func retryAssetBalanceExistenseFetch(for chainAsset: ChainAsset) } @@ -13,11 +14,12 @@ protocol SwapBaseInteractorOutputProtocol: AnyObject { func didReceive(fee: AssetConversion.FeeModel?, transactionId: TransactionFeeId, feeChainAssetId: ChainAssetId?) func didReceive(baseError: SwapBaseError) func didReceive(price: PriceData?, priceId: AssetModel.PriceId) - func didReceive(payAccountId: AccountId?) func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId: AccountId) func didReceiveAssetBalance(existense: AssetBalanceExistence, chainAssetId: ChainAssetId) } +protocol SwapBaseWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, ErrorPresentable {} + enum SwapBaseError: Error { case quote(Error, AssetConversion.QuoteArgs) case fetchFeeFailed(Error, TransactionFeeId, FeeChainAssetId?) diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift index 36ccaa85b6..fc7b2061f4 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift @@ -32,8 +32,7 @@ protocol SwapConfirmInteractorOutputProtocol: SwapBaseInteractorOutputProtocol { func didReceive(error: SwapConfirmError) } -protocol SwapConfirmWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, AddressOptionsPresentable, - ErrorPresentable, SwapErrorPresentable, ShortTextInfoPresentable, ModalAlertPresenting, MessageSheetPresentable { +protocol SwapConfirmWireframeProtocol: SwapBaseWireframeProtocol, AddressOptionsPresentable, SwapErrorPresentable, ShortTextInfoPresentable, ModalAlertPresenting, MessageSheetPresentable { func complete(on view: ControllerBackedProtocol?, locale: Locale) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index eac7248575..919e764980 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -49,8 +49,7 @@ protocol SwapSetupInteractorOutputProtocol: SwapBaseInteractorOutputProtocol { func didReceive(setupError: SwapSetupError) } -protocol SwapSetupWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, - ErrorPresentable, SwapErrorPresentable, ShortTextInfoPresentable, PurchasePresentable { +protocol SwapSetupWireframeProtocol: SwapBaseWireframeProtocol, ShortTextInfoPresentable, PurchasePresentable { func showPayTokenSelection( from view: ControllerBackedProtocol?, chainAsset: ChainAsset?, diff --git a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift index f31dc2846f..4603002e02 100644 --- a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift +++ b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift @@ -108,6 +108,26 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { action: swapMaxAction, locale: locale ) + case let .violatingConsumers(model): + let utilityChainAsset = params.utilityChainAsset ?? params.feeChainAsset + + let params = SwapDisplayError.InsufficientBalanceDueConsumers( + minBalance: viewModelFactory.amountFromValue( + targetAssetInfo: utilityChainAsset.assetDisplayInfo, + value: model.minBalance + ).value(for: locale), + fee: viewModelFactory.amountFromValue( + targetAssetInfo: params.feeChainAsset.assetDisplayInfo, + value: model.fee + ).value(for: locale) + ) + + self?.presentable.presentInsufficientBalance( + from: view, + reason: .dueConsumers(params), + action: swapMaxAction, + locale: locale + ) } self?.presentable.presentNotEnoughLiquidity(from: view, locale: locale) diff --git a/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift b/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift index e5f7706f40..fc13575cdc 100644 --- a/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift +++ b/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift @@ -99,6 +99,12 @@ extension SwapErrorPresentable where Self: AlertPresentable & ErrorPresentable { value.fee, preferredLanguages: locale.rLanguages ) + case let .dueConsumers(value): + message = R.string.localizable.swapsViolatingConsumersMessage( + value.minBalance, + value.fee, + preferredLanguages: locale.rLanguages + ) } let cancelAction = AlertPresentableAction( diff --git a/novawallet/Modules/Swaps/Validation/SwapErrorPresentableParams.swift b/novawallet/Modules/Swaps/Validation/SwapErrorPresentableParams.swift index a5702e7e96..4865401240 100644 --- a/novawallet/Modules/Swaps/Validation/SwapErrorPresentableParams.swift +++ b/novawallet/Modules/Swaps/Validation/SwapErrorPresentableParams.swift @@ -12,9 +12,15 @@ enum SwapDisplayError { let fee: String } + struct InsufficientBalanceDueConsumers { + let minBalance: String + let fee: String + } + enum InsufficientBalance { case dueFeePayAsset(InsufficientBalanceDueFeePayAsset) case dueFeeNativeAsset(InsufficientBalanceDueFeeNativeAsset) + case dueConsumers(InsufficientBalanceDueConsumers) } struct DustRemainsDueNativeSwap { diff --git a/novawallet/Modules/Swaps/Validation/SwapModel.swift b/novawallet/Modules/Swaps/Validation/SwapModel.swift index 6347ecfa4b..f2af92ca02 100644 --- a/novawallet/Modules/Swaps/Validation/SwapModel.swift +++ b/novawallet/Modules/Swaps/Validation/SwapModel.swift @@ -18,10 +18,16 @@ struct SwapModel { let minBalanceInNativeAsset: Decimal } + struct InsufficientDueConsumers { + let minBalance: Decimal + let fee: Decimal + } + enum InsufficientBalanceReason { case amountToHigh(InsufficientDueBalance) case feeInNativeAsset(InsufficientDueNativeFee) case feeInPayAsset(InsufficientDuePayAssetFee) + case violatingConsumers(InsufficientDueConsumers) } struct DustAfterSwap { @@ -79,8 +85,8 @@ struct SwapModel { spendingAmount?.toSubstrateAmount(precision: payChainAsset.assetDisplayInfo.assetPrecision) } - var payAssetBalanceAfterSwap: BigUInt { - let balance = payAssetBalance?.transferable ?? 0 + var payAssetTotalBalanceAfterSwap: BigUInt { + let balance = payAssetBalance?.freeInPlank ?? 0 let fee = isFeeInPayToken ? (feeModel?.totalFee.targetAmount ?? 0) : 0 let spendingAmount = spendingAmountInPlank ?? 0 @@ -106,6 +112,16 @@ struct SwapModel { if balance < swapAmount { return .amountToHigh(.init(available: balance.decimal(precision: payChainAsset.asset.precision))) + } else if !notViolatingConsumers { + let minBalance = utilityAssetExistense?.minBalance ?? 0 + let precision = (utilityChainAsset ?? feeChainAsset).asset.precision + let fee = feeModel?.totalFee.targetAmount ?? 0 + return .violatingConsumers( + .init( + minBalance: minBalance.decimal(precision: precision), + fee: fee.decimal(precision: precision) + ) + ) } else if payChainAsset.isUtilityAsset { let available = balance > fee ? balance - fee : 0 @@ -146,35 +162,43 @@ struct SwapModel { return true } - let totalBalance = utilityAssetBalance?.totalInPlank ?? 0 + let totalBalance = utilityAssetBalance?.freeInPlank ?? 0 let minBalance = utilityAssetExistense?.minBalance ?? 0 let fee = feeModel?.totalFee.targetAmount ?? 0 return totalBalance >= minBalance + fee } - var willKillAccount: Bool { - guard payChainAsset.isUtilityAsset else { + var accountWillBeKilled: Bool { + let balance: BigUInt + + if payChainAsset.isUtilityAsset { + balance = payAssetTotalBalanceAfterSwap + } else if feeChainAsset.isUtilityAsset { + let total = feeAssetBalance?.freeInPlank ?? 0 + let fee = feeModel?.totalFee.targetAmount ?? 0 + balance = total > fee ? total - fee : 0 + } else { + // if fee is paid in non native token then we will have at least ed return false } - let balance = payAssetBalanceAfterSwap let minBalance = utilityAssetExistense?.minBalance ?? 0 return balance < minBalance } var notViolatingConsumers: Bool { - guard willKillAccount else { - return false + guard accountWillBeKilled else { + return true } - return (accountInfo?.consumers ?? 0) > 0 + return !(accountInfo?.hasConsumers ?? false) } func checkCanReceive() -> CannotReceiveReason? { let isSelfSufficient = receiveAssetExistense?.isSelfSufficient ?? false - let amountAfterSwap = (receiveAssetBalance?.totalInPlank ?? 0) + (quote?.amountOut ?? 0) + let amountAfterSwap = (receiveAssetBalance?.freeInPlank ?? 0) + (quote?.amountOut ?? 0) let feeInReceiveAsset = feeChainAsset.chainAssetId == receiveChainAsset.chainAssetId ? (feeModel?.totalFee.targetAmount ?? 0) : 0 let minBalance = receiveAssetExistense?.minBalance ?? 0 @@ -183,7 +207,7 @@ struct SwapModel { return .existense( .init(minBalance: minBalance.decimal(precision: receiveChainAsset.asset.precision)) ) - } else if !isSelfSufficient, willKillAccount { + } else if !isSelfSufficient, accountWillBeKilled { let utilityMinBalance = utilityAssetExistense?.minBalance ?? 0 let precision = (utilityChainAsset ?? feeChainAsset).asset.precision return .noProvider( @@ -195,7 +219,7 @@ struct SwapModel { } func checkDustAfterSwap() -> DustReason? { - let balance = payAssetBalanceAfterSwap + let balance = payAssetTotalBalanceAfterSwap let minBalance = payAssetExistense?.minBalance ?? 0 guard balance > 0, balance < minBalance else { diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 0017a917fd..5daf19bb84 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1421,3 +1421,4 @@ "swaps.dust.remains.fee.pay.asset.message" = "You should keep at least %@ after paying %@ network fee and converting %@ to %@ to meet %@ minimum balance.\n\nDo you want to fully swap by adding remaining %@ as well?"; "common.receive.at.least.ed.error" = "You can’t receive less than %@"; "common.receive.not.sufficient.native.asset.error" = "You must keep at least %@ to receive %@ token"; +"swaps.violating.consumers.message" = "You should keep at least %@ after paying %@ network fee as you are holding non sufficient tokens."; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 7ae5b1c4ad..9685713675 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1421,3 +1421,4 @@ "swaps.dust.remains.fee.pay.asset.message" = "Вам необходимо оставить минимум %@ после оплаты %@ комиссии сети и обмена %@ на %@ для поддержания минимального баланса %@.\n\nВы хотите добавить к обмену оставшиеся %@ тоже?"; "common.receive.at.least.ed.error" = "Вы не можете получить меньше чем %@"; "common.receive.not.sufficient.native.asset.error" = "У вас должно быть минимум %@ для получения %@ токена"; +"swaps.violating.consumers.message" = "Вам необходимо оставить минимум %@ после уплаты %@ комиссии сети так как вы владеете несамодостаточными токенами."; From 816c489ea8dc8292b2a20fe401b1d30a584e4056 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 6 Nov 2023 16:12:10 +0100 Subject: [PATCH 122/204] subscribe to block number and account info --- novawallet.xcodeproj/project.pbxproj | 4 - .../GeneralStorageSubscriptionFactory.swift | 25 + .../Subscription/DecodedProviderTypes.swift | 1 + .../GeneralLocalStorageHandler.swift | 12 + .../GeneralLocalStorageSubscriber.swift | 40 +- .../Common/Model/AssetBalanceExistence.swift | 2 +- .../BatchStorageSubscriptionResult.swift | 14 + .../Common/Substrate/Types/AccountInfo.swift | 2 +- .../Service/AssetConversionFeeService.swift | 4 +- .../Swaps/Base/Model/SwapMaxModel.swift | 32 +- .../Swaps/Base/SwapBaseInteractor.swift | 70 +- .../Swaps/Base/SwapBasePresenter.swift | 255 ++++++- .../Swaps/Base/SwapBaseProtocols.swift | 10 +- ...ceDifferenceViewModelFactoryProtocol.swift | 2 +- .../Swaps/Confirm/SwapConfirmInteractor.swift | 2 + .../Swaps/Confirm/SwapConfirmPresenter.swift | 13 +- .../Swaps/Confirm/SwapConfirmProtocols.swift | 3 +- .../Confirm/SwapConfirmViewFactory.swift | 10 +- .../SwapNetworkFeeSheetLayout.swift | 6 +- .../Swaps/Setup/SwapSetupInteractor.swift | 120 ++++ .../Setup/SwapSetupPresenter+Validating.swift | 50 -- .../Swaps/Setup/SwapSetupPresenter.swift | 629 ++++++++---------- .../Swaps/Setup/SwapSetupProtocols.swift | 5 + .../Swaps/Setup/SwapSetupViewFactory.swift | 23 +- .../Swaps/Setup/SwapSetupWireframe.swift | 7 +- 25 files changed, 847 insertions(+), 494 deletions(-) delete mode 100644 novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 8f0b9c6de8..8ecc084cc6 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -700,7 +700,6 @@ 7719018C2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719018B2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift */; }; 7719018E2AE0E71F00D9C918 /* SwapErrorPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719018D2AE0E71F00D9C918 /* SwapErrorPresentable.swift */; }; 771901902AE2424B00D9C918 /* SwapsValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719018F2AE2424B00D9C918 /* SwapsValidationTests.swift */; }; - 771901972AE2897F00D9C918 /* SwapSetupPresenter+Validating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771901962AE2897F00D9C918 /* SwapSetupPresenter+Validating.swift */; }; 7719019B2AE670AE00D9C918 /* ShortTextInfoPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719019A2AE670AE00D9C918 /* ShortTextInfoPresentable.swift */; }; 7719019D2AE6996600D9C918 /* SwapPairView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719019C2AE6996600D9C918 /* SwapPairView.swift */; }; 7719019F2AE6C9DC00D9C918 /* SwapElementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7719019E2AE6C9DC00D9C918 /* SwapElementView.swift */; }; @@ -4762,7 +4761,6 @@ 7719018B2AE0E70B00D9C918 /* SwapDataValidatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapDataValidatorFactory.swift; sourceTree = ""; }; 7719018D2AE0E71F00D9C918 /* SwapErrorPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapErrorPresentable.swift; sourceTree = ""; }; 7719018F2AE2424B00D9C918 /* SwapsValidationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapsValidationTests.swift; sourceTree = ""; }; - 771901962AE2897F00D9C918 /* SwapSetupPresenter+Validating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwapSetupPresenter+Validating.swift"; sourceTree = ""; }; 7719019A2AE670AE00D9C918 /* ShortTextInfoPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortTextInfoPresentable.swift; sourceTree = ""; }; 7719019C2AE6996600D9C918 /* SwapPairView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPairView.swift; sourceTree = ""; }; 7719019E2AE6C9DC00D9C918 /* SwapElementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapElementView.swift; sourceTree = ""; }; @@ -9104,7 +9102,6 @@ C585109AC3A2580AB1253C31 /* SwapSetupInteractor.swift */, BBEB2BC52266AF493D310834 /* SwapSetupViewController.swift */, 6A6BCE8F8DB7576E1E5B5974 /* SwapSetupViewFactory.swift */, - 771901962AE2897F00D9C918 /* SwapSetupPresenter+Validating.swift */, ); path = Setup; sourceTree = ""; @@ -20697,7 +20694,6 @@ 842876AA24AE049B00D91AD8 /* SelectionTitleTableViewCell.swift in Sources */, 840E59B92A187E0700BA6ADD /* GladingPatternModel.swift in Sources */, 84CA68DD26BEA60A003B9453 /* ConnectionFactory.swift in Sources */, - 771901972AE2897F00D9C918 /* SwapSetupPresenter+Validating.swift in Sources */, 84A58FD428A05820003F6ABF /* HardwareSigningError.swift in Sources */, F4B39C53273270A300BB6E10 /* AcalaContributionSetupPresenter.swift in Sources */, 842A737E27DCD1A0006EE1EA /* OperationDetailsExtrinsicView.swift in Sources */, diff --git a/novawallet/Common/DataProvider/GeneralStorageSubscriptionFactory.swift b/novawallet/Common/DataProvider/GeneralStorageSubscriptionFactory.swift index cf7a845552..7b993cd8cd 100644 --- a/novawallet/Common/DataProvider/GeneralStorageSubscriptionFactory.swift +++ b/novawallet/Common/DataProvider/GeneralStorageSubscriptionFactory.swift @@ -4,6 +4,11 @@ import SubstrateSdk protocol GeneralStorageSubscriptionFactoryProtocol { func getBlockNumberProvider(for chainId: ChainModel.Id) throws -> AnyDataProvider + + func getAccountInfoProvider( + for accountId: AccountId, + chainId: ChainModel.Id + ) throws -> AnyDataProvider } final class GeneralStorageSubscriptionFactory: SubstrateLocalSubscriptionFactory, @@ -19,4 +24,24 @@ final class GeneralStorageSubscriptionFactory: SubstrateLocalSubscriptionFactory shouldUseFallback: false ) } + + func getAccountInfoProvider( + for accountId: AccountId, + chainId: ChainModel.Id + ) throws -> AnyDataProvider { + let codingPath = StorageCodingPath.account + + let localKey = try LocalStorageKeyFactory().createFromStoragePath( + codingPath, + accountId: accountId, + chainId: chainId + ) + + return try getDataProvider( + for: localKey, + chainId: chainId, + storageCodingPath: codingPath, + shouldUseFallback: false + ) + } } diff --git a/novawallet/Common/DataProvider/Subscription/DecodedProviderTypes.swift b/novawallet/Common/DataProvider/Subscription/DecodedProviderTypes.swift index 84fa10d1b9..b2adc18c7b 100644 --- a/novawallet/Common/DataProvider/Subscription/DecodedProviderTypes.swift +++ b/novawallet/Common/DataProvider/Subscription/DecodedProviderTypes.swift @@ -12,6 +12,7 @@ typealias DecodedActiveEra = ChainStorageDecodedItem typealias DecodedEraIndex = ChainStorageDecodedItem> typealias DecodedPayee = ChainStorageDecodedItem typealias DecodedBlockNumber = ChainStorageDecodedItem> +typealias DecodedAccountInfo = ChainStorageDecodedItem typealias DecodedCrowdloanFunds = ChainStorageDecodedItem typealias DecodedBagListNode = ChainStorageDecodedItem typealias DecodedPoolMember = ChainStorageDecodedItem diff --git a/novawallet/Common/DataProvider/Subscription/GeneralLocalStorageHandler.swift b/novawallet/Common/DataProvider/Subscription/GeneralLocalStorageHandler.swift index a85102c7e4..10d3c2ae30 100644 --- a/novawallet/Common/DataProvider/Subscription/GeneralLocalStorageHandler.swift +++ b/novawallet/Common/DataProvider/Subscription/GeneralLocalStorageHandler.swift @@ -5,6 +5,12 @@ protocol GeneralLocalStorageHandler { result: Result, chainId: ChainModel.Id ) + + func handleAccountInfo( + result: Result, + accountId: AccountId, + chainId: ChainModel.Id + ) } extension GeneralLocalStorageHandler { @@ -12,4 +18,10 @@ extension GeneralLocalStorageHandler { result _: Result, chainId _: ChainModel.Id ) {} + + func handleAccountInfo( + result _: Result, + accountId _: AccountId, + chainId _: ChainModel.Id + ) {} } diff --git a/novawallet/Common/DataProvider/Subscription/GeneralLocalStorageSubscriber.swift b/novawallet/Common/DataProvider/Subscription/GeneralLocalStorageSubscriber.swift index 57abee453b..79541f5602 100644 --- a/novawallet/Common/DataProvider/Subscription/GeneralLocalStorageSubscriber.swift +++ b/novawallet/Common/DataProvider/Subscription/GeneralLocalStorageSubscriber.swift @@ -1,7 +1,7 @@ import Foundation import RobinHood -protocol GeneralLocalStorageSubscriber where Self: AnyObject { +protocol GeneralLocalStorageSubscriber: LocalStorageProviderObserving { var generalLocalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol { get } var generalLocalSubscriptionHandler: GeneralLocalStorageHandler { get } @@ -9,6 +9,11 @@ protocol GeneralLocalStorageSubscriber where Self: AnyObject { func subscribeToBlockNumber( for chainId: ChainModel.Id ) -> AnyDataProvider? + + func subscribeAccountInfo( + for accountId: AccountId, + chainId: ChainModel.Id + ) -> AnyDataProvider? } extension GeneralLocalStorageSubscriber { @@ -51,6 +56,39 @@ extension GeneralLocalStorageSubscriber { return blockNumberProvider } + + func subscribeAccountInfo( + for accountId: AccountId, + chainId: ChainModel.Id + ) -> AnyDataProvider? { + guard + let provider = try? generalLocalSubscriptionFactory.getAccountInfoProvider( + for: accountId, + chainId: chainId + ) else { + return nil + } + + addDataProviderObserver( + for: provider, + updateClosure: { [weak self] accountInfo in + self?.generalLocalSubscriptionHandler.handleAccountInfo( + result: .success(accountInfo), + accountId: accountId, + chainId: chainId + ) + }, + failureClosure: { [weak self] error in + self?.generalLocalSubscriptionHandler.handleAccountInfo( + result: .failure(error), + accountId: accountId, + chainId: chainId + ) + } + ) + + return provider + } } extension GeneralLocalStorageSubscriber where Self: GeneralLocalStorageHandler { diff --git a/novawallet/Common/Model/AssetBalanceExistence.swift b/novawallet/Common/Model/AssetBalanceExistence.swift index 215896ad09..dba715fd83 100644 --- a/novawallet/Common/Model/AssetBalanceExistence.swift +++ b/novawallet/Common/Model/AssetBalanceExistence.swift @@ -1,7 +1,7 @@ import Foundation import BigInt -struct AssetBalanceExistence { +struct AssetBalanceExistence: Equatable { let minBalance: BigUInt let isSelfSufficient: Bool } diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/BatchStorageSubscriptionResult.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/BatchStorageSubscriptionResult.swift index a8f1dc2331..daf5e30b39 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/BatchStorageSubscriptionResult.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/BatchStorageSubscriptionResult.swift @@ -18,3 +18,17 @@ protocol BatchStorageSubscriptionResult { context: [CodingUserInfoKey: Any]? ) throws } + +struct BatchStorageSubscriptionRawResult: BatchStorageSubscriptionResult { + let values: [BatchStorageSubscriptionResultValue] + let blockHashJson: JSON + + init( + values: [BatchStorageSubscriptionResultValue], + blockHashJson: JSON, + context _: [CodingUserInfoKey: Any]? + ) throws { + self.values = values + self.blockHashJson = blockHashJson + } +} diff --git a/novawallet/Common/Substrate/Types/AccountInfo.swift b/novawallet/Common/Substrate/Types/AccountInfo.swift index c95078f31c..ca91bbbec3 100644 --- a/novawallet/Common/Substrate/Types/AccountInfo.swift +++ b/novawallet/Common/Substrate/Types/AccountInfo.swift @@ -6,7 +6,7 @@ struct AccountInfo: Codable, Equatable { @StringCodable var nonce: UInt32 @OptionStringCodable var consumers: UInt32? let data: AccountData - + var hasConsumers: Bool { (consumers ?? 0) > 0 } diff --git a/novawallet/Modules/AssetConversion/Service/AssetConversionFeeService.swift b/novawallet/Modules/AssetConversion/Service/AssetConversionFeeService.swift index bb0440e868..8bf5c4dbef 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetConversionFeeService.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetConversionFeeService.swift @@ -2,12 +2,12 @@ import Foundation import BigInt extension AssetConversion { - struct AmountWithNative { + struct AmountWithNative: Equatable { let targetAmount: BigUInt let nativeAmount: BigUInt } - struct FeeModel { + struct FeeModel: Equatable { let totalFee: AmountWithNative let networkFeeAddition: AmountWithNative? diff --git a/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift b/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift index 32f2c1d2ba..3cc0c11bfc 100644 --- a/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift +++ b/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift @@ -8,56 +8,56 @@ struct SwapMaxModel { let payAssetExistense: AssetBalanceExistence? let receiveAssetExistense: AssetBalanceExistence? let accountInfo: AccountInfo? - - func minBalanceCoveredByFrozen(in balance: AssetBalance) { + + func minBalanceCoveredByFrozen(in balance: AssetBalance) -> Bool { let minBalance = payAssetExistense?.minBalance ?? 0 - + return balance.transferable + minBalance <= balance.freeInPlank } - + var shouldKeepMinBalance: Bool { guard payChainAsset?.isUtilityAsset == true else { return false } - + let receiveSelfSufficient = receiveAssetExistense?.isSelfSufficient ?? false let hasConsumers = (accountInfo?.hasConsumers ?? false) - + return (!receiveSelfSufficient || hasConsumers) } - + private func calculateForNativeAsset(_ payChainAsset: ChainAsset, balance: AssetBalance) -> Decimal { var maxAmount = balance.transferable - - if shouldKeepMinBalance && !minBalanceCoveredByFrozen(in: balance) { + + if shouldKeepMinBalance, !minBalanceCoveredByFrozen(in: balance) { let minBalance = payAssetExistense?.minBalance ?? 0 maxAmount = maxAmount.saturatingSub(minBalance) } - + if let feeModel = feeModel { let fee = feeModel.totalFee.targetAmount maxAmount = maxAmount.saturatingSub(fee) } - + return maxAmount.decimal(precision: payChainAsset.asset.precision) } - + private func calculateForCustomAsset(_ payChainAsset: ChainAsset, balance: AssetBalance) -> Decimal { guard let feeModel = feeModel, payChainAsset.chainAssetId == feeChainAsset?.chainAssetId else { return balance.transferable.decimal(precision: payChainAsset.asset.precision) } - + let fee = feeModel.totalFee.targetAmount let maxAmount = balance.transferable.saturatingSub(fee) - + return maxAmount.decimal(precision: payChainAsset.asset.precision) } - + func calculate() -> Decimal { guard let payChainAsset = payChainAsset, let balance = balance else { return 0 } - + if payChainAsset.isUtilityAsset { return calculateForNativeAsset(payChainAsset, balance: balance) } else { diff --git a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift index b1b5034c0a..8760208e2d 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift @@ -10,6 +10,7 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB let assetStorageFactory: AssetStorageInfoOperationFactoryProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol + let generalLocalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol let currencyManager: CurrencyManagerProtocol let selectedWallet: MetaAccountModel let operationQueue: OperationQueue @@ -19,7 +20,9 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB private var priceProviders: [ChainAssetId: StreamableProvider] = [:] private var assetBalanceProviders: [ChainAssetId: StreamableProvider] = [:] private var feeModelBuilder: AssetHubFeeModelBuilder? - private var currentChain: ChainModel? + private var accountInfoProvider: AnyDataProvider? + + var currentChain: ChainModel? init( assetConversionAggregator: AssetConversionAggregationFactoryProtocol, @@ -28,6 +31,7 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB assetStorageFactory: AssetStorageInfoOperationFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + generalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol, currencyManager: CurrencyManagerProtocol, selectedWallet: MetaAccountModel, operationQueue: OperationQueue @@ -38,6 +42,7 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB self.assetStorageFactory = assetStorageFactory self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory + generalLocalSubscriptionFactory = generalSubscriptionFactory self.currencyManager = currencyManager self.selectedWallet = selectedWallet self.operationQueue = operationQueue @@ -92,7 +97,7 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB ) { [weak self] feeModel, callArgs, feeChainAssetId in self?.basePresenter?.didReceive( fee: feeModel, - transactionId: callArgs.identifier, + transactionFeeId: callArgs.identifier, feeChainAssetId: feeChainAssetId ) } @@ -100,6 +105,25 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB assetBalanceProviders[utilityAsset.chainAssetId] = assetBalanceSubscription(chainAsset: utilityAsset) } + func updateChain(with newChain: ChainModel) { + let oldChainId = currentChain?.chainId + currentChain = newChain + + if newChain.chainId != oldChainId { + updateAccountInfoProvider(for: newChain) + } + } + + func updateAccountInfoProvider(for chain: ChainModel) { + clear(dataProvider: &accountInfoProvider) + + guard let accountId = selectedWallet.fetch(for: chain.accountRequest())?.accountId else { + return + } + + accountInfoProvider = subscribeAccountInfo(for: accountId, chainId: chain.chainId) + } + func updateSubscriptions(activeChainAssets: Set) { priceProviders = clear(providers: priceProviders, activeChainAssets: activeChainAssets) assetBalanceProviders = clear(providers: assetBalanceProviders, activeChainAssets: activeChainAssets) @@ -192,7 +216,7 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB } func set(receiveChainAsset chainAsset: ChainAsset) { - currentChain = chainAsset.chain + updateChain(with: chainAsset.chain) updateFeeModelBuilder(for: chainAsset.chain) @@ -203,7 +227,7 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB } func set(payChainAsset chainAsset: ChainAsset) { - currentChain = chainAsset.chain + updateChain(with: chainAsset.chain) updateFeeModelBuilder(for: chainAsset.chain) @@ -244,7 +268,7 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB ) { fee(args: args) } - + func retryAssetBalanceSubscription(for chainAsset: ChainAsset) { clear(streamableProvider: &assetBalanceProviders[chainAsset.chainAssetId]) assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset) @@ -258,6 +282,21 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB func retryAssetBalanceExistenseFetch(for chainAsset: ChainAsset) { provideAssetBalanceExistense(for: chainAsset) } + + func retryAccountInfoSubscription() { + guard let chain = currentChain else { + return + } + + updateAccountInfoProvider(for: chain) + } + + // MARK: Overridable General Subscription Handlers + + func handleBlockNumber( + result _: Result, + chainId _: ChainModel.Id + ) {} } extension SwapBaseInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { @@ -290,14 +329,25 @@ extension SwapBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscript feeModelBuilder?.apply(recepientUtilityBalance: balance) } - basePresenter?.didReceive( - balance: balance, - for: chainAssetId, - accountId: accountId - ) + basePresenter?.didReceive(balance: balance, for: chainAssetId) case let .failure(error): basePresenter?.didReceive(baseError: .assetBalance(error, chainAssetId, accountId)) } } } + +extension SwapBaseInteractor: GeneralLocalStorageSubscriber, GeneralLocalStorageHandler { + func handleAccountInfo( + result: Result, + accountId _: AccountId, + chainId: ChainModel.Id + ) { + switch result { + case let .success(accountInfo): + basePresenter?.didReceive(accountInfo: accountInfo, chainId: chainId) + case let .failure(error): + basePresenter?.didReceive(baseError: .accountInfo(error)) + } + } +} diff --git a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift index 9b3ac880bc..4d062b7654 100644 --- a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift +++ b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift @@ -1,25 +1,36 @@ import Foundation -final class SwapBasePresenter { +class SwapBasePresenter { let logger: LoggerProtocol let selectedWallet: MetaAccountModel + let dataValidatingFactory: SwapDataValidatorFactoryProtocol + + init( + selectedWallet: MetaAccountModel, + dataValidatingFactory: SwapDataValidatorFactoryProtocol, + logger: LoggerProtocol + ) { + self.selectedWallet = selectedWallet + self.dataValidatingFactory = dataValidatingFactory + self.logger = logger + } private(set) var balances: [ChainAssetId: AssetBalance] = [:] var payAssetBalance: AssetBalance? { - payChainAsset.flatMap { balances[$0.chainAssetId] } + getPayChainAsset().flatMap { balances[$0.chainAssetId] } } var feeAssetBalance: AssetBalance? { - feeChainAsset.flatMap { balances[$0.chainAssetId] } + getFeeChainAsset().flatMap { balances[$0.chainAssetId] } } var receiveAssetBalance: AssetBalance? { - receiveChainAsset.flatMap { balances[$0.chainAssetId] } + getReceiveChainAsset().flatMap { balances[$0.chainAssetId] } } var utilityAssetBalance: AssetBalance? { - guard let utilityAssetId = feeChainAsset?.chain.utilityChainAssetId() else { + guard let utilityAssetId = getFeeChainAsset()?.chain.utilityChainAssetId() else { return nil } @@ -29,60 +40,130 @@ final class SwapBasePresenter { private(set) var prices: [ChainAssetId: PriceData] = [:] var payAssetPriceData: PriceData? { - payChainAsset.flatMap { prices[$0.chainAssetId] } + getPayChainAsset().flatMap { prices[$0.chainAssetId] } } var receiveAssetPriceData: PriceData? { - receiveChainAsset.flatMap { prices[$0.chainAssetId] } + getReceiveChainAsset().flatMap { prices[$0.chainAssetId] } } var feeAssetPriceData: PriceData? { - feeChainAsset.flatMap { prices[$0.chainAssetId] } + getFeeChainAsset().flatMap { prices[$0.chainAssetId] } } var assetBalanceExistences: [ChainAssetId: AssetBalanceExistence] = [:] var payAssetBalanceExistense: AssetBalanceExistence? { - payChainAsset.flatMap { assetBalanceExistences[$0.chainAssetId] } + getPayChainAsset().flatMap { assetBalanceExistences[$0.chainAssetId] } } var receiveAssetBalanceExistense: AssetBalanceExistence? { - receiveChainAsset.flatMap { assetBalanceExistences[$0.chainAssetId] } + getReceiveChainAsset().flatMap { assetBalanceExistences[$0.chainAssetId] } } var feeAssetBalanceExistense: AssetBalanceExistence? { - feeChainAsset.flatMap { assetBalanceExistences[$0.chainAssetId] } + getFeeChainAsset().flatMap { assetBalanceExistences[$0.chainAssetId] } } var utilityAssetBalanceExistense: AssetBalanceExistence? { - feeChainAsset?.chain.utilityChainAsset().flatMap { + getFeeChainAsset()?.chain.utilityChainAsset().flatMap { assetBalanceExistences[$0.chainAssetId] } } - + var fee: AssetConversion.FeeModel? var quote: AssetConversion.Quote? - + var accountInfo: AccountInfo? + + func getSwapModel() -> SwapModel? { + guard + let payChainAsset = getPayChainAsset(), + let receiveChainAsset = getReceiveChainAsset(), + let feeChainAsset = getFeeChainAsset() else { + return nil + } + + return .init( + payChainAsset: payChainAsset, + receiveChainAsset: receiveChainAsset, + feeChainAsset: feeChainAsset, + spendingAmount: getInputAmount(), + payAssetBalance: payAssetBalance, + feeAssetBalance: feeAssetBalance, + receiveAssetBalance: receiveAssetBalance, + utilityAssetBalance: utilityAssetBalance, + payAssetExistense: payAssetBalanceExistense, + receiveAssetExistense: receiveAssetBalanceExistense, + feeAssetExistense: feeAssetBalanceExistense, + utilityAssetExistense: utilityAssetBalanceExistense, + feeModel: fee, + quote: quote, + accountInfo: accountInfo + ) + } + + func getMaxModel() -> SwapMaxModel? { + .init( + payChainAsset: getPayChainAsset(), + feeChainAsset: getFeeChainAsset(), + balance: payAssetBalance, + feeModel: fee, + payAssetExistense: payAssetBalanceExistense, + receiveAssetExistense: receiveAssetBalanceExistense, + accountInfo: nil + ) + } + + func getInputAmount() -> Decimal? { + fatalError("Must be implemented by parent class") + } + func getPayChainAsset() -> ChainAsset? { - nil + fatalError("Must be implemented by parent class") } - + func getReceiveChainAsset() -> ChainAsset? { - nil + fatalError("Must be implemented by parent class") } - + func getFeeChainAsset() -> ChainAsset? { - nil + fatalError("Must be implemented by parent class") + } + + func shouldHandleQuote(for _: AssetConversion.QuoteArgs?) -> Bool { + fatalError("Must be implemented by parent class") } - - func shouldHandleQuote(for args: AssetConversion.QuoteArgs?) -> Bool { - true + + func shouldHandleFee(for _: TransactionFeeId, feeChainAssetId _: ChainAssetId?) -> Bool { + fatalError("Must be implemented by parent class") } - - func shouldHandleFee(for feeIdentifier: TransactionFeeId, feeChainAssetId: ChainAssetId) -> Bool { - true + + func estimateFee() { + fatalError("Must be implemented by parent class") } - + + func applySwapMax() { + fatalError("Must be implemented by parent class") + } + + func handleBaseError(_: SwapBaseError) {} + + func handleNewQuote(_: AssetConversion.Quote, for _: AssetConversion.QuoteArgs) {} + + func handleNewFee( + _: AssetConversion.FeeModel?, + transactionFeeId _: TransactionFeeId, + feeChainAssetId _: ChainAssetId? + ) {} + + func handleNewPrice(_: PriceData?, chainAssetId _: ChainAssetId) {} + + func handleNewBalance(_: AssetBalance?, for _: ChainAssetId) {} + + func handleNewBalanceExistense(_: AssetBalanceExistence, chainAssetId _: ChainAssetId) {} + + func handleNewAccountInfo(_: AccountInfo?, chainId _: ChainModel.Id) {} + func handleBaseError( _ error: SwapBaseError, view: ControllerBackedProtocol?, @@ -90,15 +171,15 @@ final class SwapBasePresenter { wireframe: SwapBaseWireframeProtocol, locale: Locale ) { - logger.error("Did receive base error: \(baseError)") + logger.error("Did receive base error: \(error)") - switch baseError { + switch error { case let .quote(_, args): guard shouldHandleQuote(for: args) else { return } - - wireframe.presentRequestStatus(on: view, locale: locale) { [weak self] in + + wireframe.presentRequestStatus(on: view, locale: locale) { interactor.calculateQuote(for: args) } case let .fetchFeeFailed(_, id, feeChainAssetId): @@ -130,9 +211,121 @@ final class SwapBasePresenter { .forEach(interactor.retryAssetBalanceSubscription) } case let .assetBalanceExistense(_, chainAsset): - wireframe.presentRequestStatus(on: view, locale: locale) { [weak self] in + wireframe.presentRequestStatus(on: view, locale: locale) { interactor.retryAssetBalanceExistenseFetch(for: chainAsset) } + case .accountInfo: + wireframe.presentRequestStatus(on: view, locale: locale) { + interactor.retryAccountInfoSubscription() + } + } + } + + func getBaseValidations(for swapModel: SwapModel, locale: Locale) -> [DataValidating] { + [ + dataValidatingFactory.hasInPlank( + fee: swapModel.feeModel?.totalFee.targetAmount, + locale: locale, + precision: swapModel.feeChainAsset.assetDisplayInfo.assetPrecision + ) { [weak self] in + self?.estimateFee() + }, + dataValidatingFactory.hasSufficientBalance( + params: swapModel, + swapMaxAction: { [weak self] in + self?.applySwapMax() + }, + locale: locale + ), + dataValidatingFactory.notViolatingMinBalancePaying( + fee: swapModel.feeChainAsset.isUtilityAsset ? swapModel.feeModel?.totalFee.targetAmount : 0, + total: swapModel.utilityAssetBalance?.freeInPlank, + minBalance: swapModel.utilityAssetExistense?.minBalance, + locale: locale + ), + dataValidatingFactory.canReceive(params: swapModel, locale: locale), + dataValidatingFactory.noDustRemains( + params: swapModel, + swapMaxAction: { [weak self] in + self?.applySwapMax() + }, + locale: locale + ) + ] + } +} + +extension SwapBasePresenter: SwapBaseInteractorOutputProtocol { + func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) { + guard shouldHandleQuote(for: quoteArgs), self.quote != quote else { + return + } + + self.quote = quote + + handleNewQuote(quote, for: quoteArgs) + } + + func didReceive( + fee: AssetConversion.FeeModel?, + transactionFeeId: TransactionFeeId, + feeChainAssetId: ChainAssetId? + ) { + guard shouldHandleFee(for: transactionFeeId, feeChainAssetId: feeChainAssetId), self.fee != fee else { + return + } + + self.fee = fee + + handleNewFee(fee, transactionFeeId: transactionFeeId, feeChainAssetId: feeChainAssetId) + } + + func didReceive(baseError: SwapBaseError) { + handleBaseError(baseError) + } + + func didReceive(price: PriceData?, priceId: AssetModel.PriceId) { + let optChainAssetId = [getPayChainAsset(), getReceiveChainAsset(), getFeeChainAsset()] + .compactMap { $0 } + .filter { $0.asset.priceId == priceId } + .first?.chainAssetId + + guard let chainAssetId = optChainAssetId, prices[chainAssetId] != price else { + return + } + + prices[chainAssetId] = price + + handleNewPrice(price, chainAssetId: chainAssetId) + } + + func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId) { + guard balances[chainAsset] != balance else { + return + } + + balances[chainAsset] = balance + + handleNewBalance(balance, for: chainAsset) + } + + func didReceiveAssetBalance(existense: AssetBalanceExistence, chainAssetId: ChainAssetId) { + guard assetBalanceExistences[chainAssetId] != existense else { + return } + + assetBalanceExistences[chainAssetId] = existense + + handleNewBalanceExistense(existense, chainAssetId: chainAssetId) + } + + func didReceive(accountInfo: AccountInfo?, chainId: ChainModel.Id) { + guard self.accountInfo != accountInfo else { + return + } + + logger.debug("New account info: \(String(describing: accountInfo))") + + handleNewAccountInfo(accountInfo, chainId: chainId) } } diff --git a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift index aead8908f2..03df66b5f9 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift @@ -7,18 +7,21 @@ protocol SwapBaseInteractorInputProtocol: AnyObject { func remakePriceSubscription(for chainAsset: ChainAsset) func retryAssetBalanceSubscription(for chainAsset: ChainAsset) func retryAssetBalanceExistenseFetch(for chainAsset: ChainAsset) + func retryAccountInfoSubscription() } protocol SwapBaseInteractorOutputProtocol: AnyObject { func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) - func didReceive(fee: AssetConversion.FeeModel?, transactionId: TransactionFeeId, feeChainAssetId: ChainAssetId?) + func didReceive(fee: AssetConversion.FeeModel?, transactionFeeId: TransactionFeeId, feeChainAssetId: ChainAssetId?) func didReceive(baseError: SwapBaseError) func didReceive(price: PriceData?, priceId: AssetModel.PriceId) - func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId: AccountId) + func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId) func didReceiveAssetBalance(existense: AssetBalanceExistence, chainAssetId: ChainAssetId) + func didReceive(accountInfo: AccountInfo?, chainId: ChainModel.Id) } -protocol SwapBaseWireframeProtocol: AnyObject, AlertPresentable, CommonRetryable, ErrorPresentable {} +protocol SwapBaseWireframeProtocol: AnyObject, SwapErrorPresentable, AlertPresentable, + CommonRetryable, ErrorPresentable {} enum SwapBaseError: Error { case quote(Error, AssetConversion.QuoteArgs) @@ -26,4 +29,5 @@ enum SwapBaseError: Error { case price(Error, AssetModel.PriceId) case assetBalance(Error, ChainAssetId, AccountId) case assetBalanceExistense(Error, ChainAsset) + case accountInfo(Error) } diff --git a/novawallet/Modules/Swaps/Base/SwapPriceDifferenceViewModelFactoryProtocol.swift b/novawallet/Modules/Swaps/Base/SwapPriceDifferenceViewModelFactoryProtocol.swift index 8d04ca07ed..45bdfddfb3 100644 --- a/novawallet/Modules/Swaps/Base/SwapPriceDifferenceViewModelFactoryProtocol.swift +++ b/novawallet/Modules/Swaps/Base/SwapPriceDifferenceViewModelFactoryProtocol.swift @@ -40,7 +40,7 @@ extension SwapPriceDifferenceViewModelFactoryProtocol { return nil } - var diff = abs(amountPriceIn - amountPriceOut) / amountPriceIn + let diff = abs(amountPriceIn - amountPriceOut) / amountPriceIn let diffString = localizedPercentForamatter.stringFromDecimal(diff)?.inParenthesis() ?? "" switch diff { diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift index ce51cc1a4b..1c82b582e7 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift @@ -23,6 +23,7 @@ final class SwapConfirmInteractor: SwapBaseInteractor { extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + generalLocalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol, currencyManager: CurrencyManagerProtocol, selectedWallet: MetaAccountModel, operationQueue: OperationQueue, @@ -41,6 +42,7 @@ final class SwapConfirmInteractor: SwapBaseInteractor { assetStorageFactory: assetStorageFactory, priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, + generalSubscriptionFactory: generalLocalSubscriptionFactory, currencyManager: currencyManager, selectedWallet: selectedWallet, operationQueue: operationQueue diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 6c3474de69..d9819d1ce6 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -370,7 +370,7 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { func didReceive( fee: AssetConversion.FeeModel?, - transactionId _: TransactionFeeId, + transactionFeeId _: TransactionFeeId, feeChainAssetId _: ChainAssetId? ) { self.fee = fee @@ -394,12 +394,7 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { } } - func didReceive(payAccountId: AccountId?) { - self.payAccountId = payAccountId - estimateFee() - } - - func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId _: AccountId) { + func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId) { balances[chainAsset] = balance } @@ -435,9 +430,13 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in self?.interactor.retryAssetBalanceExistenseFetch(for: chainAsset) } + case .accountInfo: + break } } + func didReceive(accountInfo _: AccountInfo?, chainId _: ChainModel.Id) {} + func didReceive(error: SwapConfirmError) { view?.didReceiveStopLoading() switch error { diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift index fc7b2061f4..f2030c5960 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift @@ -32,7 +32,8 @@ protocol SwapConfirmInteractorOutputProtocol: SwapBaseInteractorOutputProtocol { func didReceive(error: SwapConfirmError) } -protocol SwapConfirmWireframeProtocol: SwapBaseWireframeProtocol, AddressOptionsPresentable, SwapErrorPresentable, ShortTextInfoPresentable, ModalAlertPresenting, MessageSheetPresentable { +protocol SwapConfirmWireframeProtocol: SwapBaseWireframeProtocol, AddressOptionsPresentable, + ShortTextInfoPresentable, ModalAlertPresenting, MessageSheetPresentable { func complete(on view: ControllerBackedProtocol?, locale: Locale) } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index 2c413e8ec8..a2605ec0e7 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -4,7 +4,8 @@ import RobinHood struct SwapConfirmViewFactory { static func createView( - initState: SwapConfirmInitState + initState: SwapConfirmInitState, + generalSubscriptonFactory: GeneralStorageSubscriptionFactoryProtocol ) -> SwapConfirmViewProtocol? { let accountRequest = initState.chainAssetIn.chain.accountRequest() @@ -15,7 +16,8 @@ struct SwapConfirmViewFactory { } guard let interactor = createInteractor( wallet: wallet, - initState: initState + initState: initState, + generalSubscriptonFactory: generalSubscriptonFactory ) else { return nil } @@ -61,7 +63,8 @@ struct SwapConfirmViewFactory { private static func createInteractor( wallet: MetaAccountModel, - initState: SwapConfirmInitState + initState: SwapConfirmInitState, + generalSubscriptonFactory: GeneralStorageSubscriptionFactoryProtocol ) -> SwapConfirmInteractor? { let chainRegistry = ChainRegistryFacade.sharedRegistry let accountRequest = initState.chainAssetIn.chain.accountRequest() @@ -115,6 +118,7 @@ struct SwapConfirmViewFactory { extrinsicServiceFactory: extrinsicServiceFactory, priceLocalSubscriptionFactory: PriceProviderFactory.shared, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + generalLocalSubscriptionFactory: generalSubscriptonFactory, currencyManager: currencyManager, selectedWallet: wallet, operationQueue: operationQueue, diff --git a/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetLayout.swift b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetLayout.swift index eefb395e05..bbc417c169 100644 --- a/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetLayout.swift +++ b/novawallet/Modules/Swaps/Setup/NetworkFeeBottomSheet/SwapNetworkFeeSheetLayout.swift @@ -83,12 +83,16 @@ extension SwapNetworkFeeSheetLayout { } private func height(for label: UILabel, with text: String) -> CGFloat { + guard let font = label.font else { + return 0 + } + let width = UIScreen.main.bounds.width - UIConstants.horizontalInset * 2 let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude) let boundingBox = text.boundingRect( with: constraintRect, options: .usesLineFragmentOrigin, - attributes: [.font: label.font], + attributes: [.font: font], context: nil ) return boundingBox.height diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 05e1e7845a..82d6217f6c 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -1,13 +1,18 @@ import UIKit import RobinHood import BigInt +import SubstrateSdk final class SwapSetupInteractor: SwapBaseInteractor { let xcmTransfersSyncService: XcmTransfersSyncServiceProtocol + let storageRepository: AnyDataProviderRepository private var xcmTransfers: XcmTransfers? private var canPayFeeInAssetCall = CancellableCallStore() + private var remoteSubscription: CallbackBatchStorageSubscription? + private var blockNumberSubscription: AnyDataProvider? + init( xcmTransfersSyncService: XcmTransfersSyncServiceProtocol, assetConversionAggregatorFactory: AssetConversionAggregationFactoryProtocol, @@ -16,11 +21,14 @@ final class SwapSetupInteractor: SwapBaseInteractor { assetStorageFactory: AssetStorageInfoOperationFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + generalLocalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol, + storageRepository: AnyDataProviderRepository, currencyManager: CurrencyManagerProtocol, selectedWallet: MetaAccountModel, operationQueue: OperationQueue ) { self.xcmTransfersSyncService = xcmTransfersSyncService + self.storageRepository = storageRepository super.init( assetConversionAggregator: assetConversionAggregatorFactory, @@ -29,6 +37,7 @@ final class SwapSetupInteractor: SwapBaseInteractor { assetStorageFactory: assetStorageFactory, priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, + generalSubscriptionFactory: generalLocalSubscriptionFactory, currencyManager: currencyManager, selectedWallet: selectedWallet, operationQueue: operationQueue @@ -71,6 +80,7 @@ final class SwapSetupInteractor: SwapBaseInteractor { deinit { xcmTransfersSyncService.throttle() canPayFeeInAssetCall.cancel() + clearRemoteSubscription() } private func setupXcmTransfersSyncService() { @@ -141,10 +151,99 @@ final class SwapSetupInteractor: SwapBaseInteractor { } } + private func clearRemoteSubscription() { + remoteSubscription?.unsubscribe() + remoteSubscription = nil + } + + private func setupRemoteSubscription(for chain: ChainModel) throws { + guard + let accountId = selectedWallet.fetch(for: chain.accountRequest())?.accountId, + let connection = chainRegistry.getConnection(for: chain.chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chain.chainId) else { + return + } + + let localKeyFactory = LocalStorageKeyFactory() + + let blockNumberKey = try localKeyFactory.createFromStoragePath(.blockNumber, chainId: chain.chainId) + let blockNumberRequest = BatchStorageSubscriptionRequest( + innerRequest: UnkeyedSubscriptionRequest( + storagePath: .blockNumber, + localKey: blockNumberKey + ), + mappingKey: nil + ) + + let accountInfoKey = try localKeyFactory.createFromStoragePath( + .account, + accountId: accountId, + chainId: chain.chainId + ) + let accountInfoRequest = BatchStorageSubscriptionRequest( + innerRequest: MapSubscriptionRequest( + storagePath: .account, + localKey: accountInfoKey, + keyParamClosure: { + BytesCodable(wrappedValue: accountId) + } + ), + mappingKey: nil + ) + + remoteSubscription = CallbackBatchStorageSubscription( + requests: [blockNumberRequest, accountInfoRequest], + connection: connection, + runtimeService: runtimeService, + repository: storageRepository, + operationQueue: operationQueue, + callbackQueue: .main, + callbackClosure: { _ in + // we are listening remote subscription via database + } + ) + + remoteSubscription?.subscribe() + } + + private func updateBlockNumberSubscription(for chain: ChainModel) { + clear(dataProvider: &blockNumberSubscription) + blockNumberSubscription = subscribeToBlockNumber(for: chain.chainId) + } + + override func updateChain(with newChain: ChainModel) { + let oldChainId = currentChain?.chainId + + super.updateChain(with: newChain) + + if newChain.chainId != oldChainId { + updateBlockNumberSubscription(for: newChain) + + do { + clearRemoteSubscription() + try setupRemoteSubscription(for: newChain) + } catch { + presenter?.didReceive(setupError: .remoteSubscription(error)) + } + } + } + override func setup() { super.setup() setupXcmTransfersSyncService() } + + override func handleBlockNumber( + result: Result, + chainId: ChainModel.Id + ) { + switch result { + case let .success(blockNumber): + presenter?.didReceiveBlockNumber(blockNumber, chainId: chainId) + case let .failure(error): + presenter?.didReceive(setupError: .blockNumber(error)) + } + } } extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { @@ -176,4 +275,25 @@ extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { set(feeChainAsset: $0) } } + + func retryRemoteSubscription() { + guard let chain = currentChain else { + return + } + + do { + clearRemoteSubscription() + try setupRemoteSubscription(for: chain) + } catch { + presenter?.didReceive(setupError: .remoteSubscription(error)) + } + } + + func retryBlockNumberSubscription() { + guard let chain = currentChain else { + return + } + + updateBlockNumberSubscription(for: chain) + } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift deleted file mode 100644 index a8dd3a113f..0000000000 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter+Validating.swift +++ /dev/null @@ -1,50 +0,0 @@ -import Foundation -import BigInt - -extension SwapSetupPresenter { - func validators( - spendingAmount: Decimal?, - payChainAsset: ChainAsset, - feeChainAsset: ChainAsset - ) -> [DataValidating] { - let validators: [DataValidating] = [ - dataValidatingFactory.hasInPlank( - fee: fee?.totalFee.targetAmount, - locale: selectedLocale, - precision: feeChainAsset.assetDisplayInfo.assetPrecision - ) { [weak self] in - self?.estimateFee() - }, - dataValidatingFactory.canSpendAmountInPlank( - balance: payAssetBalance?.transferable, - spendingAmount: spendingAmount, - asset: payChainAsset.assetDisplayInfo, - locale: selectedLocale - ), - dataValidatingFactory.canPayFeeSpendingAmountInPlank( - balance: payAssetBalance?.transferable, - fee: payChainAsset.chainAssetId == feeChainAsset.chainAssetId ? fee?.totalFee.targetAmount : 0, - spendingAmount: spendingAmount, - asset: feeChainAsset.assetDisplayInfo, - locale: selectedLocale - ), - dataValidatingFactory.notViolatingMinBalancePaying( - fee: feeChainAsset.isUtilityAsset ? fee?.totalFee.targetAmount : 0, - total: utilityAssetBalance?.totalInPlank, - minBalance: utilityAssetMinBalance, - locale: selectedLocale - ), - dataValidatingFactory.has( - quote: quote, - payChainAssetId: payChainAsset.chainAssetId, - receiveChainAssetId: receiveChainAsset?.chainAssetId, - locale: selectedLocale, - onError: { [weak self] in - self?.refreshQuote(direction: self?.quoteArgs?.direction ?? .sell) - } - ) - ] - - return validators - } -} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 73aea4c83d..598c3d8238 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -2,90 +2,30 @@ import Foundation import SoraFoundation import BigInt -final class SwapSetupPresenter: PurchaseFlowManaging { +final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { weak var view: SwapSetupViewProtocol? let wireframe: SwapSetupWireframeProtocol let interactor: SwapSetupInteractorInputProtocol - let dataValidatingFactory: SwapDataValidatorFactoryProtocol - let logger: LoggerProtocol - let selectedAccount: MetaAccountModel let purchaseProvider: PurchaseProviderProtocol private(set) var viewModelFactory: SwapsSetupViewModelFactoryProtocol - private(set) var balances: [ChainAssetId: AssetBalance] = [:] - - var payAssetBalance: AssetBalance? { - payChainAsset.flatMap { balances[$0.chainAssetId] } - } - - var feeAssetBalance: AssetBalance? { - feeChainAsset.flatMap { balances[$0.chainAssetId] } - } - - var receiveAssetBalance: AssetBalance? { - receiveChainAsset.flatMap { balances[$0.chainAssetId] } - } - - var utilityAssetBalance: AssetBalance? { - guard let utilityAssetId = feeChainAsset?.chain.utilityChainAssetId() else { - return nil - } - - return balances[utilityAssetId] - } - - private(set) var prices: [ChainAssetId: PriceData] = [:] - - var payAssetPriceData: PriceData? { - payChainAsset.flatMap { prices[$0.chainAssetId] } - } - - var receiveAssetPriceData: PriceData? { - receiveChainAsset.flatMap { prices[$0.chainAssetId] } - } - - var feeAssetPriceData: PriceData? { - feeChainAsset.flatMap { prices[$0.chainAssetId] } - } - - private(set) var payChainAsset: ChainAsset? - private(set) var canPayFeeInPayAsset: Bool = false - private(set) var receiveChainAsset: ChainAsset? - private(set) var feeChainAsset: ChainAsset? - private(set) var payAmountInput: AmountInputResult? - private(set) var receiveAmountInput: Decimal? - private(set) var fee: AssetConversion.FeeModel? - private(set) var quote: AssetConversion.Quote? private(set) var quoteArgs: AssetConversion.QuoteArgs? { didSet { provideDetailsViewModel(isAvailable: quoteArgs != nil) } } - private(set) var assetBalanceExistences: [ChainAssetId: AssetBalanceExistence] = [:] + private var payAmountInput: AmountInputResult? + private var receiveAmountInput: Decimal? - var payAssetMinBalance: BigUInt? { - payChainAsset.flatMap { assetBalanceExistences[$0.chainAssetId]?.minBalance } - } - - var receiveAssetMinBalance: BigUInt? { - receiveChainAsset.flatMap { assetBalanceExistences[$0.chainAssetId]?.minBalance } - } + private var canPayFeeInPayAsset: Bool = false + private var payChainAsset: ChainAsset? + private var receiveChainAsset: ChainAsset? + private var feeChainAsset: ChainAsset? - var feeAssetMinBalance: BigUInt? { - feeChainAsset.flatMap { assetBalanceExistences[$0.chainAssetId]?.minBalance } - } - - var utilityAssetMinBalance: BigUInt? { - feeChainAsset?.chain.utilityChainAsset().flatMap { - assetBalanceExistences[$0.chainAssetId]?.minBalance - } - } - - private var slippage: BigRational? private var feeIdentifier: SwapSetupFeeIdentifier? - private var accountId: AccountId? + private var slippage: BigRational? private var depositOperations: [DepositOperationModel] = [] private var purchaseActions: [PurchaseAction] = [] private var depositCrossChainAssets: [ChainAsset] = [] @@ -98,7 +38,7 @@ final class SwapSetupPresenter: PurchaseFlowManaging { viewModelFactory: SwapsSetupViewModelFactoryProtocol, dataValidatingFactory: SwapDataValidatorFactoryProtocol, localizationManager: LocalizationManagerProtocol, - selectedAccount: MetaAccountModel, + selectedWallet: MetaAccountModel, purchaseProvider: PurchaseProviderProtocol, logger: LoggerProtocol ) { @@ -107,24 +47,24 @@ final class SwapSetupPresenter: PurchaseFlowManaging { self.interactor = interactor self.wireframe = wireframe self.viewModelFactory = viewModelFactory - self.dataValidatingFactory = dataValidatingFactory - self.logger = logger - self.selectedAccount = selectedAccount self.purchaseProvider = purchaseProvider + + super.init( + selectedWallet: selectedWallet, + dataValidatingFactory: dataValidatingFactory, + logger: logger + ) + self.localizationManager = localizationManager } - private func provideButtonState() { - let buttonState = viewModelFactory.buttonState( - assetIn: payChainAsset?.chainAssetId, - assetOut: receiveChainAsset?.chainAssetId, - amountIn: getPayAmount(for: payAmountInput), - amountOut: receiveAmountInput - ) - view?.didReceiveButtonState( - title: buttonState.title.value(for: selectedLocale), - enabled: buttonState.enabled - ) + private func getPayAmount(for input: AmountInputResult?) -> Decimal? { + guard let input = input else { + return nil + } + + let maxAmount = getMaxModel()?.calculate() + return input.absoluteValue(from: maxAmount ?? 0) } private func providePayTitle() { @@ -136,22 +76,33 @@ final class SwapSetupPresenter: PurchaseFlowManaging { } private func providePayAssetViewModel() { - let payAssetViewModel = viewModelFactory.payAssetViewModel( - chainAsset: payChainAsset - ) + let payAssetViewModel = viewModelFactory.payAssetViewModel(chainAsset: payChainAsset) view?.didReceiveInputChainAsset(payViewModel: payAssetViewModel) } + private func providePayAmountInputViewModel() { + guard let payChainAsset = payChainAsset else { + return + } + let amountInputViewModel = viewModelFactory.amountInputViewModel( + chainAsset: payChainAsset, + amount: getPayAmount(for: payAmountInput) + ) + view?.didReceiveAmount(payInputViewModel: amountInputViewModel) + } + private func providePayInputPriceViewModel() { guard let assetDisplayInfo = payChainAsset?.assetDisplayInfo else { view?.didReceiveAmountInputPrice(payViewModel: nil) return } + let inputPriceViewModel = viewModelFactory.inputPriceViewModel( assetDisplayInfo: assetDisplayInfo, amount: getPayAmount(for: payAmountInput), priceData: payAssetPriceData ) + view?.didReceiveAmountInputPrice(payViewModel: inputPriceViewModel) } @@ -167,6 +118,17 @@ final class SwapSetupPresenter: PurchaseFlowManaging { view?.didReceiveInputChainAsset(receiveViewModel: receiveAssetViewModel) } + private func provideReceiveAmountInputViewModel() { + guard let receiveChainAsset = receiveChainAsset else { + return + } + let amountInputViewModel = viewModelFactory.amountInputViewModel( + chainAsset: receiveChainAsset, + amount: receiveAmountInput + ) + view?.didReceiveAmount(receiveInputViewModel: amountInputViewModel) + } + private func provideReceiveInputPriceViewModel() { guard let assetDisplayInfo = receiveChainAsset?.assetDisplayInfo else { view?.didReceiveAmountInputPrice(receiveViewModel: nil) @@ -203,39 +165,6 @@ final class SwapSetupPresenter: PurchaseFlowManaging { )) } - private func providePayAmountInputViewModel() { - guard let payChainAsset = payChainAsset else { - return - } - let amountInputViewModel = viewModelFactory.amountInputViewModel( - chainAsset: payChainAsset, - amount: getPayAmount(for: payAmountInput) - ) - view?.didReceiveAmount(payInputViewModel: amountInputViewModel) - } - - private func provideReceiveAmountInputViewModel() { - guard let receiveChainAsset = receiveChainAsset else { - return - } - let amountInputViewModel = viewModelFactory.amountInputViewModel( - chainAsset: receiveChainAsset, - amount: receiveAmountInput - ) - view?.didReceiveAmount(receiveInputViewModel: amountInputViewModel) - } - - private func provideSettingsState() { - view?.didReceiveSettingsState(isAvailable: payChainAsset != nil) - } - - private func getPayAmount(for input: AmountInputResult?) -> Decimal? { - guard let input = input, let balanceMinusFee = balanceMinusFee() else { - return nil - } - return input.absoluteValue(from: balanceMinusFee) - } - private func providePayAssetViews() { providePayTitle() providePayAssetViewModel() @@ -250,6 +179,24 @@ final class SwapSetupPresenter: PurchaseFlowManaging { provideReceiveAmountInputViewModel() } + private func provideButtonState() { + let buttonState = viewModelFactory.buttonState( + assetIn: payChainAsset?.chainAssetId, + assetOut: receiveChainAsset?.chainAssetId, + amountIn: getPayAmount(for: payAmountInput), + amountOut: receiveAmountInput + ) + + view?.didReceiveButtonState( + title: buttonState.title.value(for: selectedLocale), + enabled: buttonState.enabled + ) + } + + private func provideSettingsState() { + view?.didReceiveSettingsState(isAvailable: payChainAsset != nil) + } + private func provideDetailsViewModel(isAvailable: Bool) { view?.didReceiveDetailsState(isAvailable: isAvailable) } @@ -291,50 +238,17 @@ final class SwapSetupPresenter: PurchaseFlowManaging { view?.didReceiveNetworkFee(viewModel: .loaded(value: viewModel)) } - private func provideErrors() { - guard let payAmount = getPayAmount(for: payAmountInput) else { - view?.didReceive(errors: []) - return - } - var errors: [SwapSetupViewError] = [] - let balanceMinusFee = balanceMinusFee() ?? 0 - - if payAmount > balanceMinusFee { - errors.append(.insufficientToken) - } - - view?.didReceive(errors: errors) - } - - func estimateFee() { - guard let quote = quote, - let accountId = accountId, - let quoteArgs = quoteArgs, - let slippage = slippage else { - return - } - - let args = AssetConversion.CallArgs( - assetIn: quote.assetIn, - amountIn: quote.amountIn, - assetOut: quote.assetOut, - amountOut: quote.amountOut, - receiver: accountId, - direction: quoteArgs.direction, - slippage: slippage - ) - - let newIdentifier = SwapSetupFeeIdentifier( - transactionId: args.identifier, - feeChainAssetId: feeChainAsset?.chainAssetId - ) + private func provideIssues() { + var issues: [SwapSetupViewError] = [] - guard newIdentifier != feeIdentifier else { - return + if + let payAmount = getPayAmount(for: payAmountInput), + let maxAmount = getMaxModel()?.calculate(), + payAmount > maxAmount { + issues.append(.insufficientToken) } - feeIdentifier = newIdentifier - interactor.calculateFee(args: args) + view?.didReceive(errors: issues) } func refreshQuote(direction: AssetConversion.Direction, forceUpdate: Bool = true) { @@ -344,7 +258,9 @@ final class SwapSetupPresenter: PurchaseFlowManaging { return } - quote = nil + if forceUpdate { + quote = nil + } switch direction { case .buy: @@ -413,60 +329,185 @@ final class SwapSetupPresenter: PurchaseFlowManaging { } } - private func balanceMinusFee() -> Decimal? { - guard let payChainAsset = payChainAsset else { + private func updateFeeChainAsset(_ chainAsset: ChainAsset?) { + feeChainAsset = chainAsset + providePayAssetViews() + interactor.update(feeChainAsset: chainAsset) + + fee = nil + provideFeeViewModel() + + estimateFee() + } + + // MARK: Base implementation + + override func getInputAmount() -> Decimal? { + guard let payAmountInput = payAmountInput else { return nil } - let balanceValue = payAssetBalance?.transferable ?? 0 - let feeValue = payChainAsset.chainAssetId == feeChainAsset?.chainAssetId ? fee?.totalFee.targetAmount : 0 - let precision = Int16(payChainAsset.asset.precision) + let maxAmount = getMaxModel()?.calculate() ?? 0 + return payAmountInput.absoluteValue(from: maxAmount) + } + + override func getPayChainAsset() -> ChainAsset? { + payChainAsset + } - guard - let balance = Decimal.fromSubstrateAmount(balanceValue, precision: precision), - let fee = Decimal.fromSubstrateAmount(feeValue ?? 0, precision: precision) else { - return 0 + override func getReceiveChainAsset() -> ChainAsset? { + receiveChainAsset + } + + override func getFeeChainAsset() -> ChainAsset? { + feeChainAsset + } + + override func shouldHandleQuote(for args: AssetConversion.QuoteArgs?) -> Bool { + quoteArgs == args + } + + override func shouldHandleFee(for feeIdentifier: TransactionFeeId, feeChainAssetId: ChainAssetId?) -> Bool { + self.feeIdentifier == SwapSetupFeeIdentifier(transactionId: feeIdentifier, feeChainAssetId: feeChainAssetId) + } + + override func estimateFee() { + guard let quote = quote, + let receiveChain = receiveChainAsset?.chain, + let accountId = selectedWallet.fetch(for: receiveChain.accountRequest())?.accountId, + let quoteArgs = quoteArgs, + let slippage = slippage else { + return } - return max(0, balance - fee) + let args = AssetConversion.CallArgs( + assetIn: quote.assetIn, + amountIn: quote.amountIn, + assetOut: quote.assetOut, + amountOut: quote.amountOut, + receiver: accountId, + direction: quoteArgs.direction, + slippage: slippage + ) + + let newIdentifier = SwapSetupFeeIdentifier( + transactionId: args.identifier, + feeChainAssetId: feeChainAsset?.chainAssetId + ) + + guard newIdentifier != feeIdentifier else { + return + } + + feeIdentifier = newIdentifier + interactor.calculateFee(args: args) } - private func handleAssetBalanceError(chainAssetId: ChainAssetId) { - switch chainAssetId { - case payChainAsset?.chainAssetId: - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.payChainAsset.map { self?.interactor.update(payChainAsset: $0) } + override func applySwapMax() { + payAmountInput = .rate(1) + providePayAssetViews() + refreshQuote(direction: .sell) + provideButtonState() + provideIssues() + } + + override func handleBaseError(_ error: SwapBaseError) { + handleBaseError( + error, + view: view, + interactor: interactor, + wireframe: wireframe, + locale: selectedLocale + ) + } + + override func handleNewQuote(_ quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) { + logger.debug("New quote: \(quote)") + + switch quoteArgs.direction { + case .buy: + let payAmount = payChainAsset.map { + Decimal.fromSubstrateAmount( + quote.amountIn, + precision: Int16($0.asset.precision) + ) ?? 0 } - case feeChainAsset?.chainAssetId: - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.feeChainAsset.map { self?.interactor.update(feeChainAsset: $0) } + payAmountInput = payAmount.map { .absolute($0) } + providePayAmountInputViewModel() + case .sell: + receiveAmountInput = receiveChainAsset.map { + Decimal.fromSubstrateAmount( + quote.amountOut, + precision: $0.asset.displayInfo.assetPrecision + ) ?? 0 } - default: - break + provideReceiveAmountInputViewModel() + provideReceiveInputPriceViewModel() } + + provideRateViewModel() + provideButtonState() + + estimateFee() } - private func handlePriceError(priceId: AssetModel.PriceId) { - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - guard let self = self else { - return - } - [self.payChainAsset, self.receiveChainAsset, self.feeChainAsset] - .compactMap { $0 } - .filter { $0.asset.priceId == priceId } - .forEach(self.interactor.remakePriceSubscription) + override func handleNewFee( + _: AssetConversion.FeeModel?, + transactionFeeId _: TransactionFeeId, + feeChainAssetId _: ChainAssetId? + ) { + provideFeeViewModel() + + if case .rate = payAmountInput { + providePayInputPriceViewModel() + providePayAmountInputViewModel() } + + provideButtonState() + provideIssues() } - private func updateFeeChainAsset(_ chainAsset: ChainAsset?) { - feeChainAsset = chainAsset - providePayAssetViews() - interactor.update(feeChainAsset: chainAsset) + override func handleNewPrice(_: PriceData?, chainAssetId: ChainAssetId) { + if payChainAsset?.chainAssetId == chainAssetId { + providePayInputPriceViewModel() + } - fee = nil - provideFeeViewModel() + if receiveChainAsset?.chainAssetId == chainAssetId { + provideReceiveInputPriceViewModel() + } - estimateFee() + if feeChainAsset?.chainAssetId == chainAssetId { + provideFeeViewModel() + } + } + + override func handleNewBalance(_: AssetBalance?, for chainAsset: ChainAssetId) { + if payChainAsset?.chainAssetId == chainAsset { + providePayTitle() + provideIssues() + + if case .rate = payAmountInput { + providePayInputPriceViewModel() + providePayAmountInputViewModel() + provideButtonState() + } + } + } + + override func handleNewBalanceExistense(_: AssetBalanceExistence, chainAssetId _: ChainAssetId) { + if case .rate = payAmountInput { + providePayInputPriceViewModel() + providePayAmountInputViewModel() + provideButtonState() + } + } + + override func handleNewAccountInfo(_: AccountInfo?, chainId _: ChainModel.Id) { + if case .rate = payAmountInput { + providePayInputPriceViewModel() + providePayAmountInputViewModel() + provideButtonState() + } } } @@ -479,7 +520,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideSettingsState() // TODO: get from settings slippage = .fraction(from: AssetConversionConstants.defaultSlippage)?.fromPercents() - provideErrors() + provideIssues() interactor.setup() interactor.update(payChainAsset: payChainAsset) @@ -501,7 +542,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { self?.provideButtonState() self?.provideSettingsState() self?.provideFeeViewModel() - self?.provideErrors() + self?.provideIssues() self?.interactor.update(payChainAsset: chainAsset) self?.interactor.update(feeChainAsset: feeChainAsset) @@ -538,13 +579,14 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { payAmountInput = amount.map { .absolute($0) } refreshQuote(direction: .sell) provideButtonState() - provideErrors() + provideIssues() } func updateReceiveAmount(_ amount: Decimal?) { receiveAmountInput = amount refreshQuote(direction: .buy) provideButtonState() + provideIssues() } func flip(currentFocus: TextFieldFocus?) { @@ -586,17 +628,13 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideButtonState() provideSettingsState() provideFeeViewModel() - provideErrors() + provideIssues() view?.didReceive(focus: newFocus) } func selectMaxPayAmount() { - payAmountInput = .rate(1) - providePayAssetViews() - refreshQuote(direction: .sell) - provideButtonState() - provideErrors() + applySwapMax() } func showFeeActions() { @@ -639,29 +677,23 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } func proceed() { - guard let payChainAsset = payChainAsset, - let feeChainAsset = feeChainAsset else { + guard let swapModel = getSwapModel() else { return } - let validators = validators( - spendingAmount: getPayAmount(for: payAmountInput), - payChainAsset: payChainAsset, - feeChainAsset: feeChainAsset - ) + let validators = getBaseValidations(for: swapModel, locale: selectedLocale) DataValidationRunner(validators: validators).runValidation { [weak self] in - guard let receiveChainAsset = self?.receiveChainAsset, - let slippage = self?.slippage, - let quote = self?.quote, + guard let slippage = self?.slippage, + let quote = swapModel.quote, let quoteArgs = self?.quoteArgs else { return } let confirmInitState = SwapConfirmInitState( - chainAssetIn: payChainAsset, - chainAssetOut: receiveChainAsset, - feeChainAsset: feeChainAsset, + chainAssetIn: swapModel.payChainAsset, + chainAssetOut: swapModel.receiveChainAsset, + feeChainAsset: swapModel.feeChainAsset, slippage: slippage, quote: quote, quoteArgs: quoteArgs @@ -692,7 +724,9 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } func depositInsufficientToken() { - guard let payChainAsset = payChainAsset, let accountId = accountId else { + guard + let payChainAsset = payChainAsset, + let accountId = selectedWallet.fetch(for: payChainAsset.chain.accountRequest())?.accountId else { return } @@ -701,12 +735,12 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { let crossChainSendAvailable = depositCrossChainAssets.first != nil && sendAvailable let recieveAvailable = TokenOperation.checkReceiveOperationAvailable( - walletType: selectedAccount.type, + walletType: selectedWallet.type, chainAsset: payChainAsset ).available let buyAvailable = TokenOperation.checkBuyOperationAvailable( purchaseActions: purchaseActions, - walletType: selectedAccount.type, + walletType: selectedWallet.type, chainAsset: payChainAsset ).available depositOperations = [ @@ -724,36 +758,6 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { - func didReceive(baseError: SwapBaseError) { - logger.error("Did receive base error: \(baseError)") - - switch baseError { - case let .quote(_, args): - guard args == quoteArgs else { - return - } - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.refreshQuote(direction: args.direction) - } - case let .fetchFeeFailed(_, id, feeChainAssetId): - let identifier = SwapSetupFeeIdentifier(transactionId: id, feeChainAssetId: feeChainAssetId) - guard identifier == feeIdentifier else { - return - } - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.estimateFee() - } - case let .price(_, priceId): - handlePriceError(priceId: priceId) - case let .assetBalance(_, chainAssetId, _): - handleAssetBalanceError(chainAssetId: chainAssetId) - case let .assetBalanceExistense(_, chainAsset): - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.interactor.retryAssetBalanceExistenseFetch(for: chainAsset) - } - } - } - func didReceive(setupError: SwapSetupError) { logger.error("Did receive setup error: \(setupError)") @@ -768,112 +772,17 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in self?.interactor.setupXcm() } - } - } - - func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) { - guard quoteArgs == self.quoteArgs else { - return - } - - self.quote = quote - - switch quoteArgs.direction { - case .buy: - let payAmount = payChainAsset.map { - Decimal.fromSubstrateAmount( - quote.amountIn, - precision: Int16($0.asset.precision) - ) ?? 0 - } - payAmountInput = payAmount.map { .absolute($0) } - providePayAmountInputViewModel() - case .sell: - receiveAmountInput = receiveChainAsset.map { - Decimal.fromSubstrateAmount( - quote.amountOut, - precision: $0.asset.displayInfo.assetPrecision - ) ?? 0 + case .blockNumber: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryBlockNumberSubscription() } - provideReceiveAmountInputViewModel() - provideReceiveInputPriceViewModel() - } - - provideRateViewModel() - estimateFee() - provideButtonState() - } - - func didReceive( - fee: AssetConversion.FeeModel?, - transactionId: TransactionFeeId, - feeChainAssetId: FeeChainAssetId? - ) { - let identifier = SwapSetupFeeIdentifier( - transactionId: transactionId, - feeChainAssetId: feeChainAssetId - ) - - guard identifier == feeIdentifier else { - return - } - - self.fee = fee - - provideFeeViewModel() - - if case .rate = payAmountInput { - providePayInputPriceViewModel() - providePayAmountInputViewModel() - } - - provideButtonState() - provideErrors() - } - - func didReceive(price: PriceData?, priceId: AssetModel.PriceId) { - if let payChainAsset = payChainAsset, priceId == payChainAsset.asset.priceId { - prices[payChainAsset.chainAssetId] = price - providePayInputPriceViewModel() - } - - if let receiveChainAsset = receiveChainAsset, priceId == receiveChainAsset.asset.priceId { - prices[receiveChainAsset.chainAssetId] = price - provideReceiveInputPriceViewModel() - } - - if let feeChainAsset = feeChainAsset, priceId == feeChainAsset.asset.priceId { - prices[feeChainAsset.chainAssetId] = price - provideFeeViewModel() - } - } - - func didReceive(payAccountId: AccountId?) { - accountId = payAccountId - provideErrors() - } - - func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId, accountId _: AccountId) { - balances[chainAsset] = balance - - if chainAsset == payChainAsset?.chainAssetId { - providePayTitle() - provideErrors() - - if case .rate = payAmountInput { - providePayInputPriceViewModel() - providePayAmountInputViewModel() - provideButtonState() + case .remoteSubscription: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryRemoteSubscription() } } } - func didReceiveAssetBalance(existense: AssetBalanceExistence, chainAssetId: ChainAssetId) { - logger.debug("Did receive existense for \(chainAssetId.stringValue): \(String(existense.minBalance))") - - assetBalanceExistences[chainAssetId] = existense - } - func didReceiveCanPayFeeInPayAsset(_ value: Bool, chainAssetId: ChainAssetId) { if payChainAsset?.chainAssetId == chainAssetId { canPayFeeInPayAsset = value @@ -886,6 +795,13 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { depositCrossChainAssets = origins self.xcmTransfers = xcmTransfers } + + func didReceiveBlockNumber(_ blockNumber: BlockNumber?, chainId _: ChainModel.Id) { + logger.debug("New block number: \(String(describing: blockNumber))") + + refreshQuote(direction: quoteArgs?.direction ?? .sell, forceUpdate: false) + estimateFee() + } } extension SwapSetupPresenter: Localizable { @@ -913,7 +829,7 @@ extension SwapSetupPresenter: ModalPickerViewControllerDelegate { ) case .receive: guard let payChainAsset = payChainAsset, - let metaChainAccountResponse = selectedAccount.fetchMetaChainAccount( + let metaChainAccountResponse = selectedWallet.fetchMetaChainAccount( for: payChainAsset.chain.accountRequest() ) else { return @@ -925,12 +841,13 @@ extension SwapSetupPresenter: ModalPickerViewControllerDelegate { ) case .send: guard let payChainAsset = payChainAsset, - let accountId = accountId, - let address = try? accountId.toAddress(using: payChainAsset.chain.chainFormat), + let accountId = selectedWallet.fetch(for: payChainAsset.chain.accountRequest()), + let address = accountId.toAddress(), let origin = depositCrossChainAssets.first, let xcmTransfers = xcmTransfers else { return } + wireframe.showDepositTokensBySend( from: view, origin: origin, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 919e764980..8920629528 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -41,11 +41,14 @@ protocol SwapSetupInteractorInputProtocol: SwapBaseInteractorInputProtocol { func update(payChainAsset: ChainAsset?) func update(feeChainAsset: ChainAsset?) func setupXcm() + func retryRemoteSubscription() + func retryBlockNumberSubscription() } protocol SwapSetupInteractorOutputProtocol: SwapBaseInteractorOutputProtocol { func didReceiveAvailableXcm(origins: [ChainAsset], xcmTransfers: XcmTransfers?) func didReceiveCanPayFeeInPayAsset(_ value: Bool, chainAssetId: ChainAssetId) + func didReceiveBlockNumber(_ blockNumber: BlockNumber?, chainId: ChainModel.Id) func didReceive(setupError: SwapSetupError) } @@ -102,6 +105,8 @@ protocol SwapSetupWireframeProtocol: SwapBaseWireframeProtocol, ShortTextInfoPre enum SwapSetupError: Error { case xcm(Error) case payAssetSetFailed(Error) + case remoteSubscription(Error) + case blockNumber(Error) } enum SwapSetupViewError { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index 6c958b1e3b..f2502d6286 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -16,11 +16,22 @@ struct SwapSetupViewFactory { let balanceViewModelFactoryFacade = BalanceViewModelFactoryFacade( priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager)) - guard let interactor = createInteractor() else { + let generalLocalSubscriptionFactory = GeneralStorageSubscriptionFactory( + chainRegistry: ChainRegistryFacade.sharedRegistry, + storageFacade: SubstrateDataStorageFacade.shared, + operationManager: OperationManager(operationQueue: OperationManagerFacade.sharedDefaultQueue), + logger: Logger.shared + ) + + guard let interactor = createInteractor(with: generalLocalSubscriptionFactory) else { return nil } - let wireframe = SwapSetupWireframe(assetListObservable: assetListObservable) + let wireframe = SwapSetupWireframe( + assetListObservable: assetListObservable, + state: generalLocalSubscriptionFactory + ) + let viewModelFactory = SwapsSetupViewModelFactory( balanceViewModelFactoryFacade: balanceViewModelFactoryFacade, networkViewModelFactory: NetworkViewModelFactory(), @@ -39,7 +50,7 @@ struct SwapSetupViewFactory { viewModelFactory: viewModelFactory, dataValidatingFactory: dataValidatingFactory, localizationManager: LocalizationManager.shared, - selectedAccount: selectedWallet, + selectedWallet: selectedWallet, purchaseProvider: PurchaseAggregator.defaultAggregator(), logger: Logger.shared ) @@ -56,7 +67,9 @@ struct SwapSetupViewFactory { return view } - private static func createInteractor() -> SwapSetupInteractor? { + private static func createInteractor( + with generalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol + ) -> SwapSetupInteractor? { guard let currencyManager = CurrencyManager.shared, let selectedWallet = SelectedWalletSettings.shared.value else { return nil @@ -94,6 +107,8 @@ struct SwapSetupViewFactory { assetStorageFactory: assetStorageFactory, priceLocalSubscriptionFactory: PriceProviderFactory.shared, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + generalLocalSubscriptionFactory: generalSubscriptionFactory, + storageRepository: SubstrateRepositoryFactory().createChainStorageItemRepository(), currencyManager: currencyManager, selectedWallet: selectedWallet, operationQueue: operationQueue diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index 30c1a2bd41..c7ca661494 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -4,9 +4,11 @@ import SoraUI final class SwapSetupWireframe: SwapSetupWireframeProtocol { let assetListObservable: AssetListModelObservable + let state: GeneralStorageSubscriptionFactoryProtocol - init(assetListObservable: AssetListModelObservable) { + init(assetListObservable: AssetListModelObservable, state: GeneralStorageSubscriptionFactoryProtocol) { self.assetListObservable = assetListObservable + self.state = state } func showPayTokenSelection( @@ -74,7 +76,8 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { initState: SwapConfirmInitState ) { guard let confimView = SwapConfirmViewFactory.createView( - initState: initState + initState: initState, + generalSubscriptonFactory: state ) else { return } From 48c3369c8e337313f7cd29e4a1c013b11ce2eea9 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 6 Nov 2023 18:18:52 +0100 Subject: [PATCH 123/204] add quote validations --- novawallet.xcodeproj/project.pbxproj | 6 +- .../Foundation/Decimal+Conversion.swift | 17 ++++ .../Extension/Foundation/String+Helpers.swift | 4 + ...nceViewModelFactoryFacade+Formatting.swift | 19 ++++ .../BalanceViewModelFactoryFacade.swift | 0 .../Model/AssetConversion.swift | 13 +++ .../Swaps/Base/SwapBaseInteractor.swift | 19 ++++ .../Swaps/Base/SwapBasePresenter.swift | 30 ++++++- .../Swaps/Base/SwapBaseProtocols.swift | 4 + .../Model/SwapConfirmViewModelFactory.swift | 21 ++--- .../Swaps/Confirm/SwapConfirmPresenter.swift | 16 +--- .../Model/SwapsSetupViewModelFactory.swift | 21 ++--- .../Swaps/Setup/SwapSetupPresenter.swift | 12 ++- .../Validation/SwapDataValidatorFactory.swift | 87 +++++++++++++++++-- .../Validation/SwapErrorPresentable.swift | 41 +++++++++ .../Modules/Swaps/Validation/SwapModel.swift | 41 +++++++++ novawallet/en.lproj/Localizable.strings | 4 +- 17 files changed, 302 insertions(+), 53 deletions(-) create mode 100644 novawallet/Common/ViewModel/BalanceViewModelFactoryFacade+Formatting.swift rename novawallet/{Modules/Vote/Crowdloan/CrowdloanList => Common/ViewModel}/BalanceViewModelFactoryFacade.swift (100%) diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 8ecc084cc6..4d8a6cf739 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -73,6 +73,7 @@ 0C13DFCD2AF8A5A300E5F355 /* SwapMaxModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFCC2AF8A5A300E5F355 /* SwapMaxModel.swift */; }; 0C13DFCF2AF8ADB300E5F355 /* BigUInt+Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFCE2AF8ADB300E5F355 /* BigUInt+Operation.swift */; }; 0C13DFD12AF8AE3E00E5F355 /* SwapBasePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFD02AF8AE3E00E5F355 /* SwapBasePresenter.swift */; }; + 0C13DFD32AF949B400E5F355 /* BalanceViewModelFactoryFacade+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFD22AF949B400E5F355 /* BalanceViewModelFactoryFacade+Formatting.swift */; }; 0C154A0E2A45995500932C3F /* CompoundComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C154A0D2A45995500932C3F /* CompoundComparator.swift */; }; 0C17BD972A42F162004AF9E7 /* WalletViewModelObserverContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */; }; 0C17BD992A42F1BE004AF9E7 /* MoneyPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */; }; @@ -4130,6 +4131,7 @@ 0C13DFCC2AF8A5A300E5F355 /* SwapMaxModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapMaxModel.swift; sourceTree = ""; }; 0C13DFCE2AF8ADB300E5F355 /* BigUInt+Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BigUInt+Operation.swift"; sourceTree = ""; }; 0C13DFD02AF8AE3E00E5F355 /* SwapBasePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapBasePresenter.swift; sourceTree = ""; }; + 0C13DFD22AF949B400E5F355 /* BalanceViewModelFactoryFacade+Formatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BalanceViewModelFactoryFacade+Formatting.swift"; sourceTree = ""; }; 0C154A0D2A45995500932C3F /* CompoundComparator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompoundComparator.swift; sourceTree = ""; }; 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletViewModelObserverContainer.swift; sourceTree = ""; }; 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyPresentable.swift; sourceTree = ""; }; @@ -14015,6 +14017,8 @@ 0C1338112AB8330D0036BCD6 /* AnimatedImageViewModel.swift */, 0C1338132AB834750036BCD6 /* QRImageViewModelFactory.swift */, 0C9951D22AE2DB0200B65615 /* PromotionViewModelFactory.swift */, + 8828F4F428AD2763009E0B7C /* BalanceViewModelFactoryFacade.swift */, + 0C13DFD22AF949B400E5F355 /* BalanceViewModelFactoryFacade+Formatting.swift */, ); path = ViewModel; sourceTree = ""; @@ -17192,7 +17196,6 @@ E4E78D69E8EBC3EB4D01F8EF /* CrowdloanListInteractor.swift */, 84B66A0A26FDB70F0038B963 /* CrowdloansListInteractor+Protocols.swift */, 8828F4F228AD2734009E0B7C /* CrowdloansCalculator.swift */, - 8828F4F428AD2763009E0B7C /* BalanceViewModelFactoryFacade.swift */, 8442001F28E6FDBE00C49C4A /* CrowdloanListViewManager.swift */, ); path = CrowdloanList; @@ -21299,6 +21302,7 @@ 2AC7BC7E2731604C001D99B0 /* ChainAccountChanged.swift in Sources */, 845B07F329159C15005785D3 /* Democracy+CodingPath.swift in Sources */, 847C96492553614F002D288F /* ExportRestoreJsonPresenter.swift in Sources */, + 0C13DFD32AF949B400E5F355 /* BalanceViewModelFactoryFacade+Formatting.swift in Sources */, 84A04622277DE83E000B24DA /* DAppListErrorView.swift in Sources */, 6D315EFF2B664235D297674E /* AccountImportProtocols.swift in Sources */, F43A8A9E265BA03500A8A5A8 /* CustomValidatorListViewModel.swift in Sources */, diff --git a/novawallet/Common/Extension/Foundation/Decimal+Conversion.swift b/novawallet/Common/Extension/Foundation/Decimal+Conversion.swift index 3b30f2b135..ccf5704b6b 100644 --- a/novawallet/Common/Extension/Foundation/Decimal+Conversion.swift +++ b/novawallet/Common/Extension/Foundation/Decimal+Conversion.swift @@ -1,5 +1,6 @@ import Foundation import BigInt +import SubstrateSdk extension Decimal { static func fromSubstratePercent(value: UInt8) -> Decimal? { @@ -23,4 +24,20 @@ extension Decimal { return rounded } + + static func rateFromSubstrate( + amount1: BigUInt, + amount2: BigUInt, + precision1: Int16, + precision2: Int16 + ) -> Decimal? { + guard + let decimal1 = fromSubstrateAmount(amount1, precision: precision1), + let decimal2 = fromSubstrateAmount(amount2, precision: precision2), + decimal2 > 0 else { + return nil + } + + return decimal2 / decimal1 + } } diff --git a/novawallet/Common/Extension/Foundation/String+Helpers.swift b/novawallet/Common/Extension/Foundation/String+Helpers.swift index 334db1c85d..70211bed9e 100644 --- a/novawallet/Common/Extension/Foundation/String+Helpers.swift +++ b/novawallet/Common/Extension/Foundation/String+Helpers.swift @@ -45,6 +45,10 @@ extension String { return "(\(self))" } + + func estimatedEqual(to other: String) -> String { + "\(self) ≈ \(other)" + } } extension Optional where Wrapped == String { diff --git a/novawallet/Common/ViewModel/BalanceViewModelFactoryFacade+Formatting.swift b/novawallet/Common/ViewModel/BalanceViewModelFactoryFacade+Formatting.swift new file mode 100644 index 0000000000..d3278b107b --- /dev/null +++ b/novawallet/Common/ViewModel/BalanceViewModelFactoryFacade+Formatting.swift @@ -0,0 +1,19 @@ +import Foundation +import SoraFoundation + +extension BalanceViewModelFactoryFacadeProtocol { + func rateFromValue( + mainSymbol: String, + targetAssetInfo: AssetBalanceDisplayInfo, + value: Decimal + ) -> LocalizableResource { + let targetString = amountFromValue( + targetAssetInfo: targetAssetInfo, + value: value + ) + + return LocalizableResource { locale in + "1 \(mainSymbol)".estimatedEqual(to: targetString.value(for: locale)) + } + } +} diff --git a/novawallet/Modules/Vote/Crowdloan/CrowdloanList/BalanceViewModelFactoryFacade.swift b/novawallet/Common/ViewModel/BalanceViewModelFactoryFacade.swift similarity index 100% rename from novawallet/Modules/Vote/Crowdloan/CrowdloanList/BalanceViewModelFactoryFacade.swift rename to novawallet/Common/ViewModel/BalanceViewModelFactoryFacade.swift diff --git a/novawallet/Modules/AssetConversion/Model/AssetConversion.swift b/novawallet/Modules/AssetConversion/Model/AssetConversion.swift index aefbd794a0..0c1998cf5c 100644 --- a/novawallet/Modules/AssetConversion/Model/AssetConversion.swift +++ b/novawallet/Modules/AssetConversion/Model/AssetConversion.swift @@ -33,6 +33,19 @@ enum AssetConversion { assetIn = args.assetIn assetOut = args.assetOut } + + func matches(other quote: Quote, slippage: BigRational, direction: Direction) -> Bool { + switch direction { + case .sell: + let amountOutMin = amountOut - slippage.mul(value: amountOut) + + return amountOutMin <= quote.amountOut + case .buy: + let amountInMax = amountIn + slippage.mul(value: amountIn) + + return amountInMax >= quote.amountIn + } + } } struct CallArgs: Hashable { diff --git a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift index 8760208e2d..bdbe846460 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift @@ -291,6 +291,25 @@ class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapB updateAccountInfoProvider(for: chain) } + func requestValidatingQuote( + for args: AssetConversion.QuoteArgs, + completion: @escaping (Result) -> Void + ) { + guard let chain = currentChain else { + completion(.failure(ChainRegistryError.connectionUnavailable)) + return + } + + let wrapper = assetConversionAggregator.createQuoteWrapper(for: chain, args: args) + + execute( + wrapper: wrapper, + inOperationQueue: operationQueue, + runningCallbackIn: .main, + callbackClosure: completion + ) + } + // MARK: Overridable General Subscription Handlers func handleBlockNumber( diff --git a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift index 4d062b7654..8a1a970f8b 100644 --- a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift +++ b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift @@ -79,7 +79,9 @@ class SwapBasePresenter { guard let payChainAsset = getPayChainAsset(), let receiveChainAsset = getReceiveChainAsset(), - let feeChainAsset = getFeeChainAsset() else { + let feeChainAsset = getFeeChainAsset(), + let quoteArgs = getQuoteArgs(), + let slippage = getSlippage() else { return nil } @@ -97,7 +99,9 @@ class SwapBasePresenter { feeAssetExistense: feeAssetBalanceExistense, utilityAssetExistense: utilityAssetBalanceExistense, feeModel: fee, + quoteArgs: quoteArgs, quote: quote, + slippage: slippage, accountInfo: accountInfo ) } @@ -118,6 +122,14 @@ class SwapBasePresenter { fatalError("Must be implemented by parent class") } + func getQuoteArgs() -> AssetConversion.QuoteArgs? { + fatalError("Must be implemented by parent class") + } + + func getSlippage() -> BigRational? { + fatalError("Must be implemented by parent class") + } + func getPayChainAsset() -> ChainAsset? { fatalError("Must be implemented by parent class") } @@ -221,7 +233,11 @@ class SwapBasePresenter { } } - func getBaseValidations(for swapModel: SwapModel, locale: Locale) -> [DataValidating] { + func getBaseValidations( + for swapModel: SwapModel, + interactor: SwapBaseInteractorInputProtocol, + locale: Locale + ) -> [DataValidating] { [ dataValidatingFactory.hasInPlank( fee: swapModel.feeModel?.totalFee.targetAmount, @@ -250,6 +266,16 @@ class SwapBasePresenter { self?.applySwapMax() }, locale: locale + ), + dataValidatingFactory.passesRealtimeQuoteValidation( + params: swapModel, + remoteValidatingClosure: { [weak self] args, completion in + interactor.requestValidatingQuote(for: args, completion: completion) + }, + onQuoteUpdate: { [weak self] quote in + self?.quote = quote + }, + locale: locale ) ] } diff --git a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift index 03df66b5f9..744127e863 100644 --- a/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift +++ b/novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift @@ -8,6 +8,10 @@ protocol SwapBaseInteractorInputProtocol: AnyObject { func retryAssetBalanceSubscription(for chainAsset: ChainAsset) func retryAssetBalanceExistenseFetch(for chainAsset: ChainAsset) func retryAccountInfoSubscription() + func requestValidatingQuote( + for args: AssetConversion.QuoteArgs, + completion: @escaping (Result) -> Void + ) } protocol SwapBaseInteractorOutputProtocol: AnyObject { diff --git a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift index d01d1d16b3..243f08e45e 100644 --- a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift @@ -77,30 +77,25 @@ extension SwapConfirmViewModelFactory: SwapConfirmViewModelFactoryProtocol { func rateViewModel(from params: RateParams) -> String { guard - let amountOutDecimal = Decimal.fromSubstrateAmount( - params.amountOut, - precision: params.assetDisplayInfoOut.assetPrecision - ), - let amountInDecimal = Decimal.fromSubstrateAmount( - params.amountIn, - precision: params.assetDisplayInfoIn.assetPrecision - ), - amountInDecimal != 0 else { + let rate = Decimal.rateFromSubstrate( + amount1: params.amountIn, + amount2: params.amountOut, + precision1: params.assetDisplayInfoIn.assetPrecision, + precision2: params.assetDisplayInfoOut.assetPrecision + ) else { return "" } - let difference = amountOutDecimal / amountInDecimal - let amountIn = balanceViewModelFactoryFacade.amountFromValue( targetAssetInfo: params.assetDisplayInfoIn, value: 1 ).value(for: locale) let amountOut = balanceViewModelFactoryFacade.amountFromValue( targetAssetInfo: params.assetDisplayInfoOut, - value: difference + value: rate ).value(for: locale) - return "\(amountIn) ≈ \(amountOut)" + return amountIn.estimatedEqual(to: amountOut) } func slippageViewModel(slippage: BigRational) -> String { diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index d9819d1ce6..103fb7f3d3 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -199,18 +199,6 @@ final class SwapConfirmPresenter { spendingAmount: spendingAmount, asset: initState.feeChainAsset.assetDisplayInfo, locale: selectedLocale - ), - dataValidatingFactory.has( - quote: quote, - payChainAssetId: initState.chainAssetIn.chainAssetId, - receiveChainAssetId: initState.chainAssetOut.chainAssetId, - locale: selectedLocale, - onError: { [weak self] in - guard let self = self else { - return - } - self.interactor.calculateQuote(for: self.initState.quoteArgs) - } ) ] @@ -260,10 +248,10 @@ final class SwapConfirmPresenter { let oldRate = viewModelFactory.rateViewModel(from: oldRateParams) let newRate = viewModelFactory.rateViewModel(from: newRateParams) - let title = R.string.localizable.swapsSetupErrorRateWasUpdatedTitle( + let title = R.string.localizable.swapsErrorRateWasUpdatedTitle( preferredLanguages: selectedLocale.rLanguages ) - let message = R.string.localizable.swapsSetupErrorRateWasUpdatedMessage( + let message = R.string.localizable.swapsErrorRateWasUpdatedMessage( oldRate, newRate, preferredLanguages: selectedLocale.rLanguages diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift index 00639ee2c3..fadea27b6e 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift @@ -211,30 +211,25 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { func rateViewModel(from params: RateParams) -> String { guard - let amountOutDecimal = Decimal.fromSubstrateAmount( - params.amountOut, - precision: params.assetDisplayInfoOut.assetPrecision - ), - let amountInDecimal = Decimal.fromSubstrateAmount( - params.amountIn, - precision: params.assetDisplayInfoIn.assetPrecision - ), - amountInDecimal != 0 else { + let rate = Decimal.rateFromSubstrate( + amount1: params.amountIn, + amount2: params.amountOut, + precision1: params.assetDisplayInfoIn.assetPrecision, + precision2: params.assetDisplayInfoOut.assetPrecision + ) else { return "" } - let difference = amountOutDecimal / amountInDecimal - let amountIn = balanceViewModelFactoryFacade.amountFromValue( targetAssetInfo: params.assetDisplayInfoIn, value: 1 ).value(for: locale) let amountOut = balanceViewModelFactoryFacade.amountFromValue( targetAssetInfo: params.assetDisplayInfoOut, - value: difference + value: rate ).value(for: locale) - return "\(amountIn) ≈ \(amountOut)" + return amountIn.estimatedEqual(to: amountOut) } func amountInputViewModel( diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 598c3d8238..6fcfde7f80 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -363,6 +363,14 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { feeChainAsset } + override func getQuoteArgs() -> AssetConversion.QuoteArgs? { + quoteArgs + } + + override func getSlippage() -> BigRational? { + slippage + } + override func shouldHandleQuote(for args: AssetConversion.QuoteArgs?) -> Bool { quoteArgs == args } @@ -681,11 +689,11 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { return } - let validators = getBaseValidations(for: swapModel, locale: selectedLocale) + let validators = getBaseValidations(for: swapModel, interactor: interactor, locale: selectedLocale) DataValidationRunner(validators: validators).runValidation { [weak self] in guard let slippage = self?.slippage, - let quote = swapModel.quote, + let quote = self?.quote, let quoteArgs = self?.quoteArgs else { return } diff --git a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift index 4603002e02..e04ed71d44 100644 --- a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift +++ b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift @@ -17,12 +17,11 @@ protocol SwapDataValidatorFactoryProtocol: BaseDataValidatingFactoryProtocol { locale: Locale ) -> DataValidating - func has( - quote: AssetConversion.Quote?, - payChainAssetId: ChainAssetId?, - receiveChainAssetId: ChainAssetId?, - locale: Locale, - onError: (() -> Void)? + func passesRealtimeQuoteValidation( + params: SwapModel, + remoteValidatingClosure: @escaping ((AssetConversion.QuoteArgs, @escaping SwapModel.QuoteValidateClosure) -> Void), + onQuoteUpdate: @escaping (AssetConversion.Quote) -> Void, + locale: Locale ) -> DataValidating } @@ -253,6 +252,82 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { }) } + // swiftlint:disable:next function_body_length + func passesRealtimeQuoteValidation( + params: SwapModel, + remoteValidatingClosure: @escaping ((AssetConversion.QuoteArgs, @escaping SwapModel.QuoteValidateClosure) -> Void), + onQuoteUpdate: @escaping (AssetConversion.Quote) -> Void, + locale: Locale + ) -> DataValidating { + var reason: SwapModel.InvalidQuoteReason? + + return AsyncWarningConditionViolation( + onWarning: { [weak self] delegate in + guard + let reason = reason, + let view = self?.view, + let viewModelFactory = self?.balanceViewModelFactoryFacade else { + return + } + + switch reason { + case let .rateChange(rateUpdate): + onQuoteUpdate(rateUpdate.newQuote) + + let oldRate = Decimal.rateFromSubstrate( + amount1: rateUpdate.oldQuote.amountIn, + amount2: rateUpdate.oldQuote.amountOut, + precision1: params.payChainAsset.assetDisplayInfo.assetPrecision, + precision2: params.receiveChainAsset.assetDisplayInfo.assetPrecision + ) ?? 0 + + let oldRateString = viewModelFactory.rateFromValue( + mainSymbol: params.payChainAsset.asset.symbol, + targetAssetInfo: params.receiveChainAsset.assetDisplayInfo, + value: oldRate + ).value(for: locale) + + let newRate = Decimal.rateFromSubstrate( + amount1: rateUpdate.newQuote.amountIn, + amount2: rateUpdate.newQuote.amountOut, + precision1: params.payChainAsset.assetDisplayInfo.assetPrecision, + precision2: params.receiveChainAsset.assetDisplayInfo.assetPrecision + ) ?? 0 + + let newRateString = viewModelFactory.rateFromValue( + mainSymbol: params.payChainAsset.asset.symbol, + targetAssetInfo: params.receiveChainAsset.assetDisplayInfo, + value: newRate + ).value(for: locale) + + self?.presentable.presentRateUpdated( + from: view, + oldRate: oldRateString, + newRate: newRateString, + onConfirm: { + delegate.didCompleteAsyncHandling() + }, + locale: locale + ) + + case .noLiqudity: + self?.presentable.presentNotEnoughLiquidity( + from: view, + locale: locale + ) + } + }, + preservesCondition: { preservationCallback in + params.asyncCheckQuoteValidity(remoteValidatingClosure) { result in + let preserves = result == nil + reason = result + + preservationCallback(preserves) + } + } + ) + } + func has( quote: AssetConversion.Quote?, payChainAssetId: ChainAssetId?, diff --git a/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift b/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift index fc13575cdc..06f50b700a 100644 --- a/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift +++ b/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift @@ -3,6 +3,14 @@ import Foundation protocol SwapErrorPresentable: BaseErrorPresentable { func presentNotEnoughLiquidity(from view: ControllerBackedProtocol, locale: Locale?) + func presentRateUpdated( + from view: ControllerBackedProtocol, + oldRate: String, + newRate: String, + onConfirm: @escaping () -> Void, + locale: Locale? + ) + func presentInsufficientBalance( from view: ControllerBackedProtocol?, reason: SwapDisplayError.InsufficientBalance, @@ -42,6 +50,39 @@ extension SwapErrorPresentable where Self: AlertPresentable & ErrorPresentable { present(message: nil, title: title, closeAction: closeAction, from: view) } + func presentRateUpdated( + from view: ControllerBackedProtocol, + oldRate: String, + newRate: String, + onConfirm: @escaping () -> Void, + locale: Locale? + ) { + let title = R.string.localizable.swapsErrorRateWasUpdatedTitle(preferredLanguages: locale?.rLanguages) + let message = R.string.localizable.swapsErrorRateWasUpdatedMessage( + oldRate, + newRate, + preferredLanguages: locale?.rLanguages + ) + + let cancelAction = AlertPresentableAction( + title: R.string.localizable.commonCancel(preferredLanguages: locale?.rLanguages) + ) + + let confirmAction = AlertPresentableAction( + title: R.string.localizable.commonConfirm(preferredLanguages: locale?.rLanguages), + handler: onConfirm + ) + + let viewModel = AlertPresentableViewModel( + title: title, + message: message, + actions: [cancelAction, confirmAction], + closeAction: nil + ) + + present(viewModel: viewModel, style: .alert, from: view) + } + func presentNoProviderForNonSufficientToken( from view: ControllerBackedProtocol, utilityMinBalance: String, diff --git a/novawallet/Modules/Swaps/Validation/SwapModel.swift b/novawallet/Modules/Swaps/Validation/SwapModel.swift index f2af92ca02..b12f56ee87 100644 --- a/novawallet/Modules/Swaps/Validation/SwapModel.swift +++ b/novawallet/Modules/Swaps/Validation/SwapModel.swift @@ -61,6 +61,18 @@ struct SwapModel { case noProvider(CannotReceiveDueNoProviders) } + struct InvalidQuoteDueRateChange { + let oldQuote: AssetConversion.Quote + let newQuote: AssetConversion.Quote + } + + enum InvalidQuoteReason { + case rateChange(InvalidQuoteDueRateChange) + case noLiqudity + } + + typealias QuoteValidateClosure = (Result) -> Void + let payChainAsset: ChainAsset let receiveChainAsset: ChainAsset let feeChainAsset: ChainAsset @@ -74,7 +86,9 @@ struct SwapModel { let feeAssetExistense: AssetBalanceExistence? let utilityAssetExistense: AssetBalanceExistence? let feeModel: AssetConversion.FeeModel? + let quoteArgs: AssetConversion.QuoteArgs let quote: AssetConversion.Quote? + let slippage: BigRational let accountInfo: AccountInfo? var utilityChainAsset: ChainAsset? { @@ -251,4 +265,31 @@ struct SwapModel { ) } } + + func asyncCheckQuoteValidity( + _ newQuoteClosure: @escaping (AssetConversion.QuoteArgs, @escaping QuoteValidateClosure) -> Void, + completion: @escaping (InvalidQuoteReason?) -> Void + ) { + guard let currenQuote = quote else { + completion(.noLiqudity) + return + } + + newQuoteClosure(quoteArgs) { result in + switch result { + case let .success(newQuote): + if !currenQuote.matches( + other: newQuote, + slippage: slippage, + direction: quoteArgs.direction + ) { + completion(.rateChange(.init(oldQuote: currenQuote, newQuote: newQuote))) + } else { + completion(nil) + } + case .failure: + completion(.noLiqudity) + } + } + } } diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 5daf19bb84..cbc0bc283e 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1409,8 +1409,8 @@ "swaps.setup.network.fee.token.title" = "Token for paying network fee"; "swaps.setup.network.fee.token.hint" = "Network fee is added on top of entered amount"; "swaps.setup.price.difference.description" = "Price difference refers to the difference in price between two different assets. When making a swap in crypto, the price difference is usually the difference between the price of the asset you are swapping for and the price of the asset you are swapping with."; -"swaps.setup.error.rate.was.updated.title" = "Swap rate was updated"; -"swaps.setup.error.rate.was.updated.message" = "Old rate: %@.\nNew rate: %@"; +"swaps.error.rate.was.updated.title" = "Swap rate was updated"; +"swaps.error.rate.was.updated.message" = "Old rate: %@.\nNew rate: %@"; "swaps.setup.deposit.by.cross.chain.transfer.title" = "Cross-chain transfer"; "swaps.setup.deposit.by.cross.chain.transfer.subtitle" = "Transfer %@ from another network"; "swaps.setup.deposit.by.receive.subtitle" = "Receive %@ with QR or your address"; From 12454c80c4cd4d4cbaa472164f51dc1b0613eb27 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 7 Nov 2023 07:26:23 +0100 Subject: [PATCH 124/204] add inline style for erros --- .../colorBorderError.colorset/Contents.json | 20 ++++ .../iconInfoAccent.imageset/Contents.json | 12 +++ .../iconInfoAccent.pdf | Bin 0 -> 1643 bytes .../UIKit/Style/RoundedView+Style.swift | 10 ++ novawallet/Common/View/InlineAlertView.swift | 8 ++ .../Swaps/Setup/SwapSetupPresenter.swift | 10 +- .../Swaps/Setup/SwapSetupProtocols.swift | 7 +- .../Swaps/Setup/SwapSetupViewController.swift | 35 +++++- .../Setup/View/SwapAmountInputView.swift | 34 +++++- .../Setup/View/SwapSetupViewLayout.swift | 101 ++++++++++++++++++ .../Validation/SwapDataValidatorFactory.swift | 30 +----- novawallet/en.lproj/Localizable.strings | 2 + novawallet/ru.lproj/Localizable.strings | 2 + 13 files changed, 235 insertions(+), 36 deletions(-) create mode 100644 novawallet/Assets.xcassets/colors/border/colorBorderError.colorset/Contents.json create mode 100644 novawallet/Assets.xcassets/iconInfoAccent.imageset/Contents.json create mode 100644 novawallet/Assets.xcassets/iconInfoAccent.imageset/iconInfoAccent.pdf diff --git a/novawallet/Assets.xcassets/colors/border/colorBorderError.colorset/Contents.json b/novawallet/Assets.xcassets/colors/border/colorBorderError.colorset/Contents.json new file mode 100644 index 0000000000..67981ae6d0 --- /dev/null +++ b/novawallet/Assets.xcassets/colors/border/colorBorderError.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x50", + "green" : "0x34", + "red" : "0xE5" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconInfoAccent.imageset/Contents.json b/novawallet/Assets.xcassets/iconInfoAccent.imageset/Contents.json new file mode 100644 index 0000000000..f01eaf76da --- /dev/null +++ b/novawallet/Assets.xcassets/iconInfoAccent.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "iconInfoAccent.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconInfoAccent.imageset/iconInfoAccent.pdf b/novawallet/Assets.xcassets/iconInfoAccent.imageset/iconInfoAccent.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a83adb0b77bf12107819b448e7c3ddef8fc62546 GIT binary patch literal 1643 zcmZXVO>f&U42JLe6}%K^2UOO#ZJ;Qyrt2_l!@6|0Vh7Jr+YE`l+3wKw*N>EC*~xTL z9#P~=(qc|-uW!yJ&l!^-p#AZa0bE?bLHe0nZYtS+ExhGY&zLIa(!GNqCA_qspi$ z#Bsv9(VkpIiOYyHs@be;rgK)aznLxKKCvA0Dhp)*H@U!4Vg89}?qp!QB~#isC-H(X zR--J;OrwOZ!>C^VMG@!TCjs*hC03jDm1WzLP7 z?4&cgq?$!k^1KZ0NL^x9B2$>@f=P=-wsHGr|kTd%3#sv;PAPb1&k~pzwL6eY1od-yY60<%;5@*8^ zQaDbJkaqLMp{uq#-!bamhifdyY2WVs8Juo6XJb*ab=wX=35Nn_ciUHw>cihc)eRvX NX_RCqC-1)8`~wi!TlfF~ literal 0 HcmV?d00001 diff --git a/novawallet/Common/Extension/UIKit/Style/RoundedView+Style.swift b/novawallet/Common/Extension/UIKit/Style/RoundedView+Style.swift index dc1ba9e35e..150cb0a62f 100644 --- a/novawallet/Common/Extension/UIKit/Style/RoundedView+Style.swift +++ b/novawallet/Common/Extension/UIKit/Style/RoundedView+Style.swift @@ -213,6 +213,16 @@ extension RoundedView.Style { rounding: .init(radius: 12, corners: .allCorners) ) + static let strokeOnError = RoundedView.Style( + shadowOpacity: 0, + strokeWidth: 0, + strokeColor: R.color.colorBorderError(), + highlightedStrokeColor: R.color.colorBorderError(), + fillColor: R.color.colorInputBackground()!, + highlightedFillColor: R.color.colorInputBackground()!, + rounding: .init(radius: 12, corners: .allCorners) + ) + static let clear = RoundedView.Style( shadowOpacity: 0, strokeWidth: 0, diff --git a/novawallet/Common/View/InlineAlertView.swift b/novawallet/Common/View/InlineAlertView.swift index c2638f8c04..334e0de41c 100644 --- a/novawallet/Common/View/InlineAlertView.swift +++ b/novawallet/Common/View/InlineAlertView.swift @@ -63,4 +63,12 @@ extension InlineAlertView { view.contentView.stackView.alignment = .top return view } + + static func info() -> InlineAlertView { + let view = InlineAlertView() + view.backgroundView.fillColor = R.color.colorIndividualChipBackground()! + view.contentView.imageView.image = R.image.iconInfoAccent()! + view.contentView.stackView.alignment = .top + return view + } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 6fcfde7f80..6e7ae3bfb9 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -239,7 +239,13 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { } private func provideIssues() { - var issues: [SwapSetupViewError] = [] + var issues: [SwapSetupViewIssue] = [] + + if + let balance = payAssetBalance?.transferable, + balance == 0 { + issues.append(.zeroBalance) + } if let payAmount = getPayAmount(for: payAmountInput), @@ -248,7 +254,7 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { issues.append(.insufficientToken) } - view?.didReceive(errors: issues) + view?.didReceive(issues: issues) } func refreshQuote(direction: AssetConversion.Direction, forceUpdate: Bool = true) { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 8920629528..d8f05267a9 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -15,7 +15,7 @@ protocol SwapSetupViewProtocol: ControllerBackedProtocol { func didReceiveNetworkFee(viewModel: LoadableViewModelState) func didReceiveDetailsState(isAvailable: Bool) func didReceiveSettingsState(isAvailable: Bool) - func didReceive(errors: [SwapSetupViewError]) + func didReceive(issues: [SwapSetupViewIssue]) func didReceive(focus: TextFieldFocus?) } @@ -109,6 +109,9 @@ enum SwapSetupError: Error { case blockNumber(Error) } -enum SwapSetupViewError { +enum SwapSetupViewIssue { + case zeroBalance case insufficientToken + case minBalanceViolation(String) + case noLiqudity } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index e33e400f11..6a2ecb6f2e 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -260,11 +260,36 @@ extension SwapSetupViewController: SwapSetupViewProtocol { } } - func didReceive(errors: [SwapSetupViewError]) { - if errors.contains(.insufficientToken) { - rootView.changeDepositTokenButtonVisibility(hidden: false) - } else { - rootView.changeDepositTokenButtonVisibility(hidden: true) + func didReceive(issues: [SwapSetupViewIssue]) { + rootView.hideIssues() + rootView.changeDepositTokenButtonVisibility(hidden: true) + + issues.forEach { issue in + switch issue { + case .zeroBalance: + rootView.changeDepositTokenButtonVisibility(hidden: false) + case .insufficientToken: + rootView.changeDepositTokenButtonVisibility(hidden: false) + + let message = R.string.localizable.swapsNotEnoughTokens( + preferredLanguages: selectedLocale.rLanguages + ) + + rootView.displayPayIssue(with: message) + case let .minBalanceViolation(minBalance): + let message = R.string.localizable.commonReceiveAtLeastEdError( + minBalance, + preferredLanguages: selectedLocale.rLanguages + ) + + rootView.displayReceiveIssue(with: message) + case .noLiqudity: + let message = R.string.localizable.swapsNotEnoughLiquidity( + preferredLanguages: selectedLocale.rLanguages + ) + + rootView.displayPayIssue(with: message) + } } } } diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift index 265dbf1fbe..ec3dcb8b24 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift @@ -114,9 +114,13 @@ final class SwapAmountInputView: RoundedView { ) } - @objc private func actionEditingDidBeginEnd() { + private func updateFocusState() { strokeWidth = textInputView.textField.isFirstResponder ? 0.5 : 0.0 } + + @objc private func actionEditingDidBeginEnd() { + updateFocusState() + } } extension SwapAmountInputView { @@ -158,3 +162,31 @@ extension SwapAmountInputView { } } } + +extension SwapAmountInputView { + struct Style { + let contentStyle: RoundedView.Style + let textColor: UIColor? + } + + func applyInput(style: Style) { + apply(style: style.contentStyle) + + textInputView.textField.textColor = style.textColor + textInputView.textField.tintColor = style.textColor + + updateFocusState() + } +} + +extension SwapAmountInputView.Style { + static let normal = SwapAmountInputView.Style( + contentStyle: .strokeOnEditing, + textColor: R.color.colorTextPrimary() + ) + + static let error = SwapAmountInputView.Style( + contentStyle: .strokeOnError, + textColor: R.color.colorTextNegative() + ) +} diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index f70183600f..22557f00a1 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -41,6 +41,58 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { detailsView.networkFeeCell } + var payIssueLabel: UILabel? + + var receiveIssueLabel: UILabel? + + var notificationView: InlineAlertView? + + private func setupPayIssueLabel() -> UILabel { + if let payIssueLabel = payIssueLabel { + return payIssueLabel + } + + let label = UILabel(style: .caption1Negative) + label.numberOfLines = 0 + + insertArrangedSubview(label, after: payAmountInputView, spacingAfter: 8) + stackView.setCustomSpacing(8, after: payAmountInputView) + + payIssueLabel = label + + return label + } + + private func setupReceiveIssueLabel() -> UILabel { + if let receiveIssueLabel = receiveIssueLabel { + return receiveIssueLabel + } + + let label = UILabel(style: .caption1Negative) + label.numberOfLines = 0 + + insertArrangedSubview(label, after: receiveAmountInputView, spacingAfter: 8) + stackView.setCustomSpacing(8, after: receiveAmountInputView) + + receiveIssueLabel = label + + return label + } + + private func setupNotificationView() -> InlineAlertView { + if let notificationView = notificationView { + return notificationView + } + + let view = InlineAlertView.info() + insertArrangedSubview(view, after: detailsView, spacingAfter: 8) + stackView.setCustomSpacing(16, after: detailsView) + + notificationView = view + + return view + } + override func setupStyle() { backgroundColor = R.color.colorSecondaryScreenBackground() } @@ -109,4 +161,53 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { depositTokenButton.isHidden = hidden setNeedsLayout() } + + func hideIssues() { + hidePayIssue() + hideReceiveIssue() + } + + func displayPayIssue(with text: String) { + let payIssueLabel = setupPayIssueLabel() + payIssueLabel.text = text + + payAmountInputView.applyInput(style: .error) + } + + func hidePayIssue() { + payIssueLabel?.removeFromSuperview() + payIssueLabel = nil + + stackView.setCustomSpacing(12, after: payAmountInputView) + + payAmountInputView.applyInput(style: .normal) + } + + func displayReceiveIssue(with text: String) { + let receiveIssueLabel = setupReceiveIssueLabel() + receiveIssueLabel.text = text + + receiveAmountInputView.applyInput(style: .error) + } + + func hideReceiveIssue() { + receiveIssueLabel?.removeFromSuperview() + receiveIssueLabel = nil + + stackView.setCustomSpacing(16, after: receiveAmountInputView) + + receiveAmountInputView.applyInput(style: .normal) + } + + func displayInfoNotification(with text: String) { + let notificationView = setupNotificationView() + notificationView.contentView.detailsLabel.text = text + } + + func hideNotification() { + notificationView?.removeFromSuperview() + notificationView = nil + + stackView.setCustomSpacing(8, after: detailsView) + } } diff --git a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift index e04ed71d44..878aef0641 100644 --- a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift +++ b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift @@ -2,6 +2,8 @@ import Foundation import BigInt import SoraFoundation +typealias SwapRemoteValidatingClosure = (AssetConversion.QuoteArgs, @escaping SwapModel.QuoteValidateClosure) -> Void + protocol SwapDataValidatorFactoryProtocol: BaseDataValidatingFactoryProtocol { func hasSufficientBalance( params: SwapModel, @@ -19,7 +21,7 @@ protocol SwapDataValidatorFactoryProtocol: BaseDataValidatingFactoryProtocol { func passesRealtimeQuoteValidation( params: SwapModel, - remoteValidatingClosure: @escaping ((AssetConversion.QuoteArgs, @escaping SwapModel.QuoteValidateClosure) -> Void), + remoteValidatingClosure: @escaping SwapRemoteValidatingClosure, onQuoteUpdate: @escaping (AssetConversion.Quote) -> Void, locale: Locale ) -> DataValidating @@ -255,7 +257,7 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { // swiftlint:disable:next function_body_length func passesRealtimeQuoteValidation( params: SwapModel, - remoteValidatingClosure: @escaping ((AssetConversion.QuoteArgs, @escaping SwapModel.QuoteValidateClosure) -> Void), + remoteValidatingClosure: @escaping SwapRemoteValidatingClosure, onQuoteUpdate: @escaping (AssetConversion.Quote) -> Void, locale: Locale ) -> DataValidating { @@ -327,28 +329,4 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { } ) } - - func has( - quote: AssetConversion.Quote?, - payChainAssetId: ChainAssetId?, - receiveChainAssetId: ChainAssetId?, - locale: Locale, - onError: (() -> Void)? - ) -> DataValidating { - ErrorConditionViolation(onError: { [weak self] in - defer { - onError?() - } - - guard let view = self?.view else { - return - } - self?.presentable.presentNotEnoughLiquidity(from: view, locale: locale) - }, preservesCondition: { - guard let quote = quote else { - return false - } - return quote.assetIn == payChainAssetId && quote.assetOut == receiveChainAssetId - }) - } } diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index cbc0bc283e..4e9b32bb15 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1422,3 +1422,5 @@ "common.receive.at.least.ed.error" = "You can’t receive less than %@"; "common.receive.not.sufficient.native.asset.error" = "You must keep at least %@ to receive %@ token"; "swaps.violating.consumers.message" = "You should keep at least %@ after paying %@ network fee as you are holding non sufficient tokens."; +"swaps.not.enough.tokens" = "Not enough tokens to swap"; +"swaps.not.enough.liquidity" = "Not enough liquidity"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 9685713675..b74d47aa0e 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1422,3 +1422,5 @@ "common.receive.at.least.ed.error" = "Вы не можете получить меньше чем %@"; "common.receive.not.sufficient.native.asset.error" = "У вас должно быть минимум %@ для получения %@ токена"; "swaps.violating.consumers.message" = "Вам необходимо оставить минимум %@ после уплаты %@ комиссии сети так как вы владеете несамодостаточными токенами."; +"swaps.not.enough.tokens" = "Недостаточно токенов для обмена"; +"swaps.not.enough.liquidity" = "Недостаточно ликвидности"; From 85fc13755f880d88c073b83f8cb39d45e3625d9c Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 7 Nov 2023 10:19:49 +0100 Subject: [PATCH 125/204] add fee notification --- .../Model/SwapsSetupViewModelFactory.swift | 56 +++ .../Swaps/Setup/SwapSetupPresenter.swift | 381 ++++++++++-------- .../Swaps/Setup/SwapSetupProtocols.swift | 1 + .../Swaps/Setup/SwapSetupViewController.swift | 8 + .../AssetStorageInfoOperationFactory.swift | 11 +- novawallet/en.lproj/Localizable.strings | 1 + novawallet/ru.lproj/Localizable.strings | 1 + 7 files changed, 280 insertions(+), 179 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift index fadea27b6e..f1a58c7137 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift @@ -37,6 +37,15 @@ protocol SwapsSetupViewModelFactoryProtocol: SwapPriceDifferenceViewModelFactory isEditable: Bool, priceData: PriceData? ) -> SwapFeeViewModel + + func minimalBalanceSwapForFeeMessage( + for networkFeeAddition: AssetConversion.AmountWithNative, + feeChainAsset: ChainAsset, + utilityChainAsset: ChainAsset, + utilityPriceData: PriceData? + ) -> String + + func amountFromValue(_ decimal: Decimal, chainAsset: ChainAsset) -> String } final class SwapsSetupViewModelFactory { @@ -260,4 +269,51 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { return .init(isEditable: isEditable, balanceViewModel: balanceViewModel) } + + func minimalBalanceSwapForFeeMessage( + for networkFeeAddition: AssetConversion.AmountWithNative, + feeChainAsset: ChainAsset, + utilityChainAsset: ChainAsset, + utilityPriceData: PriceData? + ) -> String { + let targetAmount = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: feeChainAsset.assetDisplayInfo, + value: networkFeeAddition.targetAmount.decimal(precision: feeChainAsset.asset.precision) + ).value(for: locale) + + let nativeAmountDecimal = networkFeeAddition.nativeAmount.decimal(precision: utilityChainAsset.asset.precision) + let nativeAmountWithoutPrice = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: utilityChainAsset.assetDisplayInfo, + value: nativeAmountDecimal + ).value(for: locale) + + let nativeAmount: String + + if let priceData = utilityPriceData { + let price = balanceViewModelFactoryFacade.priceFromAmount( + targetAssetInfo: utilityChainAsset.assetDisplayInfo, + amount: nativeAmountDecimal, + priceData: priceData + ).value(for: locale) + + nativeAmount = "\(nativeAmountWithoutPrice) \(price.inParenthesis())" + } else { + nativeAmount = nativeAmountWithoutPrice + } + + return R.string.localizable.swapsPayAssetFeeEdMessage( + feeChainAsset.asset.symbol, + targetAmount, + nativeAmount, + utilityChainAsset.asset.symbol, + preferredLanguages: locale.rLanguages + ) + } + + func amountFromValue(_ decimal: Decimal, chainAsset: ChainAsset) -> String { + balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: chainAsset.assetDisplayInfo, + value: decimal + ).value(for: locale) + } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 6e7ae3bfb9..171e7baf79 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -58,6 +58,189 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { self.localizationManager = localizationManager } + // MARK: Base implementation + + override func getInputAmount() -> Decimal? { + guard let payAmountInput = payAmountInput else { + return nil + } + + let maxAmount = getMaxModel()?.calculate() ?? 0 + return payAmountInput.absoluteValue(from: maxAmount) + } + + override func getPayChainAsset() -> ChainAsset? { + payChainAsset + } + + override func getReceiveChainAsset() -> ChainAsset? { + receiveChainAsset + } + + override func getFeeChainAsset() -> ChainAsset? { + feeChainAsset + } + + override func getQuoteArgs() -> AssetConversion.QuoteArgs? { + quoteArgs + } + + override func getSlippage() -> BigRational? { + slippage + } + + override func shouldHandleQuote(for args: AssetConversion.QuoteArgs?) -> Bool { + quoteArgs == args + } + + override func shouldHandleFee(for feeIdentifier: TransactionFeeId, feeChainAssetId: ChainAssetId?) -> Bool { + self.feeIdentifier == SwapSetupFeeIdentifier(transactionId: feeIdentifier, feeChainAssetId: feeChainAssetId) + } + + override func estimateFee() { + guard let quote = quote, + let receiveChain = receiveChainAsset?.chain, + let accountId = selectedWallet.fetch(for: receiveChain.accountRequest())?.accountId, + let quoteArgs = quoteArgs, + let slippage = slippage else { + return + } + + let args = AssetConversion.CallArgs( + assetIn: quote.assetIn, + amountIn: quote.amountIn, + assetOut: quote.assetOut, + amountOut: quote.amountOut, + receiver: accountId, + direction: quoteArgs.direction, + slippage: slippage + ) + + let newIdentifier = SwapSetupFeeIdentifier( + transactionId: args.identifier, + feeChainAssetId: feeChainAsset?.chainAssetId + ) + + guard newIdentifier != feeIdentifier else { + return + } + + feeIdentifier = newIdentifier + interactor.calculateFee(args: args) + } + + override func applySwapMax() { + payAmountInput = .rate(1) + providePayAssetViews() + refreshQuote(direction: .sell) + provideButtonState() + provideIssues() + } + + override func handleBaseError(_ error: SwapBaseError) { + handleBaseError( + error, + view: view, + interactor: interactor, + wireframe: wireframe, + locale: selectedLocale + ) + } + + override func handleNewQuote(_ quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) { + logger.debug("New quote: \(quote)") + + switch quoteArgs.direction { + case .buy: + let payAmount = payChainAsset.map { + Decimal.fromSubstrateAmount( + quote.amountIn, + precision: Int16($0.asset.precision) + ) ?? 0 + } + payAmountInput = payAmount.map { .absolute($0) } + providePayAmountInputViewModel() + case .sell: + receiveAmountInput = receiveChainAsset.map { + Decimal.fromSubstrateAmount( + quote.amountOut, + precision: $0.asset.displayInfo.assetPrecision + ) ?? 0 + } + provideReceiveAmountInputViewModel() + provideReceiveInputPriceViewModel() + } + + provideRateViewModel() + provideButtonState() + + estimateFee() + } + + override func handleNewFee( + _: AssetConversion.FeeModel?, + transactionFeeId _: TransactionFeeId, + feeChainAssetId _: ChainAssetId? + ) { + provideFeeViewModel() + + if case .rate = payAmountInput { + providePayInputPriceViewModel() + providePayAmountInputViewModel() + } + + provideButtonState() + provideIssues() + provideNotification() + } + + override func handleNewPrice(_: PriceData?, chainAssetId: ChainAssetId) { + if payChainAsset?.chainAssetId == chainAssetId { + providePayInputPriceViewModel() + } + + if receiveChainAsset?.chainAssetId == chainAssetId { + provideReceiveInputPriceViewModel() + } + + if feeChainAsset?.chainAssetId == chainAssetId { + provideFeeViewModel() + } + + provideNotification() + } + + override func handleNewBalance(_: AssetBalance?, for chainAsset: ChainAssetId) { + if payChainAsset?.chainAssetId == chainAsset { + providePayTitle() + provideIssues() + + if case .rate = payAmountInput { + providePayInputPriceViewModel() + providePayAmountInputViewModel() + provideButtonState() + } + } + } + + override func handleNewBalanceExistense(_: AssetBalanceExistence, chainAssetId _: ChainAssetId) { + if case .rate = payAmountInput { + providePayInputPriceViewModel() + providePayAmountInputViewModel() + provideButtonState() + } + } + + override func handleNewAccountInfo(_: AccountInfo?, chainId _: ChainModel.Id) { + if case .rate = payAmountInput { + providePayInputPriceViewModel() + providePayAmountInputViewModel() + provideButtonState() + } + } +} + +extension SwapSetupPresenter { private func getPayAmount(for input: AmountInputResult?) -> Decimal? { guard let input = input else { return nil @@ -257,6 +440,26 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { view?.didReceive(issues: issues) } + private func provideNotification() { + guard + let networkFeeAddition = fee?.networkFeeAddition, + let feeChainAsset = feeChainAsset, + !feeChainAsset.isUtilityAsset, + let utilityChainAsset = feeChainAsset.chain.utilityChainAsset() else { + view?.didSetNotification(message: nil) + return + } + + let message = viewModelFactory.minimalBalanceSwapForFeeMessage( + for: networkFeeAddition, + feeChainAsset: feeChainAsset, + utilityChainAsset: utilityChainAsset, + utilityPriceData: prices[utilityChainAsset.chainAssetId] + ) + + view?.didSetNotification(message: message) + } + func refreshQuote(direction: AssetConversion.Direction, forceUpdate: Bool = true) { guard let payChainAsset = payChainAsset, @@ -345,184 +548,6 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { estimateFee() } - - // MARK: Base implementation - - override func getInputAmount() -> Decimal? { - guard let payAmountInput = payAmountInput else { - return nil - } - - let maxAmount = getMaxModel()?.calculate() ?? 0 - return payAmountInput.absoluteValue(from: maxAmount) - } - - override func getPayChainAsset() -> ChainAsset? { - payChainAsset - } - - override func getReceiveChainAsset() -> ChainAsset? { - receiveChainAsset - } - - override func getFeeChainAsset() -> ChainAsset? { - feeChainAsset - } - - override func getQuoteArgs() -> AssetConversion.QuoteArgs? { - quoteArgs - } - - override func getSlippage() -> BigRational? { - slippage - } - - override func shouldHandleQuote(for args: AssetConversion.QuoteArgs?) -> Bool { - quoteArgs == args - } - - override func shouldHandleFee(for feeIdentifier: TransactionFeeId, feeChainAssetId: ChainAssetId?) -> Bool { - self.feeIdentifier == SwapSetupFeeIdentifier(transactionId: feeIdentifier, feeChainAssetId: feeChainAssetId) - } - - override func estimateFee() { - guard let quote = quote, - let receiveChain = receiveChainAsset?.chain, - let accountId = selectedWallet.fetch(for: receiveChain.accountRequest())?.accountId, - let quoteArgs = quoteArgs, - let slippage = slippage else { - return - } - - let args = AssetConversion.CallArgs( - assetIn: quote.assetIn, - amountIn: quote.amountIn, - assetOut: quote.assetOut, - amountOut: quote.amountOut, - receiver: accountId, - direction: quoteArgs.direction, - slippage: slippage - ) - - let newIdentifier = SwapSetupFeeIdentifier( - transactionId: args.identifier, - feeChainAssetId: feeChainAsset?.chainAssetId - ) - - guard newIdentifier != feeIdentifier else { - return - } - - feeIdentifier = newIdentifier - interactor.calculateFee(args: args) - } - - override func applySwapMax() { - payAmountInput = .rate(1) - providePayAssetViews() - refreshQuote(direction: .sell) - provideButtonState() - provideIssues() - } - - override func handleBaseError(_ error: SwapBaseError) { - handleBaseError( - error, - view: view, - interactor: interactor, - wireframe: wireframe, - locale: selectedLocale - ) - } - - override func handleNewQuote(_ quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) { - logger.debug("New quote: \(quote)") - - switch quoteArgs.direction { - case .buy: - let payAmount = payChainAsset.map { - Decimal.fromSubstrateAmount( - quote.amountIn, - precision: Int16($0.asset.precision) - ) ?? 0 - } - payAmountInput = payAmount.map { .absolute($0) } - providePayAmountInputViewModel() - case .sell: - receiveAmountInput = receiveChainAsset.map { - Decimal.fromSubstrateAmount( - quote.amountOut, - precision: $0.asset.displayInfo.assetPrecision - ) ?? 0 - } - provideReceiveAmountInputViewModel() - provideReceiveInputPriceViewModel() - } - - provideRateViewModel() - provideButtonState() - - estimateFee() - } - - override func handleNewFee( - _: AssetConversion.FeeModel?, - transactionFeeId _: TransactionFeeId, - feeChainAssetId _: ChainAssetId? - ) { - provideFeeViewModel() - - if case .rate = payAmountInput { - providePayInputPriceViewModel() - providePayAmountInputViewModel() - } - - provideButtonState() - provideIssues() - } - - override func handleNewPrice(_: PriceData?, chainAssetId: ChainAssetId) { - if payChainAsset?.chainAssetId == chainAssetId { - providePayInputPriceViewModel() - } - - if receiveChainAsset?.chainAssetId == chainAssetId { - provideReceiveInputPriceViewModel() - } - - if feeChainAsset?.chainAssetId == chainAssetId { - provideFeeViewModel() - } - } - - override func handleNewBalance(_: AssetBalance?, for chainAsset: ChainAssetId) { - if payChainAsset?.chainAssetId == chainAsset { - providePayTitle() - provideIssues() - - if case .rate = payAmountInput { - providePayInputPriceViewModel() - providePayAmountInputViewModel() - provideButtonState() - } - } - } - - override func handleNewBalanceExistense(_: AssetBalanceExistence, chainAssetId _: ChainAssetId) { - if case .rate = payAmountInput { - providePayInputPriceViewModel() - providePayAmountInputViewModel() - provideButtonState() - } - } - - override func handleNewAccountInfo(_: AccountInfo?, chainId _: ChainModel.Id) { - if case .rate = payAmountInput { - providePayInputPriceViewModel() - providePayAmountInputViewModel() - provideButtonState() - } - } } extension SwapSetupPresenter: SwapSetupPresenterProtocol { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index d8f05267a9..7f92482099 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -16,6 +16,7 @@ protocol SwapSetupViewProtocol: ControllerBackedProtocol { func didReceiveDetailsState(isAvailable: Bool) func didReceiveSettingsState(isAvailable: Bool) func didReceive(issues: [SwapSetupViewIssue]) + func didSetNotification(message: String?) func didReceive(focus: TextFieldFocus?) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 6a2ecb6f2e..9a7dfd8d22 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -292,6 +292,14 @@ extension SwapSetupViewController: SwapSetupViewProtocol { } } } + + func didSetNotification(message: String?) { + if let message = message { + rootView.displayInfoNotification(with: message) + } else { + rootView.hideNotification() + } + } } extension SwapSetupViewController: Localizable { diff --git a/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift b/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift index 394998eb75..45ba3055ac 100644 --- a/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift +++ b/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift @@ -111,7 +111,16 @@ extension AssetStorageInfoOperationFactory: AssetStorageInfoOperationFactoryProt let fetchWrapper: CompoundOperationWrapper<[StorageResponse]> = requestFactory.queryItems( engine: connection, - keyParams: { [StringScaleMapper(value: extras.assetId)] }, + keyParams: { + let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + let param = try StatemineAssetSerializer.decode( + assetId: extras.assetId, + palletName: extras.palletName, + codingFactory: codingFactory + ) + + return [param] + }, factory: { try codingFactoryOperation.extractNoCancellableResultData() }, storagePath: assetsDetailsPath ) diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 4e9b32bb15..f45ce30ce1 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1424,3 +1424,4 @@ "swaps.violating.consumers.message" = "You should keep at least %@ after paying %@ network fee as you are holding non sufficient tokens."; "swaps.not.enough.tokens" = "Not enough tokens to swap"; "swaps.not.enough.liquidity" = "Not enough liquidity"; +"swaps.pay.asset.fee.ed.message" = "To pay network fee with %@, Nova will automatically swap %@ for %@ to maintain your account's minimum %@ balance."; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index b74d47aa0e..be8c644e37 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1424,3 +1424,4 @@ "swaps.violating.consumers.message" = "Вам необходимо оставить минимум %@ после уплаты %@ комиссии сети так как вы владеете несамодостаточными токенами."; "swaps.not.enough.tokens" = "Недостаточно токенов для обмена"; "swaps.not.enough.liquidity" = "Недостаточно ликвидности"; +"swaps.pay.asset.fee.ed.message" = "Для оплаты комиссии сети %@ токеном, Nova автоматически поменяет %@ в %@ для сохранения минимального %@ баланса аккаунта."; From be98d9fa83050fc40475c76485ee02d0a223d1a7 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 7 Nov 2023 13:06:55 +0100 Subject: [PATCH 126/204] add issues view model factory --- novawallet.xcodeproj/project.pbxproj | 8 ++ .../Swaps/Base/SwapBasePresenter.swift | 28 ++++--- .../Setup/Model/SwapIssueCheckParams.swift | 13 ++++ .../Model/SwapIssueViewModelFactory.swift | 76 +++++++++++++++++++ .../Swaps/Setup/SwapSetupPresenter.swift | 45 ++++++----- .../Swaps/Setup/SwapSetupProtocols.swift | 2 +- .../Swaps/Setup/SwapSetupViewController.swift | 2 +- .../Swaps/Setup/SwapSetupViewFactory.swift | 6 ++ 8 files changed, 151 insertions(+), 29 deletions(-) create mode 100644 novawallet/Modules/Swaps/Setup/Model/SwapIssueCheckParams.swift create mode 100644 novawallet/Modules/Swaps/Setup/Model/SwapIssueViewModelFactory.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 4d8a6cf739..0f1c0a86e8 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -74,6 +74,8 @@ 0C13DFCF2AF8ADB300E5F355 /* BigUInt+Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFCE2AF8ADB300E5F355 /* BigUInt+Operation.swift */; }; 0C13DFD12AF8AE3E00E5F355 /* SwapBasePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFD02AF8AE3E00E5F355 /* SwapBasePresenter.swift */; }; 0C13DFD32AF949B400E5F355 /* BalanceViewModelFactoryFacade+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFD22AF949B400E5F355 /* BalanceViewModelFactoryFacade+Formatting.swift */; }; + 0C13DFD52AFA4F1500E5F355 /* SwapIssueCheckParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFD42AFA4F1500E5F355 /* SwapIssueCheckParams.swift */; }; + 0C13DFD72AFA50A200E5F355 /* SwapIssueViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFD62AFA50A200E5F355 /* SwapIssueViewModelFactory.swift */; }; 0C154A0E2A45995500932C3F /* CompoundComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C154A0D2A45995500932C3F /* CompoundComparator.swift */; }; 0C17BD972A42F162004AF9E7 /* WalletViewModelObserverContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */; }; 0C17BD992A42F1BE004AF9E7 /* MoneyPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */; }; @@ -4132,6 +4134,8 @@ 0C13DFCE2AF8ADB300E5F355 /* BigUInt+Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BigUInt+Operation.swift"; sourceTree = ""; }; 0C13DFD02AF8AE3E00E5F355 /* SwapBasePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapBasePresenter.swift; sourceTree = ""; }; 0C13DFD22AF949B400E5F355 /* BalanceViewModelFactoryFacade+Formatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BalanceViewModelFactoryFacade+Formatting.swift"; sourceTree = ""; }; + 0C13DFD42AFA4F1500E5F355 /* SwapIssueCheckParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapIssueCheckParams.swift; sourceTree = ""; }; + 0C13DFD62AFA50A200E5F355 /* SwapIssueViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapIssueViewModelFactory.swift; sourceTree = ""; }; 0C154A0D2A45995500932C3F /* CompoundComparator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompoundComparator.swift; sourceTree = ""; }; 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletViewModelObserverContainer.swift; sourceTree = ""; }; 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyPresentable.swift; sourceTree = ""; }; @@ -9934,6 +9938,8 @@ 77C976232AF3A5280049272C /* SwapViewModels.swift */, 77C9761F2AF36A170049272C /* SwapModels.swift */, 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */, + 0C13DFD42AFA4F1500E5F355 /* SwapIssueCheckParams.swift */, + 0C13DFD62AFA50A200E5F355 /* SwapIssueViewModelFactory.swift */, ); path = Model; sourceTree = ""; @@ -22431,6 +22437,7 @@ 8465DA3D298ECF2500C7CFF1 /* GovernanceAddDelegationTracksProtocols.swift in Sources */, 6797F109D7C270DE4877B435 /* NftListInteractor.swift in Sources */, 845B811528F43C350040CE84 /* Treasury.swift in Sources */, + 0C13DFD72AFA50A200E5F355 /* SwapIssueViewModelFactory.swift in Sources */, 0C7C886C2A9622F800DD96A1 /* StakingSelectedEntityViewModel.swift in Sources */, 84C3420B283187D800156569 /* BlockTimeEstimationService.swift in Sources */, EB376E61CD1C39AC148DE80C /* NftListViewController.swift in Sources */, @@ -23148,6 +23155,7 @@ 8C68C4CFAF7CB9312C86D5B8 /* GovernanceDelegateSearchViewController.swift in Sources */, 77F9FB0B2A9D97A100820625 /* NominationPoolBondMoreSetupWireframe.swift in Sources */, F0C3DCA3CD4F850C16406716 /* GovernanceDelegateSearchViewFactory.swift in Sources */, + 0C13DFD52AFA4F1500E5F355 /* SwapIssueCheckParams.swift in Sources */, C98A02D4DEAC6E4CACB9E47E /* StakingRebagConfirmProtocols.swift in Sources */, B09F155D14D146377FB2952A /* StakingRebagConfirmWireframe.swift in Sources */, 840AE2E329C9A715008FF665 /* OptionStringCodable+Empty.swift in Sources */, diff --git a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift index 8a1a970f8b..2e0e90514f 100644 --- a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift +++ b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift @@ -72,7 +72,17 @@ class SwapBasePresenter { } var fee: AssetConversion.FeeModel? - var quote: AssetConversion.Quote? + var quoteResult: Result? + + var quote: AssetConversion.Quote? { + switch quoteResult { + case let .success(quote): + return quote + case .failure, .none: + return nil + } + } + var accountInfo: AccountInfo? func getSwapModel() -> SwapModel? { @@ -89,7 +99,7 @@ class SwapBasePresenter { payChainAsset: payChainAsset, receiveChainAsset: receiveChainAsset, feeChainAsset: feeChainAsset, - spendingAmount: getInputAmount(), + spendingAmount: getSpendingInputAmount(), payAssetBalance: payAssetBalance, feeAssetBalance: feeAssetBalance, receiveAssetBalance: receiveAssetBalance, @@ -118,7 +128,7 @@ class SwapBasePresenter { ) } - func getInputAmount() -> Decimal? { + func getSpendingInputAmount() -> Decimal? { fatalError("Must be implemented by parent class") } @@ -186,14 +196,12 @@ class SwapBasePresenter { logger.error("Did receive base error: \(error)") switch error { - case let .quote(_, args): + case let .quote(error, args): guard shouldHandleQuote(for: args) else { return } - wireframe.presentRequestStatus(on: view, locale: locale) { - interactor.calculateQuote(for: args) - } + quoteResult = .failure(error) case let .fetchFeeFailed(_, id, feeChainAssetId): guard shouldHandleFee(for: id, feeChainAssetId: feeChainAssetId) else { return @@ -269,11 +277,11 @@ class SwapBasePresenter { ), dataValidatingFactory.passesRealtimeQuoteValidation( params: swapModel, - remoteValidatingClosure: { [weak self] args, completion in + remoteValidatingClosure: { args, completion in interactor.requestValidatingQuote(for: args, completion: completion) }, onQuoteUpdate: { [weak self] quote in - self?.quote = quote + self?.quoteResult = .success(quote) }, locale: locale ) @@ -287,7 +295,7 @@ extension SwapBasePresenter: SwapBaseInteractorOutputProtocol { return } - self.quote = quote + quoteResult = .success(quote) handleNewQuote(quote, for: quoteArgs) } diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapIssueCheckParams.swift b/novawallet/Modules/Swaps/Setup/Model/SwapIssueCheckParams.swift new file mode 100644 index 0000000000..f2f55686c4 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/Model/SwapIssueCheckParams.swift @@ -0,0 +1,13 @@ +import Foundation + +struct SwapIssueCheckParams { + let payChainAsset: ChainAsset? + let receiveChainAsset: ChainAsset? + let payAmount: Decimal? + let receiveAmount: Decimal? + let payAssetBalance: AssetBalance? + let receiveAssetBalance: AssetBalance? + let payAssetExistense: AssetBalanceExistence? + let receiveAssetExistense: AssetBalanceExistence? + let quoteResult: Result? +} diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapIssueViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapIssueViewModelFactory.swift new file mode 100644 index 0000000000..62444277c9 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/Model/SwapIssueViewModelFactory.swift @@ -0,0 +1,76 @@ +import Foundation + +protocol SwapIssueViewModelFactoryProtocol { + func detectIssues(in model: SwapIssueCheckParams, locale: Locale) -> [SwapSetupViewIssue] +} + +final class SwapIssueViewModelFactory { + let balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol + + init(balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol) { + self.balanceViewModelFactoryFacade = balanceViewModelFactoryFacade + } + + func detectZeroBalance(in model: SwapIssueCheckParams) -> SwapSetupViewIssue? { + if let balance = model.payAssetBalance?.transferable, balance == 0 { + return .zeroBalance + } else { + return nil + } + } + + func detectInsufficientBalance(in model: SwapIssueCheckParams) -> SwapSetupViewIssue? { + if let payAmount = model.payAmount, + let payChainAsset = model.payChainAsset, + let balance = model.payAssetBalance?.transferable.decimal(precision: payChainAsset.asset.precision), + payAmount > balance { + return .insufficientBalance + } else { + return nil + } + } + + func detectMinBalanceViolationOnReceive(in model: SwapIssueCheckParams, locale: Locale) -> SwapSetupViewIssue? { + guard + let receiveChainAsset = model.receiveChainAsset, + let receiveAmount = model.receiveAmount, + let minBalance = model.receiveAssetExistense?.minBalance.decimal( + precision: receiveChainAsset.asset.precision + ), + let beforeSwapBalance = model.receiveAssetBalance?.freeInPlank.decimal( + precision: receiveChainAsset.asset.precision + ) else { + return nil + } + + guard beforeSwapBalance + receiveAmount < minBalance else { + return nil + } + + let minBalanceString = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: receiveChainAsset.assetDisplayInfo, + value: minBalance + ).value(for: locale) + + return .minBalanceViolation(minBalanceString) + } + + func detectNoLiquidity(in model: SwapIssueCheckParams) -> SwapSetupViewIssue? { + if case .failure = model.quoteResult { + return .noLiqudity + } else { + return nil + } + } +} + +extension SwapIssueViewModelFactory: SwapIssueViewModelFactoryProtocol { + func detectIssues(in model: SwapIssueCheckParams, locale: Locale) -> [SwapSetupViewIssue] { + [ + detectZeroBalance(in: model), + detectInsufficientBalance(in: model), + detectMinBalanceViolationOnReceive(in: model, locale: locale), + detectNoLiquidity(in: model) + ].compactMap { $0 } + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 171e7baf79..556002435e 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -7,6 +7,7 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { let wireframe: SwapSetupWireframeProtocol let interactor: SwapSetupInteractorInputProtocol let purchaseProvider: PurchaseProviderProtocol + let issuesViewModelFactory: SwapIssueViewModelFactoryProtocol private(set) var viewModelFactory: SwapsSetupViewModelFactoryProtocol @@ -36,6 +37,7 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { interactor: SwapSetupInteractorInputProtocol, wireframe: SwapSetupWireframeProtocol, viewModelFactory: SwapsSetupViewModelFactoryProtocol, + issuesViewModelFactory: SwapIssueViewModelFactoryProtocol, dataValidatingFactory: SwapDataValidatorFactoryProtocol, localizationManager: LocalizationManagerProtocol, selectedWallet: MetaAccountModel, @@ -47,6 +49,7 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { self.interactor = interactor self.wireframe = wireframe self.viewModelFactory = viewModelFactory + self.issuesViewModelFactory = issuesViewModelFactory self.purchaseProvider = purchaseProvider super.init( @@ -60,7 +63,7 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { // MARK: Base implementation - override func getInputAmount() -> Decimal? { + override func getSpendingInputAmount() -> Decimal? { guard let payAmountInput = payAmountInput else { return nil } @@ -145,6 +148,8 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { wireframe: wireframe, locale: selectedLocale ) + + provideIssues() } override func handleNewQuote(_ quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) { @@ -213,7 +218,6 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { override func handleNewBalance(_: AssetBalance?, for chainAsset: ChainAssetId) { if payChainAsset?.chainAssetId == chainAsset { providePayTitle() - provideIssues() if case .rate = payAmountInput { providePayInputPriceViewModel() @@ -221,6 +225,8 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { provideButtonState() } } + + provideIssues() } override func handleNewBalanceExistense(_: AssetBalanceExistence, chainAssetId _: ChainAssetId) { @@ -229,6 +235,8 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { providePayAmountInputViewModel() provideButtonState() } + + provideIssues() } override func handleNewAccountInfo(_: AccountInfo?, chainId _: ChainModel.Id) { @@ -422,20 +430,20 @@ extension SwapSetupPresenter { } private func provideIssues() { - var issues: [SwapSetupViewIssue] = [] - - if - let balance = payAssetBalance?.transferable, - balance == 0 { - issues.append(.zeroBalance) - } - - if - let payAmount = getPayAmount(for: payAmountInput), - let maxAmount = getMaxModel()?.calculate(), - payAmount > maxAmount { - issues.append(.insufficientToken) - } + let issues = issuesViewModelFactory.detectIssues( + in: .init( + payChainAsset: payChainAsset, + receiveChainAsset: receiveChainAsset, + payAmount: getSpendingInputAmount(), + receiveAmount: receiveAmountInput, + payAssetBalance: payAssetBalance, + receiveAssetBalance: receiveAssetBalance, + payAssetExistense: payAssetBalanceExistense, + receiveAssetExistense: receiveAssetBalanceExistense, + quoteResult: quoteResult + ), + locale: selectedLocale + ) view?.didReceive(issues: issues) } @@ -468,7 +476,7 @@ extension SwapSetupPresenter { } if forceUpdate { - quote = nil + quoteResult = nil } switch direction { @@ -509,6 +517,7 @@ extension SwapSetupPresenter { if forceUpdate { payAmountInput = nil providePayAmountInputViewModel() + provideIssues() } else { refreshQuote(direction: .sell) } @@ -532,6 +541,7 @@ extension SwapSetupPresenter { receiveAmountInput = nil provideReceiveAmountInputViewModel() provideReceiveInputPriceViewModel() + provideIssues() } else { refreshQuote(direction: .buy) } @@ -601,6 +611,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { self?.receiveChainAsset = chainAsset self?.provideReceiveAssetViews() self?.provideButtonState() + self?.provideIssues() self?.interactor.update(receiveChainAsset: chainAsset) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 7f92482099..92e288e26e 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -112,7 +112,7 @@ enum SwapSetupError: Error { enum SwapSetupViewIssue { case zeroBalance - case insufficientToken + case insufficientBalance case minBalanceViolation(String) case noLiqudity } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 9a7dfd8d22..3ba8b6f437 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -268,7 +268,7 @@ extension SwapSetupViewController: SwapSetupViewProtocol { switch issue { case .zeroBalance: rootView.changeDepositTokenButtonVisibility(hidden: false) - case .insufficientToken: + case .insufficientBalance: rootView.changeDepositTokenButtonVisibility(hidden: false) let message = R.string.localizable.swapsNotEnoughTokens( diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index f2502d6286..f26e5e767e 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -38,6 +38,11 @@ struct SwapSetupViewFactory { percentForamatter: NumberFormatter.percentSingle.localizableResource(), locale: LocalizationManager.shared.selectedLocale ) + + let issuesViewModelFactory = SwapIssueViewModelFactory( + balanceViewModelFactoryFacade: balanceViewModelFactoryFacade + ) + let dataValidatingFactory = SwapDataValidatorFactory( presentable: wireframe, balanceViewModelFactoryFacade: balanceViewModelFactoryFacade @@ -48,6 +53,7 @@ struct SwapSetupViewFactory { interactor: interactor, wireframe: wireframe, viewModelFactory: viewModelFactory, + issuesViewModelFactory: issuesViewModelFactory, dataValidatingFactory: dataValidatingFactory, localizationManager: LocalizationManager.shared, selectedWallet: selectedWallet, From 600ee59e07f3f172b101e9c30123f65f90f7edbd Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 7 Nov 2023 15:04:22 +0100 Subject: [PATCH 127/204] refactor slippage --- novawallet.xcodeproj/project.pbxproj | 8 ++--- novawallet/Common/Model/BigRational.swift | 4 +++ .../Model/AssetConversionConstants.swift | 5 --- .../Swaps/Base/Model/SlippageConfig.swift | 27 +++++++++++++++ .../Modules/Swaps/Base/SlippageBounds.swift | 16 +++++++-- .../Swaps/Base/SwapBasePresenter.swift | 7 ++-- .../Swaps/Confirm/SwapConfirmPresenter.swift | 4 ++- .../Confirm/SwapConfirmViewFactory.swift | 1 + .../Swaps/Setup/SwapSetupPresenter.swift | 11 +++--- .../Swaps/Setup/SwapSetupViewFactory.swift | 1 + .../Slippage/SwapSlippagePresenter.swift | 34 +++++++++++-------- .../Slippage/SwapSlippageViewFactory.swift | 3 +- 12 files changed, 83 insertions(+), 38 deletions(-) delete mode 100644 novawallet/Modules/AssetConversion/Model/AssetConversionConstants.swift create mode 100644 novawallet/Modules/Swaps/Base/Model/SlippageConfig.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 0f1c0a86e8..90b5ee6c7c 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -76,6 +76,7 @@ 0C13DFD32AF949B400E5F355 /* BalanceViewModelFactoryFacade+Formatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFD22AF949B400E5F355 /* BalanceViewModelFactoryFacade+Formatting.swift */; }; 0C13DFD52AFA4F1500E5F355 /* SwapIssueCheckParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFD42AFA4F1500E5F355 /* SwapIssueCheckParams.swift */; }; 0C13DFD72AFA50A200E5F355 /* SwapIssueViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFD62AFA50A200E5F355 /* SwapIssueViewModelFactory.swift */; }; + 0C13DFDB2AFA691400E5F355 /* SlippageConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFDA2AFA691400E5F355 /* SlippageConfig.swift */; }; 0C154A0E2A45995500932C3F /* CompoundComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C154A0D2A45995500932C3F /* CompoundComparator.swift */; }; 0C17BD972A42F162004AF9E7 /* WalletViewModelObserverContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */; }; 0C17BD992A42F1BE004AF9E7 /* MoneyPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */; }; @@ -256,7 +257,6 @@ 0CC2E56A2A6E6EBB004092E7 /* LocalStorageProviderObserving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2E5692A6E6EBB004092E7 /* LocalStorageProviderObserving.swift */; }; 0CC4CCF42A67C9C400F63041 /* Multistaking+NominationPools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC4CCF32A67C9C400F63041 /* Multistaking+NominationPools.swift */; }; 0CC6C8D82AAB401200AD8D9B /* CustomValidatorsFullList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC6C8D72AAB401200AD8D9B /* CustomValidatorsFullList.swift */; }; - 0CC74A3C2AEA9B06005280F5 /* AssetConversionConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC74A3B2AEA9B06005280F5 /* AssetConversionConstants.swift */; }; 0CCA245B2AC6917400AEF23D /* XcmV3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA245A2AC6917400AEF23D /* XcmV3.swift */; }; 0CCA245D2AC6918800AEF23D /* XcmV3Junction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA245C2AC6918800AEF23D /* XcmV3Junction.swift */; }; 0CCA245F2AC6974200AEF23D /* XcmV3Multilocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA245E2AC6974200AEF23D /* XcmV3Multilocation.swift */; }; @@ -4136,6 +4136,7 @@ 0C13DFD22AF949B400E5F355 /* BalanceViewModelFactoryFacade+Formatting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BalanceViewModelFactoryFacade+Formatting.swift"; sourceTree = ""; }; 0C13DFD42AFA4F1500E5F355 /* SwapIssueCheckParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapIssueCheckParams.swift; sourceTree = ""; }; 0C13DFD62AFA50A200E5F355 /* SwapIssueViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapIssueViewModelFactory.swift; sourceTree = ""; }; + 0C13DFDA2AFA691400E5F355 /* SlippageConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlippageConfig.swift; sourceTree = ""; }; 0C154A0D2A45995500932C3F /* CompoundComparator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompoundComparator.swift; sourceTree = ""; }; 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletViewModelObserverContainer.swift; sourceTree = ""; }; 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyPresentable.swift; sourceTree = ""; }; @@ -4323,7 +4324,6 @@ 0CC41A9A168AF0F1FBE5F799 /* Pods_novawalletAll_novawallet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_novawalletAll_novawallet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0CC4CCF32A67C9C400F63041 /* Multistaking+NominationPools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Multistaking+NominationPools.swift"; sourceTree = ""; }; 0CC6C8D72AAB401200AD8D9B /* CustomValidatorsFullList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomValidatorsFullList.swift; sourceTree = ""; }; - 0CC74A3B2AEA9B06005280F5 /* AssetConversionConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionConstants.swift; sourceTree = ""; }; 0CCA245A2AC6917400AEF23D /* XcmV3.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmV3.swift; sourceTree = ""; }; 0CCA245C2AC6918800AEF23D /* XcmV3Junction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmV3Junction.swift; sourceTree = ""; }; 0CCA245E2AC6974200AEF23D /* XcmV3Multilocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmV3Multilocation.swift; sourceTree = ""; }; @@ -8342,7 +8342,6 @@ isa = PBXGroup; children = ( 0C0CB37E2AC540B200EAC516 /* AssetConversion.swift */, - 0CC74A3B2AEA9B06005280F5 /* AssetConversionConstants.swift */, ); path = Model; sourceTree = ""; @@ -8473,6 +8472,7 @@ children = ( 0C2FDF182AEB82AA006A6C59 /* AssetHubFeeModelBuilder.swift */, 0C13DFCC2AF8A5A300E5F355 /* SwapMaxModel.swift */, + 0C13DFDA2AFA691400E5F355 /* SlippageConfig.swift */, ); path = Model; sourceTree = ""; @@ -21609,6 +21609,7 @@ 84E258A12892CC0F00DC8A51 /* AccountManagementViewLayout.swift in Sources */, 848FD1A528AE27E700CCD9E2 /* HardwareWalletOption.swift in Sources */, 845CB6FA26276326005F798B /* LongrunOperation.swift in Sources */, + 0C13DFDB2AFA691400E5F355 /* SlippageConfig.swift in Sources */, 8499FEC627BEC68000712589 /* UniquesInstanceMetadata.swift in Sources */, 840EAE6729FA8F0000453C7E /* WalletConnectDecision.swift in Sources */, 84EBFCE9285E7C100006327E /* XcmInstruction.swift in Sources */, @@ -21699,7 +21700,6 @@ 84D6D7FA27A7EC810094FC33 /* AssetListAssetCell.swift in Sources */, 61E0DC83C1D60D677274D7CE /* AccountExportPasswordViewFactory.swift in Sources */, 84EBFCF4285E90F80006327E /* XcmWeightMessages.swift in Sources */, - 0CC74A3C2AEA9B06005280F5 /* AssetConversionConstants.swift in Sources */, 843F9ABC29DDAF8F004F1737 /* JSONRPCTimeout.swift in Sources */, 841E5540282D524800C8438F /* ParastakingLocalStorageSubscriber.swift in Sources */, 84350AD228457CC30031EF24 /* BaseValidatorInfoViewModelFactory.swift in Sources */, diff --git a/novawallet/Common/Model/BigRational.swift b/novawallet/Common/Model/BigRational.swift index 97488e5ddc..fbbeb48263 100644 --- a/novawallet/Common/Model/BigRational.swift +++ b/novawallet/Common/Model/BigRational.swift @@ -59,4 +59,8 @@ extension BigRational { let denominator = denominator.decimal(precision: 0) return numerator / denominator } + + var decimalOrZeroValue: Decimal { + decimalValue ?? 0 + } } diff --git a/novawallet/Modules/AssetConversion/Model/AssetConversionConstants.swift b/novawallet/Modules/AssetConversion/Model/AssetConversionConstants.swift deleted file mode 100644 index db5647a14b..0000000000 --- a/novawallet/Modules/AssetConversion/Model/AssetConversionConstants.swift +++ /dev/null @@ -1,5 +0,0 @@ -import Foundation - -enum AssetConversionConstants { - static let defaultSlippage: Decimal = 0.5 -} diff --git a/novawallet/Modules/Swaps/Base/Model/SlippageConfig.swift b/novawallet/Modules/Swaps/Base/Model/SlippageConfig.swift new file mode 100644 index 0000000000..e3388280e2 --- /dev/null +++ b/novawallet/Modules/Swaps/Base/Model/SlippageConfig.swift @@ -0,0 +1,27 @@ +import Foundation + +struct SlippageConfig { + let defaultSlippage: BigRational + let slippageTips: [BigRational] + let minAvailableSlippage: BigRational + let maxAvailableSlippage: BigRational + let smallSlippage: BigRational + let bigSlippage: BigRational +} + +extension SlippageConfig { + static var defaultConfig: SlippageConfig { + .init( + defaultSlippage: BigRational(numerator: 5, denominator: 1000), + slippageTips: [ + BigRational(numerator: 1, denominator: 1000), + BigRational(numerator: 5, denominator: 1000), + BigRational(numerator: 1, denominator: 100) + ], + minAvailableSlippage: BigRational(numerator: 1, denominator: 10000), + maxAvailableSlippage: BigRational(numerator: 50, denominator: 100), + smallSlippage: BigRational(numerator: 5, denominator: 10000), + bigSlippage: BigRational(numerator: 1, denominator: 100) + ) + } +} diff --git a/novawallet/Modules/Swaps/Base/SlippageBounds.swift b/novawallet/Modules/Swaps/Base/SlippageBounds.swift index 93b59b4af2..635a239922 100644 --- a/novawallet/Modules/Swaps/Base/SlippageBounds.swift +++ b/novawallet/Modules/Swaps/Base/SlippageBounds.swift @@ -1,13 +1,25 @@ import Foundation struct SlippageBounds { - let restriction: BoundValue = .init(lower: 0.1, upper: 50) - let recommendation: BoundValue = .init(lower: 0.1, upper: 5) + let restriction: BoundValue + let recommendation: BoundValue struct BoundValue { let lower: Decimal let upper: Decimal } + + init(config: SlippageConfig) { + restriction = .init( + lower: config.minAvailableSlippage.toPercents().decimalOrZeroValue, + upper: config.maxAvailableSlippage.toPercents().decimalOrZeroValue + ) + + recommendation = .init( + lower: config.smallSlippage.toPercents().decimalOrZeroValue, + upper: config.bigSlippage.toPercents().decimalOrZeroValue + ) + } } extension SlippageBounds { diff --git a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift index 2e0e90514f..d87cf5d601 100644 --- a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift +++ b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift @@ -90,8 +90,7 @@ class SwapBasePresenter { let payChainAsset = getPayChainAsset(), let receiveChainAsset = getReceiveChainAsset(), let feeChainAsset = getFeeChainAsset(), - let quoteArgs = getQuoteArgs(), - let slippage = getSlippage() else { + let quoteArgs = getQuoteArgs() else { return nil } @@ -111,7 +110,7 @@ class SwapBasePresenter { feeModel: fee, quoteArgs: quoteArgs, quote: quote, - slippage: slippage, + slippage: getSlippage(), accountInfo: accountInfo ) } @@ -136,7 +135,7 @@ class SwapBasePresenter { fatalError("Must be implemented by parent class") } - func getSlippage() -> BigRational? { + func getSlippage() -> BigRational { fatalError("Must be implemented by parent class") } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 103fb7f3d3..c637baf160 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -8,7 +8,7 @@ final class SwapConfirmPresenter { let interactor: SwapConfirmInteractorInputProtocol let dataValidatingFactory: SwapDataValidatorFactoryProtocol let initState: SwapConfirmInitState - let slippageBounds = SlippageBounds() + let slippageBounds: SlippageBounds private var viewModelFactory: SwapConfirmViewModelFactoryProtocol private var feePriceData: PriceData? @@ -25,6 +25,7 @@ final class SwapConfirmPresenter { interactor: SwapConfirmInteractorInputProtocol, wireframe: SwapConfirmWireframeProtocol, viewModelFactory: SwapConfirmViewModelFactoryProtocol, + slippageBounds: SlippageBounds, chainAccountResponse: MetaChainAccountResponse, localizationManager: LocalizationManagerProtocol, dataValidatingFactory: SwapDataValidatorFactoryProtocol, @@ -33,6 +34,7 @@ final class SwapConfirmPresenter { self.interactor = interactor self.wireframe = wireframe self.viewModelFactory = viewModelFactory + self.slippageBounds = slippageBounds self.initState = initState quote = initState.quote self.chainAccountResponse = chainAccountResponse diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index a2605ec0e7..666e2e51a0 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -43,6 +43,7 @@ struct SwapConfirmViewFactory { interactor: interactor, wireframe: wireframe, viewModelFactory: viewModelFactory, + slippageBounds: .init(config: SlippageConfig.defaultConfig), chainAccountResponse: chainAccountResponse, localizationManager: LocalizationManager.shared, dataValidatingFactory: dataValidatingFactory, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 556002435e..12187f8378 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -26,7 +26,7 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { private var feeChainAsset: ChainAsset? private var feeIdentifier: SwapSetupFeeIdentifier? - private var slippage: BigRational? + private var slippage: BigRational private var depositOperations: [DepositOperationModel] = [] private var purchaseActions: [PurchaseAction] = [] private var depositCrossChainAssets: [ChainAsset] = [] @@ -41,6 +41,7 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { dataValidatingFactory: SwapDataValidatorFactoryProtocol, localizationManager: LocalizationManagerProtocol, selectedWallet: MetaAccountModel, + slippageConfig: SlippageConfig, purchaseProvider: PurchaseProviderProtocol, logger: LoggerProtocol ) { @@ -50,6 +51,7 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { self.wireframe = wireframe self.viewModelFactory = viewModelFactory self.issuesViewModelFactory = issuesViewModelFactory + slippage = slippageConfig.defaultSlippage self.purchaseProvider = purchaseProvider super.init( @@ -88,7 +90,7 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { quoteArgs } - override func getSlippage() -> BigRational? { + override func getSlippage() -> BigRational { slippage } @@ -104,8 +106,7 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { guard let quote = quote, let receiveChain = receiveChainAsset?.chain, let accountId = selectedWallet.fetch(for: receiveChain.accountRequest())?.accountId, - let quoteArgs = quoteArgs, - let slippage = slippage else { + let quoteArgs = quoteArgs else { return } @@ -567,8 +568,6 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { provideDetailsViewModel(isAvailable: false) provideButtonState() provideSettingsState() - // TODO: get from settings - slippage = .fraction(from: AssetConversionConstants.defaultSlippage)?.fromPercents() provideIssues() interactor.setup() diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index f26e5e767e..bd60661e35 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -57,6 +57,7 @@ struct SwapSetupViewFactory { dataValidatingFactory: dataValidatingFactory, localizationManager: LocalizationManager.shared, selectedWallet: selectedWallet, + slippageConfig: .defaultConfig, purchaseProvider: PurchaseAggregator.defaultAggregator(), logger: Logger.shared ) diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index b58e8eb3fc..76e2c3993e 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -8,10 +8,12 @@ final class SwapSlippagePresenter { let numberFormatterLocalizable: LocalizableResource let percentFormatterLocalizable: LocalizableResource let completionHandler: (BigRational) -> Void - let prefilledPercents: [Decimal] = [0.1, 1, 3] - let initPercent: BigRational? let chainAsset: ChainAsset - let bounds = SlippageBounds() + + let initSlippage: Decimal? + let defaultSlippage: Decimal + let slippageTips: [Decimal] + let bounds: SlippageBounds private var percentFormatter: NumberFormatter private var numberFormatter: NumberFormatter @@ -22,14 +24,19 @@ final class SwapSlippagePresenter { numberFormatterLocalizable: LocalizableResource, percentFormatterLocalizable: LocalizableResource, localizationManager: LocalizationManagerProtocol, - initPercent: BigRational?, + initSlippage: BigRational?, + config: SlippageConfig, chainAsset: ChainAsset, completionHandler: @escaping (BigRational) -> Void ) { self.wireframe = wireframe self.numberFormatterLocalizable = numberFormatterLocalizable self.percentFormatterLocalizable = percentFormatterLocalizable - self.initPercent = initPercent + self.initSlippage = initSlippage?.decimalValue + defaultSlippage = config.defaultSlippage.toPercents().decimalOrZeroValue + bounds = .init(config: config) + slippageTips = config.slippageTips.map { $0.toPercents().decimalOrZeroValue } + self.chainAsset = chainAsset self.completionHandler = completionHandler percentFormatter = percentFormatterLocalizable.value(for: localizationManager.selectedLocale) @@ -45,17 +52,13 @@ final class SwapSlippagePresenter { percent / (percentFormatter.multiplier?.decimalValue ?? 1) } - private func initialPercent() -> Decimal? { - initPercent?.decimalValue - } - private func provideAmountViewModel() { let inputViewModel = AmountInputViewModel( symbol: "", amount: amountInput, limit: 100, formatter: numberFormatter, - precision: 1 + precision: 4 ) view?.didReceiveInput(viewModel: inputViewModel) @@ -67,10 +70,11 @@ final class SwapSlippagePresenter { stringAmountClosure: title, locale: selectedLocale ) - let canReset = amountInput != AssetConversionConstants.defaultSlippage + + let canReset = amountInput != defaultSlippage view?.didReceiveResetState(available: canReset) - let canApply = amountInput != initialPercent() && error == nil + let canApply = amountInput != initSlippage && error == nil view?.didReceiveButtonState(available: canApply) } @@ -92,14 +96,14 @@ final class SwapSlippagePresenter { extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { func setup() { - let viewModel = prefilledPercents.map { + let viewModel = slippageTips.map { SlippagePercentViewModel( value: $0, title: title(for: $0) ) } - amountInput = initialPercent() + amountInput = initSlippage provideButtonStates() provideAmountViewModel() provideWarnings() @@ -126,7 +130,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { } func reset() { - amountInput = AssetConversionConstants.defaultSlippage + amountInput = defaultSlippage provideAmountViewModel() provideButtonStates() provideErrors() diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift index e1371b60cc..57409e8bf4 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift @@ -17,7 +17,8 @@ struct SwapSlippageViewFactory { numberFormatterLocalizable: amountFormatter.localizableResource(), percentFormatterLocalizable: percentFormatter.localizableResource(), localizationManager: LocalizationManager.shared, - initPercent: percent?.toPercents(), + initSlippage: percent?.toPercents(), + config: SlippageConfig.defaultConfig, chainAsset: chainAsset, completionHandler: completionHandler ) From 239a561f60a9b27ea28cd927ade243b22fb0c3fe Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 7 Nov 2023 16:12:57 +0100 Subject: [PATCH 128/204] fix slipage logic --- novawallet.xcodeproj/project.pbxproj | 8 +++ .../Foundation/Decimal+Percents.swift | 11 +++ novawallet/Common/Model/BigRational.swift | 16 ----- .../Modules/Swaps/Base/SlippageBounds.swift | 12 ++-- .../Swaps/Confirm/SwapConfirmPresenter.swift | 2 +- .../AmountInputViewModel+Slippage.swift | 18 +++++ .../Slippage/SwapSlippagePresenter.swift | 70 +++++++++---------- .../Slippage/SwapSlippageViewFactory.swift | 6 +- 8 files changed, 82 insertions(+), 61 deletions(-) create mode 100644 novawallet/Common/Extension/Foundation/Decimal+Percents.swift create mode 100644 novawallet/Modules/Swaps/Slippage/AmountInputViewModel+Slippage.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 90b5ee6c7c..1b09839a93 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -77,6 +77,8 @@ 0C13DFD52AFA4F1500E5F355 /* SwapIssueCheckParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFD42AFA4F1500E5F355 /* SwapIssueCheckParams.swift */; }; 0C13DFD72AFA50A200E5F355 /* SwapIssueViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFD62AFA50A200E5F355 /* SwapIssueViewModelFactory.swift */; }; 0C13DFDB2AFA691400E5F355 /* SlippageConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFDA2AFA691400E5F355 /* SlippageConfig.swift */; }; + 0C13DFDD2AFA82BA00E5F355 /* AmountInputViewModel+Slippage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFDC2AFA82BA00E5F355 /* AmountInputViewModel+Slippage.swift */; }; + 0C13DFDF2AFA89EF00E5F355 /* Decimal+Percents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFDE2AFA89EF00E5F355 /* Decimal+Percents.swift */; }; 0C154A0E2A45995500932C3F /* CompoundComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C154A0D2A45995500932C3F /* CompoundComparator.swift */; }; 0C17BD972A42F162004AF9E7 /* WalletViewModelObserverContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */; }; 0C17BD992A42F1BE004AF9E7 /* MoneyPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */; }; @@ -4137,6 +4139,8 @@ 0C13DFD42AFA4F1500E5F355 /* SwapIssueCheckParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapIssueCheckParams.swift; sourceTree = ""; }; 0C13DFD62AFA50A200E5F355 /* SwapIssueViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapIssueViewModelFactory.swift; sourceTree = ""; }; 0C13DFDA2AFA691400E5F355 /* SlippageConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlippageConfig.swift; sourceTree = ""; }; + 0C13DFDC2AFA82BA00E5F355 /* AmountInputViewModel+Slippage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AmountInputViewModel+Slippage.swift"; sourceTree = ""; }; + 0C13DFDE2AFA89EF00E5F355 /* Decimal+Percents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+Percents.swift"; sourceTree = ""; }; 0C154A0D2A45995500932C3F /* CompoundComparator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompoundComparator.swift; sourceTree = ""; }; 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletViewModelObserverContainer.swift; sourceTree = ""; }; 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyPresentable.swift; sourceTree = ""; }; @@ -9092,6 +9096,7 @@ F9F3BD600F80ED0426141843 /* SwapSlippageViewController.swift */, 2E965356C7C646CB86BBEBB6 /* SwapSlippageViewLayout.swift */, 28294C13CF8F62D2FE4D0427 /* SwapSlippageViewFactory.swift */, + 0C13DFDC2AFA82BA00E5F355 /* AmountInputViewModel+Slippage.swift */, ); path = Slippage; sourceTree = ""; @@ -13662,6 +13667,7 @@ 0C7C9B982ABFF355009A0362 /* String+Html.swift */, 0C7C9B9A2AC16D7B009A0362 /* NSAttributedString+Helpers.swift */, 0C13DFCE2AF8ADB300E5F355 /* BigUInt+Operation.swift */, + 0C13DFDE2AFA89EF00E5F355 /* Decimal+Percents.swift */, ); path = Foundation; sourceTree = ""; @@ -20532,6 +20538,7 @@ 77A6F5C92A2F1EFC004AFD1A /* AssetOperationViewFactory.swift in Sources */, 848E6BD92761CAC800C91022 /* StakingRewardView.swift in Sources */, F4B39C592732712C00BB6E10 /* AcalaContributionMethod.swift in Sources */, + 0C13DFDF2AFA89EF00E5F355 /* Decimal+Percents.swift in Sources */, AEA0C8BA268113F900F9666F /* YourValidatorList+RecommendedList.swift in Sources */, 88421056289BBA8D00306F2C /* CurrencyPresenter.swift in Sources */, 84D33996262250B800130A89 /* String+Substrate.swift in Sources */, @@ -22683,6 +22690,7 @@ 88E74E8C29538C36008031A3 /* QRCodeInfo.swift in Sources */, 777BD86229F97322004969A2 /* ReferendumsFilter.swift in Sources */, 84282288289AC80600163031 /* MultiValueView+Factory.swift in Sources */, + 0C13DFDD2AFA82BA00E5F355 /* AmountInputViewModel+Slippage.swift in Sources */, 99570B581EF2EE8A1070442F /* CreateWatchOnlyViewFactory.swift in Sources */, 6003DF3EBB77510EFB70B4E4 /* MessageSheetProtocols.swift in Sources */, 0C13D31D2A82279D0054BB6F /* StartStakingDirectConfirmPresenter.swift in Sources */, diff --git a/novawallet/Common/Extension/Foundation/Decimal+Percents.swift b/novawallet/Common/Extension/Foundation/Decimal+Percents.swift new file mode 100644 index 0000000000..de75fa66d8 --- /dev/null +++ b/novawallet/Common/Extension/Foundation/Decimal+Percents.swift @@ -0,0 +1,11 @@ +import Foundation + +extension Decimal { + func fromFractionToPercents() -> Decimal { + self * 100 + } + + func fromPercentsToFraction() -> Decimal { + self / 100 + } +} diff --git a/novawallet/Common/Model/BigRational.swift b/novawallet/Common/Model/BigRational.swift index fbbeb48263..6b520bfd89 100644 --- a/novawallet/Common/Model/BigRational.swift +++ b/novawallet/Common/Model/BigRational.swift @@ -11,22 +11,6 @@ struct BigRational: Hashable { } extension BigRational { - func toPercents() -> BigRational { - let (quotient, reminder) = denominator.quotientAndRemainder(dividingBy: 100) - - if quotient > 0, reminder == 0 { - return .init(numerator: numerator, denominator: quotient) - } else { - return .init(numerator: numerator * 100, denominator: denominator) - } - } - - func fromPercents() -> BigRational { - let newDenominator = denominator * 100 - - return .init(numerator: numerator, denominator: newDenominator) - } - static func percent(of numerator: BigUInt) -> BigRational { .init(numerator: numerator, denominator: 100) } diff --git a/novawallet/Modules/Swaps/Base/SlippageBounds.swift b/novawallet/Modules/Swaps/Base/SlippageBounds.swift index 635a239922..394e7ebd15 100644 --- a/novawallet/Modules/Swaps/Base/SlippageBounds.swift +++ b/novawallet/Modules/Swaps/Base/SlippageBounds.swift @@ -11,13 +11,13 @@ struct SlippageBounds { init(config: SlippageConfig) { restriction = .init( - lower: config.minAvailableSlippage.toPercents().decimalOrZeroValue, - upper: config.maxAvailableSlippage.toPercents().decimalOrZeroValue + lower: config.minAvailableSlippage.decimalOrZeroValue, + upper: config.maxAvailableSlippage.decimalOrZeroValue ) recommendation = .init( - lower: config.smallSlippage.toPercents().decimalOrZeroValue, - upper: config.bigSlippage.toPercents().decimalOrZeroValue + lower: config.smallSlippage.decimalOrZeroValue, + upper: config.bigSlippage.decimalOrZeroValue ) } } @@ -27,12 +27,12 @@ extension SlippageBounds { guard let value = value, value > 0 else { return nil } - if value <= recommendation.lower { + if value < recommendation.lower { let warning = R.string.localizable.swapsSetupSlippageWarningLowAmount( preferredLanguages: locale.rLanguages ) return warning - } else if value >= recommendation.upper { + } else if value > recommendation.upper { let warning = R.string.localizable.swapsSetupSlippageWarningHighAmount( preferredLanguages: locale.rLanguages ) diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index c637baf160..98319fac1d 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -111,7 +111,7 @@ final class SwapConfirmPresenter { let viewModel = viewModelFactory.slippageViewModel(slippage: initState.slippage) view?.didReceiveSlippage(viewModel: viewModel) let warning = slippageBounds.warning( - for: initState.slippage.toPercents().decimalValue, + for: initState.slippage.decimalValue, locale: selectedLocale ) view?.didReceiveWarning(viewModel: warning) diff --git a/novawallet/Modules/Swaps/Slippage/AmountInputViewModel+Slippage.swift b/novawallet/Modules/Swaps/Slippage/AmountInputViewModel+Slippage.swift new file mode 100644 index 0000000000..97af68c178 --- /dev/null +++ b/novawallet/Modules/Swaps/Slippage/AmountInputViewModel+Slippage.swift @@ -0,0 +1,18 @@ +import Foundation + +extension AmountInputViewModel { + static func forAssetConversionSlippage(for amount: Decimal?, locale: Locale) -> AmountInputViewModel { + let precision: Int16 = 4 + let numberFormatter = NumberFormatter.amount.localizableResource().value(for: locale) + numberFormatter.maximumFractionDigits = Int(precision) + numberFormatter.maximumSignificantDigits = Int(precision) + + return .init( + symbol: "", + amount: amount, + limit: 100, + formatter: numberFormatter, + precision: precision + ) + } +} diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift index 76e2c3993e..0e61b9f782 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippagePresenter.swift @@ -5,7 +5,6 @@ import BigInt final class SwapSlippagePresenter { weak var view: SwapSlippageViewProtocol? let wireframe: SwapSlippageWireframeProtocol - let numberFormatterLocalizable: LocalizableResource let percentFormatterLocalizable: LocalizableResource let completionHandler: (BigRational) -> Void let chainAsset: ChainAsset @@ -15,13 +14,10 @@ final class SwapSlippagePresenter { let slippageTips: [Decimal] let bounds: SlippageBounds - private var percentFormatter: NumberFormatter - private var numberFormatter: NumberFormatter private var amountInput: Decimal? init( wireframe: SwapSlippageWireframeProtocol, - numberFormatterLocalizable: LocalizableResource, percentFormatterLocalizable: LocalizableResource, localizationManager: LocalizationManagerProtocol, initSlippage: BigRational?, @@ -30,35 +26,40 @@ final class SwapSlippagePresenter { completionHandler: @escaping (BigRational) -> Void ) { self.wireframe = wireframe - self.numberFormatterLocalizable = numberFormatterLocalizable self.percentFormatterLocalizable = percentFormatterLocalizable self.initSlippage = initSlippage?.decimalValue - defaultSlippage = config.defaultSlippage.toPercents().decimalOrZeroValue + defaultSlippage = config.defaultSlippage.decimalOrZeroValue bounds = .init(config: config) - slippageTips = config.slippageTips.map { $0.toPercents().decimalOrZeroValue } + slippageTips = config.slippageTips.map(\.decimalOrZeroValue) self.chainAsset = chainAsset self.completionHandler = completionHandler - percentFormatter = percentFormatterLocalizable.value(for: localizationManager.selectedLocale) - numberFormatter = numberFormatterLocalizable.value(for: localizationManager.selectedLocale) self.localizationManager = localizationManager } - private func title(for percent: Decimal) -> String { - percentFormatter.stringFromDecimal(value(for: percent)) ?? "" + private func provideTips() { + let formatter = percentFormatterLocalizable.value(for: selectedLocale) + + let tips = slippageTips.map { + SlippagePercentViewModel( + value: $0, + title: formatter.stringFromDecimal($0) ?? "" + ) + } + + view?.didReceivePreFilledPercents(viewModel: tips) } - private func value(for percent: Decimal) -> Decimal { - percent / (percentFormatter.multiplier?.decimalValue ?? 1) + private func percentToString(from decimal: Decimal) -> String { + percentFormatterLocalizable + .value(for: selectedLocale) + .stringFromDecimal(decimal) ?? "" } private func provideAmountViewModel() { - let inputViewModel = AmountInputViewModel( - symbol: "", - amount: amountInput, - limit: 100, - formatter: numberFormatter, - precision: 4 + let inputViewModel = AmountInputViewModel.forAssetConversionSlippage( + for: amountInput?.fromFractionToPercents(), + locale: selectedLocale ) view?.didReceiveInput(viewModel: inputViewModel) @@ -67,7 +68,7 @@ final class SwapSlippagePresenter { private func provideButtonStates() { let error = bounds.error( for: amountInput, - stringAmountClosure: title, + stringAmountClosure: percentToString, locale: selectedLocale ) @@ -81,7 +82,7 @@ final class SwapSlippagePresenter { private func provideErrors() { let error = bounds.error( for: amountInput, - stringAmountClosure: title, + stringAmountClosure: percentToString, locale: selectedLocale ) view?.didReceiveInput(error: error) @@ -92,22 +93,20 @@ final class SwapSlippagePresenter { let warning = bounds.warning(for: amountInput, locale: selectedLocale) view?.didReceiveInput(warning: warning) } + + private func updateView() { + provideAmountViewModel() + provideButtonStates() + provideWarnings() + provideErrors() + provideTips() + } } extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { func setup() { - let viewModel = slippageTips.map { - SlippagePercentViewModel( - value: $0, - title: title(for: $0) - ) - } - amountInput = initSlippage - provideButtonStates() - provideAmountViewModel() - provideWarnings() - view?.didReceivePreFilledPercents(viewModel: viewModel) + updateView() } func select(percent: SlippagePercentViewModel) { @@ -119,7 +118,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { } func updateAmount(_ amount: Decimal?) { - amountInput = amount + amountInput = amount?.fromPercentsToFraction() provideButtonStates() provideErrors() provideWarnings() @@ -139,7 +138,7 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { func apply() { if let amountInput = amountInput, - let rational = BigRational.fraction(from: amountInput)?.fromPercents() { + let rational = BigRational.fraction(from: amountInput) { completionHandler(rational) wireframe.close(from: view) } @@ -148,7 +147,6 @@ extension SwapSlippagePresenter: SwapSlippagePresenterProtocol { extension SwapSlippagePresenter: Localizable { func applyLocalization() { - percentFormatter = percentFormatterLocalizable.value(for: selectedLocale) - numberFormatter = numberFormatterLocalizable.value(for: selectedLocale) + updateView() } } diff --git a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift index 57409e8bf4..531c445b14 100644 --- a/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift +++ b/novawallet/Modules/Swaps/Slippage/SwapSlippageViewFactory.swift @@ -10,14 +10,16 @@ struct SwapSlippageViewFactory { let wireframe = SwapSlippageWireframe() let amountFormatter = NumberFormatter.amount + amountFormatter.maximumFractionDigits = 4 + amountFormatter.maximumSignificantDigits = 4 + let percentFormatter = NumberFormatter.percentSingle let presenter = SwapSlippagePresenter( wireframe: wireframe, - numberFormatterLocalizable: amountFormatter.localizableResource(), percentFormatterLocalizable: percentFormatter.localizableResource(), localizationManager: LocalizationManager.shared, - initSlippage: percent?.toPercents(), + initSlippage: percent, config: SlippageConfig.defaultConfig, chainAsset: chainAsset, completionHandler: completionHandler From b6c009ac0aa3172115addb311daa9ae4b80e4891 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 7 Nov 2023 18:11:44 +0100 Subject: [PATCH 129/204] add logic to confirm screen --- .../Validation/DataValidationProtocols.swift | 18 +- .../Validation/DataValidationRunner.swift | 21 +- .../Swaps/Base/SwapBasePresenter.swift | 1 + .../Swaps/Confirm/SwapConfirmPresenter.swift | 406 ++++++++---------- .../Confirm/SwapConfirmViewFactory.swift | 13 +- .../Swaps/Setup/SwapSetupPresenter.swift | 47 +- .../Swaps/Setup/SwapSetupProtocols.swift | 2 + .../Swaps/Setup/SwapSetupViewController.swift | 8 + .../Setup/View/SwapSetupViewLayout.swift | 10 +- 9 files changed, 269 insertions(+), 257 deletions(-) diff --git a/novawallet/Common/Validation/DataValidationProtocols.swift b/novawallet/Common/Validation/DataValidationProtocols.swift index 565d7a8949..cf696efbaa 100644 --- a/novawallet/Common/Validation/DataValidationProtocols.swift +++ b/novawallet/Common/Validation/DataValidationProtocols.swift @@ -1,6 +1,8 @@ import Foundation typealias DataValidationRunnerCompletion = () -> Void +typealias DataValidationRunnerStopClosure = (DataValidationProblem) -> Void +typealias DataValidationRunnerResumeClosure = (Int) -> Void enum DataValidationProblem { case warning @@ -18,5 +20,19 @@ protocol DataValidating { } protocol DataValidationRunnerProtocol { - func runValidation(notifyingOnSuccess closure: @escaping DataValidationRunnerCompletion) + func runValidation( + notifyingOnSuccess completionClosure: @escaping DataValidationRunnerCompletion, + notifyingOnStop stopClosure: DataValidationRunnerStopClosure?, + notifyingOnResume resumeClosure: DataValidationRunnerResumeClosure? + ) +} + +extension DataValidationRunnerProtocol { + func runValidation(notifyingOnSuccess closure: @escaping DataValidationRunnerCompletion) { + runValidation( + notifyingOnSuccess: closure, + notifyingOnStop: nil, + notifyingOnResume: nil + ) + } } diff --git a/novawallet/Common/Validation/DataValidationRunner.swift b/novawallet/Common/Validation/DataValidationRunner.swift index 24521c9e8c..f92aad11ce 100644 --- a/novawallet/Common/Validation/DataValidationRunner.swift +++ b/novawallet/Common/Validation/DataValidationRunner.swift @@ -4,7 +4,9 @@ final class DataValidationRunner { let validators: [DataValidating] private var lastIndex: Int = 0 - private var savedClosure: DataValidationRunnerCompletion? + private var completionClosure: DataValidationRunnerCompletion? + private var resumeClosure: DataValidationRunnerResumeClosure? + private var stopClosure: DataValidationRunnerStopClosure? init(validators: [DataValidating]) { self.validators = validators @@ -15,8 +17,12 @@ final class DataValidationRunner { } private func runValidation(from startIndex: Int) { + resumeClosure?(startIndex) + for index in startIndex ..< validators.count { if let problem = validators[index].validate(notifying: self) { + stopClosure?(problem) + switch problem { case .warning: lastIndex = index @@ -30,13 +36,20 @@ final class DataValidationRunner { } } - savedClosure?() + completionClosure?() } } extension DataValidationRunner: DataValidationRunnerProtocol { - func runValidation(notifyingOnSuccess closure: @escaping DataValidationRunnerCompletion) { - savedClosure = closure + func runValidation( + notifyingOnSuccess completionClosure: @escaping DataValidationRunnerCompletion, + notifyingOnStop stopClosure: DataValidationRunnerStopClosure?, + notifyingOnResume resumeClosure: DataValidationRunnerResumeClosure? + ) { + self.completionClosure = completionClosure + self.stopClosure = stopClosure + self.resumeClosure = resumeClosure + runValidation(from: 0) } } diff --git a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift index d87cf5d601..1ca7a73947 100644 --- a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift +++ b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift @@ -281,6 +281,7 @@ class SwapBasePresenter { }, onQuoteUpdate: { [weak self] quote in self?.quoteResult = .success(quote) + self?.handleNewQuote(quote, for: swapModel.quoteArgs) }, locale: locale ) diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 98319fac1d..bf0a9b5bf3 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -2,55 +2,185 @@ import Foundation import BigInt import SoraFoundation -final class SwapConfirmPresenter { +final class SwapConfirmPresenter: SwapBasePresenter { weak var view: SwapConfirmViewProtocol? let wireframe: SwapConfirmWireframeProtocol let interactor: SwapConfirmInteractorInputProtocol - let dataValidatingFactory: SwapDataValidatorFactoryProtocol let initState: SwapConfirmInitState let slippageBounds: SlippageBounds + var accountId: AccountId? { + selectedWallet.fetch(for: initState.chainAssetOut.chain.accountRequest())?.accountId + } + private var viewModelFactory: SwapConfirmViewModelFactoryProtocol - private var feePriceData: PriceData? - private var chainAssetInPriceData: PriceData? - private var chainAssetOutPriceData: PriceData? - private var quote: AssetConversion.Quote? - private var fee: AssetConversion.FeeModel? - private var payAccountId: AccountId? - private var chainAccountResponse: MetaChainAccountResponse - private var balances: [ChainAssetId: AssetBalance?] = [:] - private var assetBalanceExistences: [ChainAssetId: AssetBalanceExistence] = [:] + + private var quoteArgs: AssetConversion.QuoteArgs init( interactor: SwapConfirmInteractorInputProtocol, wireframe: SwapConfirmWireframeProtocol, + initState: SwapConfirmInitState, + selectedWallet: MetaAccountModel, viewModelFactory: SwapConfirmViewModelFactoryProtocol, slippageBounds: SlippageBounds, - chainAccountResponse: MetaChainAccountResponse, - localizationManager: LocalizationManagerProtocol, dataValidatingFactory: SwapDataValidatorFactoryProtocol, - initState: SwapConfirmInitState + localizationManager: LocalizationManagerProtocol, + logger: LoggerProtocol ) { self.interactor = interactor self.wireframe = wireframe self.viewModelFactory = viewModelFactory self.slippageBounds = slippageBounds self.initState = initState - quote = initState.quote - self.chainAccountResponse = chainAccountResponse - self.dataValidatingFactory = dataValidatingFactory + quoteArgs = initState.quoteArgs + + super.init( + selectedWallet: selectedWallet, + dataValidatingFactory: dataValidatingFactory, + logger: logger + ) + + quoteResult = .success(initState.quote) self.localizationManager = localizationManager } + override func getSpendingInputAmount() -> Decimal? { + quote?.amountIn.decimal(precision: initState.chainAssetIn.asset.precision) + } + + override func getQuoteArgs() -> AssetConversion.QuoteArgs? { + quoteArgs + } + + override func getSlippage() -> BigRational { + initState.slippage + } + + override func getPayChainAsset() -> ChainAsset? { + initState.chainAssetIn + } + + override func getReceiveChainAsset() -> ChainAsset? { + initState.chainAssetOut + } + + override func getFeeChainAsset() -> ChainAsset? { + initState.feeChainAsset + } + + override func shouldHandleQuote(for quoteArgs: AssetConversion.QuoteArgs?) -> Bool { + self.quoteArgs == quoteArgs + } + + override func shouldHandleFee(for _: TransactionFeeId, feeChainAssetId _: ChainAssetId?) -> Bool { + true + } + + override func estimateFee() { + guard let quote = quote, + let accountId = selectedWallet.fetch( + for: initState.chainAssetOut.chain.accountRequest() + )?.accountId else { + return + } + + fee = nil + provideFeeViewModel() + + interactor.calculateFee( + args: .init( + assetIn: quote.assetIn, + amountIn: quote.amountIn, + assetOut: quote.assetOut, + amountOut: quote.amountOut, + receiver: accountId, + direction: quoteArgs.direction, + slippage: initState.slippage + ) + ) + } + + override func applySwapMax() { + guard + let maxAmount = getMaxModel()?.calculate(), + maxAmount > 0, + let maxAmountInPlank = maxAmount.toSubstrateAmount( + precision: initState.chainAssetIn.assetDisplayInfo.assetPrecision + ) else { + return + } + + quoteArgs = AssetConversion.QuoteArgs( + assetIn: initState.quoteArgs.assetIn, + assetOut: initState.quoteArgs.assetOut, + amount: maxAmountInPlank, + direction: .sell + ) + + view?.didReceiveStartLoading() + + interactor.calculateQuote(for: quoteArgs) + } + + override func handleBaseError(_ error: SwapBaseError) { + handleBaseError( + error, + view: view, + interactor: interactor, + wireframe: wireframe, + locale: selectedLocale + ) + + if case .quote = error { + view?.didReceiveStopLoading() + } + } + + override func handleNewQuote(_ quote: AssetConversion.Quote, for _: AssetConversion.QuoteArgs) { + quoteResult = .success(quote) + + updateViews() + estimateFee() + } + + override func handleNewFee( + _: AssetConversion.FeeModel?, + transactionFeeId _: TransactionFeeId, + feeChainAssetId _: ChainAssetId? + ) { + provideFeeViewModel() + } + + override func handleNewPrice(_: PriceData?, chainAssetId: ChainAssetId) { + if initState.chainAssetIn.chainAssetId == chainAssetId { + provideAssetInViewModel() + providePriceDifferenceViewModel() + } + + if initState.chainAssetOut.chainAssetId == chainAssetId { + provideAssetOutViewModel() + providePriceDifferenceViewModel() + } + + if initState.feeChainAsset.chainAssetId == chainAssetId { + provideFeeViewModel() + } + } +} + +extension SwapConfirmPresenter { private func provideAssetInViewModel() { guard let quote = quote else { return } + let viewModel = viewModelFactory.assetViewModel( chainAsset: initState.chainAssetIn, amount: quote.amountIn, - priceData: chainAssetInPriceData + priceData: payAssetPriceData ) + view?.didReceiveAssetIn(viewModel: viewModel) } @@ -61,7 +191,7 @@ final class SwapConfirmPresenter { let viewModel = viewModelFactory.assetViewModel( chainAsset: initState.chainAssetOut, amount: quote.amountOut, - priceData: chainAssetOutPriceData + priceData: receiveAssetPriceData ) view?.didReceiveAssetOut(viewModel: viewModel) } @@ -98,8 +228,8 @@ final class SwapConfirmPresenter { if let viewModel = viewModelFactory.priceDifferenceViewModel( rateParams: params, - priceIn: chainAssetInPriceData, - priceOut: chainAssetOutPriceData + priceIn: payAssetPriceData, + priceOut: receiveAssetPriceData ) { view?.didReceivePriceDifference(viewModel: .loaded(value: viewModel)) } else { @@ -122,16 +252,23 @@ final class SwapConfirmPresenter { view?.didReceiveNetworkFee(viewModel: .loading) return } + let viewModel = viewModelFactory.feeViewModel( fee: fee.networkFee.targetAmount, chainAsset: initState.feeChainAsset, - priceData: feePriceData + priceData: feeAssetPriceData ) view?.didReceiveNetworkFee(viewModel: .loaded(value: viewModel)) } private func provideWalletViewModel() { + guard let chainAccountResponse = selectedWallet.fetchMetaChainAccount( + for: initState.chainAssetOut.chain.accountRequest() + ) else { + return + } + guard let walletAddress = WalletDisplayAddress(response: chainAccountResponse) else { view?.didReceiveWallet(viewModel: nil) return @@ -151,67 +288,11 @@ final class SwapConfirmPresenter { provideWalletViewModel() } - private func estimateFee() { - guard let quote = quote, - let accountId = payAccountId else { - return - } - - fee = nil - provideFeeViewModel() - - interactor.calculateFee(args: .init( - assetIn: quote.assetIn, - amountIn: quote.amountIn, - assetOut: quote.assetOut, - amountOut: quote.amountOut, - receiver: accountId, - direction: initState.quoteArgs.direction, - slippage: initState.slippage - ) - ) - } - - private func validators( - spendingAmount: Decimal? - ) -> [DataValidating] { - let feeDecimal = fee.map { Decimal.fromSubstrateAmount( - $0.totalFee.targetAmount, - precision: Int16(initState.feeChainAsset.asset.precision) - ) } ?? nil - - let feeInPayAsset = initState.chainAssetIn.chainAssetId == initState.feeChainAsset.chainAssetId ? - fee?.totalFee.targetAmount : 0 - - let payAssetBalance = balances[initState.chainAssetIn.chainAssetId] - - let validators: [DataValidating] = [ - dataValidatingFactory.has(fee: feeDecimal, locale: selectedLocale) { [weak self] in - self?.estimateFee() - }, - dataValidatingFactory.canSpendAmountInPlank( - balance: payAssetBalance??.transferable, - spendingAmount: spendingAmount, - asset: initState.chainAssetIn.assetDisplayInfo, - locale: selectedLocale - ), - dataValidatingFactory.canPayFeeSpendingAmountInPlank( - balance: payAssetBalance??.transferable, - fee: feeInPayAsset, - spendingAmount: spendingAmount, - asset: initState.feeChainAsset.assetDisplayInfo, - locale: selectedLocale - ) - ] - - return validators - } - private func submit() { - guard let quote = quote, - let accountId = payAccountId else { + guard let quote = quote, let accountId = accountId else { return } + let args = AssetConversion.CallArgs( assetIn: quote.assetIn, amountIn: quote.amountIn, @@ -222,59 +303,9 @@ final class SwapConfirmPresenter { slippage: initState.slippage ) - interactor.submit(args: args) - } - - private func checkRateChanged( - oldValue: AssetConversion.Quote, - newValue: AssetConversion.Quote, - confirmClosure: @escaping () -> Void - ) { - guard oldValue != newValue else { - confirmClosure() - return - } - let oldRateParams = RateParams( - assetDisplayInfoIn: initState.chainAssetIn.assetDisplayInfo, - assetDisplayInfoOut: initState.chainAssetOut.assetDisplayInfo, - amountIn: oldValue.amountIn, - amountOut: oldValue.amountOut - ) - let newRateParams = RateParams( - assetDisplayInfoIn: initState.chainAssetIn.assetDisplayInfo, - assetDisplayInfoOut: initState.chainAssetOut.assetDisplayInfo, - amountIn: newValue.amountIn, - amountOut: newValue.amountOut - ) - - let oldRate = viewModelFactory.rateViewModel(from: oldRateParams) - let newRate = viewModelFactory.rateViewModel(from: newRateParams) + view?.didReceiveStartLoading() - let title = R.string.localizable.swapsErrorRateWasUpdatedTitle( - preferredLanguages: selectedLocale.rLanguages - ) - let message = R.string.localizable.swapsErrorRateWasUpdatedMessage( - oldRate, - newRate, - preferredLanguages: selectedLocale.rLanguages - ) - let confirmTitle = R.string.localizable.commonConfirm( - preferredLanguages: selectedLocale.rLanguages - ) - let cancelTitle = R.string.localizable.commonCancel( - preferredLanguages: selectedLocale.rLanguages - ) - let confirmAction = AlertPresentableAction(title: confirmTitle, handler: confirmClosure) - wireframe.present( - viewModel: .init( - title: title, - message: message, - actions: [confirmAction], - closeAction: cancelTitle - ), - style: .alert, - from: view - ) + interactor.submit(args: args) } } @@ -319,7 +350,8 @@ extension SwapConfirmPresenter: SwapConfirmPresenterProtocol { guard let view = view else { return } - guard let address = chainAccountResponse.chainAccount.toAddress() else { + + guard let address = try? accountId?.toAddress(using: initState.chainAssetOut.chain.chainFormat) else { return } @@ -332,101 +364,31 @@ extension SwapConfirmPresenter: SwapConfirmPresenterProtocol { } func confirm() { - view?.didReceiveStartLoading() - let spendingAmount = quote?.amountIn.decimal(precision: initState.chainAssetIn.asset.precision) - - let validators = validators(spendingAmount: spendingAmount) - - DataValidationRunner(validators: validators).runValidation { [weak self] in - self?.submit() - } - } -} - -extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { - func didReceive(quote: AssetConversion.Quote, for _: AssetConversion.QuoteArgs) { - checkRateChanged( - oldValue: self.quote ?? initState.quote, - newValue: quote - ) { [weak self] in - self?.quote = quote - self?.provideAssetInViewModel() - self?.provideAssetOutViewModel() - self?.provideRateViewModel() - self?.providePriceDifferenceViewModel() - self?.estimateFee() - } - } - - func didReceive( - fee: AssetConversion.FeeModel?, - transactionFeeId _: TransactionFeeId, - feeChainAssetId _: ChainAssetId? - ) { - self.fee = fee - provideFeeViewModel() - } - - func didReceive(price: PriceData?, priceId: AssetModel.PriceId) { - if priceId == initState.chainAssetIn.asset.priceId { - chainAssetInPriceData = price - provideAssetInViewModel() - providePriceDifferenceViewModel() - } - if priceId == initState.chainAssetOut.asset.priceId { - chainAssetOutPriceData = price - provideAssetOutViewModel() - providePriceDifferenceViewModel() - } - if priceId == initState.feeChainAsset.asset.priceId { - feePriceData = price - provideFeeViewModel() + guard let swapModel = getSwapModel() else { + return } - } - func didReceive(balance: AssetBalance?, for chainAsset: ChainAssetId) { - balances[chainAsset] = balance - } - - func didReceiveAssetBalance(existense: AssetBalanceExistence, chainAssetId: ChainAssetId) { - assetBalanceExistences[chainAssetId] = existense - } + let validators = getBaseValidations( + for: swapModel, + interactor: interactor, + locale: selectedLocale + ) - func didReceive(baseError: SwapBaseError) { - switch baseError { - case let .quote(_, args): - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.interactor.calculateQuote(for: args) - } - case .fetchFeeFailed: - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.estimateFee() - } - case let .price(_, priceId): - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - guard let self = self else { - return - } - [self.initState.chainAssetIn, self.initState.chainAssetOut, self.initState.feeChainAsset] - .compactMap { $0 } - .filter { $0.asset.priceId == priceId } - .forEach(self.interactor.remakePriceSubscription) - } - case .assetBalance: - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.interactor.setup() - } - case let .assetBalanceExistense(_, chainAsset): - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.interactor.retryAssetBalanceExistenseFetch(for: chainAsset) + DataValidationRunner(validators: validators).runValidation( + notifyingOnSuccess: { [weak self] in + self?.submit() + }, + notifyingOnStop: { [weak self] _ in + self?.view?.didReceiveStopLoading() + }, + notifyingOnResume: { [weak self] _ in + self?.view?.didReceiveStartLoading() } - case .accountInfo: - break - } + ) } +} - func didReceive(accountInfo _: AccountInfo?, chainId _: ChainModel.Id) {} - +extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { func didReceive(error: SwapConfirmError) { view?.didReceiveStopLoading() switch error { diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index 666e2e51a0..ccfbe26522 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -9,11 +9,10 @@ struct SwapConfirmViewFactory { ) -> SwapConfirmViewProtocol? { let accountRequest = initState.chainAssetIn.chain.accountRequest() - guard let currencyManager = CurrencyManager.shared, - let wallet = SelectedWalletSettings.shared.value, - let chainAccountResponse = wallet.fetchMetaChainAccount(for: accountRequest) else { + guard let currencyManager = CurrencyManager.shared, let wallet = SelectedWalletSettings.shared.value else { return nil } + guard let interactor = createInteractor( wallet: wallet, initState: initState, @@ -21,6 +20,7 @@ struct SwapConfirmViewFactory { ) else { return nil } + let wireframe = SwapConfirmWireframe() let balanceViewModelFactoryFacade = BalanceViewModelFactoryFacade( @@ -42,12 +42,13 @@ struct SwapConfirmViewFactory { let presenter = SwapConfirmPresenter( interactor: interactor, wireframe: wireframe, + initState: initState, + selectedWallet: wallet, viewModelFactory: viewModelFactory, slippageBounds: .init(config: SlippageConfig.defaultConfig), - chainAccountResponse: chainAccountResponse, - localizationManager: LocalizationManager.shared, dataValidatingFactory: dataValidatingFactory, - initState: initState + localizationManager: LocalizationManager.shared, + logger: Logger.shared ) let view = SwapConfirmViewController( diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 12187f8378..a7b5fc4eec 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -732,27 +732,34 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { let validators = getBaseValidations(for: swapModel, interactor: interactor, locale: selectedLocale) - DataValidationRunner(validators: validators).runValidation { [weak self] in - guard let slippage = self?.slippage, - let quote = self?.quote, - let quoteArgs = self?.quoteArgs else { - return - } - - let confirmInitState = SwapConfirmInitState( - chainAssetIn: swapModel.payChainAsset, - chainAssetOut: swapModel.receiveChainAsset, - feeChainAsset: swapModel.feeChainAsset, - slippage: slippage, - quote: quote, - quoteArgs: quoteArgs - ) + DataValidationRunner(validators: validators).runValidation( + notifyingOnSuccess: { [weak self] in + guard let slippage = self?.slippage, + let quote = self?.quote, + let quoteArgs = self?.quoteArgs else { + return + } - self?.wireframe.showConfirmation( - from: self?.view, - initState: confirmInitState - ) - } + let confirmInitState = SwapConfirmInitState( + chainAssetIn: swapModel.payChainAsset, + chainAssetOut: swapModel.receiveChainAsset, + feeChainAsset: swapModel.feeChainAsset, + slippage: slippage, + quote: quote, + quoteArgs: quoteArgs + ) + + self?.wireframe.showConfirmation( + from: self?.view, + initState: confirmInitState + ) + }, + notifyingOnStop: { [weak self] _ in + self?.view?.didStopLoading() + }, notifyingOnResume: { [weak self] _ in + self?.view?.didStartLoading() + } + ) } func showSettings() { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 92e288e26e..e3f36e9fc9 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -18,6 +18,8 @@ protocol SwapSetupViewProtocol: ControllerBackedProtocol { func didReceive(issues: [SwapSetupViewIssue]) func didSetNotification(message: String?) func didReceive(focus: TextFieldFocus?) + func didStartLoading() + func didStopLoading() } protocol SwapSetupPresenterProtocol: AnyObject { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 3ba8b6f437..438dec8c31 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -300,6 +300,14 @@ extension SwapSetupViewController: SwapSetupViewProtocol { rootView.hideNotification() } } + + func didStartLoading() { + rootView.loadableActionView.startLoading() + } + + func didStopLoading() { + rootView.loadableActionView.stopLoading() + } } extension SwapSetupViewController: Localizable { diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index 22557f00a1..65f6e15a35 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -19,8 +19,10 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { let receiveAmountInputView = SwapAmountInputView() - let actionButton: TriangularedButton = .create { - $0.applyDefaultStyle() + let loadableActionView = LoadableActionView() + + var actionButton: TriangularedButton { + loadableActionView.actionButton } let switchButton: RoundedButton = .create { @@ -102,8 +104,8 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { stackView.layoutMargins = UIEdgeInsets(top: 12, left: 16, bottom: 0, right: 16) - addSubview(actionButton) - actionButton.snp.makeConstraints { make in + addSubview(loadableActionView) + loadableActionView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) make.bottom.equalTo(safeAreaLayoutGuide).inset(UIConstants.actionBottomInset) make.height.equalTo(UIConstants.actionHeight) From 02b6b5833437cb5680a2a7a67895c4d38f0808e1 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 8 Nov 2023 06:38:23 +0100 Subject: [PATCH 130/204] fix collapsing for details view --- .../View/CollapsableContainerView.swift | 30 +++++++++++-------- .../Swaps/Setup/SwapSetupViewController.swift | 22 ++++++++++++++ .../Setup/View/SwapSetupViewLayout.swift | 2 +- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/novawallet/Common/View/CollapsableContainerView.swift b/novawallet/Common/View/CollapsableContainerView.swift index 477694e955..0e4f1e3bae 100644 --- a/novawallet/Common/View/CollapsableContainerView.swift +++ b/novawallet/Common/View/CollapsableContainerView.swift @@ -1,7 +1,7 @@ import SoraUI import SnapKit -protocol CollapsableNetworkInfoViewDelegate: AnyObject { +protocol CollapsableContainerViewDelegate: AnyObject { func animateAlongsideWithInfo(sender: AnyObject?) func didChangeExpansion(isExpanded: Bool, sender: AnyObject) } @@ -53,7 +53,7 @@ class CollapsableContainerView: UIView { } } - weak var delegate: CollapsableNetworkInfoViewDelegate? + weak var delegate: CollapsableContainerViewDelegate? lazy var expansionAnimator: BlockViewAnimatorProtocol = BlockViewAnimator() @@ -78,7 +78,7 @@ class CollapsableContainerView: UIView { titleControl.deactivate(animated: animated) } - applyExpansion(animated: animated) + applyExpansion(animated: animated, shouldNotifyDelegate: false) } private func setupHandlers() { @@ -129,14 +129,14 @@ class CollapsableContainerView: UIView { } } - private func applyExpansion(animated: Bool) { + private func applyExpansion(animated: Bool, shouldNotifyDelegate: Bool) { if animated { expansionAnimator.animate(block: { [weak self] in guard let self = self else { return } - self.applyExpansionState() + self.applyExpansionState(shouldNotifyDelegate) let animation = CABasicAnimation() animation.toValue = self.backgroundView.contentView?.shapePath @@ -146,30 +146,36 @@ class CollapsableContainerView: UIView { self.delegate?.animateAlongsideWithInfo(sender: self) }, completionBlock: nil) } else { - applyExpansionState() + applyExpansionState(shouldNotifyDelegate) setNeedsLayout() } } - private func applyExpansionState() { + private func applyExpansionState(_ shouldNotifyDelegate: Bool) { if expanded { contentView.snp.updateConstraints { make in - make.top.bottom.equalToSuperview() + make.top.equalToSuperview() } layoutIfNeeded() - delegate?.didChangeExpansion(isExpanded: true, sender: self) + + if shouldNotifyDelegate { + delegate?.didChangeExpansion(isExpanded: true, sender: self) + } } else { contentView.snp.updateConstraints { make in - make.top.bottom.equalToSuperview().offset( + make.top.equalToSuperview().offset( -CGFloat(stackView.arrangedSubviews.count) * Constants.rowHeight - Constants.stackViewBottomInset ) } layoutIfNeeded() - delegate?.didChangeExpansion(isExpanded: false, sender: self) + + if shouldNotifyDelegate { + delegate?.didChangeExpansion(isExpanded: false, sender: self) + } } } @objc func actionToggleExpansion() { - applyExpansion(animated: true) + applyExpansion(animated: true, shouldNotifyDelegate: true) } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 438dec8c31..4a3e23d6bf 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -6,6 +6,8 @@ final class SwapSetupViewController: UIViewController, ViewHolder { let presenter: SwapSetupPresenterProtocol + private var toggledDetailsManually: Bool = false + init( presenter: SwapSetupPresenterProtocol, localizationManager: LocalizationManager @@ -89,6 +91,8 @@ final class SwapSetupViewController: UIViewController, ViewHolder { action: #selector(depositTokenAction), for: .touchUpInside ) + + rootView.detailsView.delegate = self } private func setupLocalization() { @@ -234,6 +238,10 @@ extension SwapSetupViewController: SwapSetupViewProtocol { func didReceiveDetailsState(isAvailable: Bool) { rootView.detailsView.isHidden = !isAvailable + + if !isAvailable { + toggledDetailsManually = false + } } func didReceiveRate(viewModel: LoadableViewModelState) { @@ -242,6 +250,10 @@ extension SwapSetupViewController: SwapSetupViewProtocol { func didReceiveNetworkFee(viewModel: LoadableViewModelState) { rootView.networkFeeCell.bind(loadableViewModel: viewModel) + + if !toggledDetailsManually, !rootView.detailsView.expanded { + rootView.detailsView.setExpanded(true, animated: true) + } } func didReceiveSettingsState(isAvailable: Bool) { @@ -310,6 +322,16 @@ extension SwapSetupViewController: SwapSetupViewProtocol { } } +extension SwapSetupViewController: CollapsableContainerViewDelegate { + func animateAlongsideWithInfo(sender _: AnyObject?) { + rootView.containerView.scrollView.layoutIfNeeded() + } + + func didChangeExpansion(isExpanded _: Bool, sender _: AnyObject) { + toggledDetailsManually = true + } +} + extension SwapSetupViewController: Localizable { func applyLocalization() { if isViewLoaded { diff --git a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift index 65f6e15a35..f72eeb9212 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapSetupViewLayout.swift @@ -31,8 +31,8 @@ final class SwapSetupViewLayout: ScrollableContainerLayoutView { } let detailsView: SwapDetailsView = .create { - $0.setExpanded(false, animated: false) $0.contentInsets = .zero + $0.setExpanded(false, animated: false) } var rateCell: SwapInfoViewCell { From f4895378915523c5b2de5be571cec882faf8db78 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 8 Nov 2023 07:13:29 +0100 Subject: [PATCH 131/204] improve button state --- .../Model/SwapsSetupViewModelFactory.swift | 49 +++++++++-------- .../Swaps/Setup/SwapSetupPresenter.swift | 54 ++++++++++--------- .../Swaps/Setup/SwapSetupViewFactory.swift | 10 ++-- 3 files changed, 59 insertions(+), 54 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift index f1a58c7137..128173215c 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift @@ -8,15 +8,11 @@ struct RateParams { let amountOut: BigUInt } -protocol SwapsSetupViewModelFactoryProtocol: SwapPriceDifferenceViewModelFactoryProtocol { +protocol SwapsSetupViewModelFactoryProtocol: SwapPriceDifferenceViewModelFactoryProtocol, SwapIssueViewModelFactoryProtocol { var locale: Locale { get set } - func buttonState( - assetIn: ChainAssetId?, - assetOut: ChainAssetId?, - amountIn: Decimal?, - amountOut: Decimal? - ) -> ButtonState + func buttonState(for issueParams: SwapIssueCheckParams) -> ButtonState + func payTitleViewModel( assetDisplayInfo: AssetBalanceDisplayInfo?, maxValue: BigUInt? @@ -50,6 +46,7 @@ protocol SwapsSetupViewModelFactoryProtocol: SwapPriceDifferenceViewModelFactory final class SwapsSetupViewModelFactory { let balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol + let issuesViewModelFactory: SwapIssueViewModelFactoryProtocol let networkViewModelFactory: NetworkViewModelFactoryProtocol let percentForamatter: LocalizableResource private(set) var localizedPercentForamatter: NumberFormatter @@ -63,11 +60,13 @@ final class SwapsSetupViewModelFactory { init( balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol, + issuesViewModelFactory: SwapIssueViewModelFactoryProtocol, networkViewModelFactory: NetworkViewModelFactoryProtocol, percentForamatter: LocalizableResource, locale: Locale ) { self.balanceViewModelFactoryFacade = balanceViewModelFactoryFacade + self.issuesViewModelFactory = issuesViewModelFactory self.networkViewModelFactory = networkViewModelFactory self.percentForamatter = percentForamatter self.locale = locale @@ -75,19 +74,17 @@ final class SwapsSetupViewModelFactory { } private static func buttonTitle( - assetIn: ChainAssetId?, - assetOut: ChainAssetId?, - amountIn: Decimal?, - amountOut: Decimal?, + params: SwapIssueCheckParams, + hasIssues: Bool, locale: Locale ) -> String { - switch (assetIn, assetOut) { + switch (params.payChainAsset, params.receiveChainAsset) { case (nil, nil), (nil, _): return R.string.localizable.swapsSetupAssetActionSelectPay(preferredLanguages: locale.rLanguages) case (_, nil): return R.string.localizable.swapsSetupAssetActionSelectReceive(preferredLanguages: locale.rLanguages) default: - if amountIn == nil || amountOut == nil { + if params.payAmount == nil || params.receiveAmount == nil || hasIssues { return R.string.localizable.swapsSetupAssetActionEnterAmount(preferredLanguages: locale.rLanguages) } else { return R.string.localizable.commonContinue(preferredLanguages: locale.rLanguages) @@ -125,24 +122,22 @@ final class SwapsSetupViewModelFactory { } extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { - func buttonState( - assetIn: ChainAssetId?, - assetOut: ChainAssetId?, - amountIn: Decimal?, - amountOut: Decimal? - ) -> ButtonState { - let dataFullFilled = assetIn != nil && assetOut != nil && amountIn != nil && amountOut != nil + func buttonState(for issueParams: SwapIssueCheckParams) -> ButtonState { + let dataFullFilled = issueParams.payChainAsset != nil && + issueParams.receiveChainAsset != nil && + issueParams.payAmount != nil && issueParams.receiveAmount != nil + + let hasIssues = !issuesViewModelFactory.detectIssues(in: issueParams, locale: locale).isEmpty + return .init( title: .init { Self.buttonTitle( - assetIn: assetIn, - assetOut: assetOut, - amountIn: amountIn, - amountOut: amountOut, + params: issueParams, + hasIssues: hasIssues, locale: $0 ) }, - enabled: dataFullFilled + enabled: dataFullFilled && !hasIssues ) } @@ -316,4 +311,8 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { value: decimal ).value(for: locale) } + + func detectIssues(in model: SwapIssueCheckParams, locale: Locale) -> [SwapSetupViewIssue] { + issuesViewModelFactory.detectIssues(in: model, locale: locale) + } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index a7b5fc4eec..35f5dbe506 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -7,7 +7,6 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { let wireframe: SwapSetupWireframeProtocol let interactor: SwapSetupInteractorInputProtocol let purchaseProvider: PurchaseProviderProtocol - let issuesViewModelFactory: SwapIssueViewModelFactoryProtocol private(set) var viewModelFactory: SwapsSetupViewModelFactoryProtocol @@ -37,7 +36,6 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { interactor: SwapSetupInteractorInputProtocol, wireframe: SwapSetupWireframeProtocol, viewModelFactory: SwapsSetupViewModelFactoryProtocol, - issuesViewModelFactory: SwapIssueViewModelFactoryProtocol, dataValidatingFactory: SwapDataValidatorFactoryProtocol, localizationManager: LocalizationManagerProtocol, selectedWallet: MetaAccountModel, @@ -50,7 +48,6 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { self.interactor = interactor self.wireframe = wireframe self.viewModelFactory = viewModelFactory - self.issuesViewModelFactory = issuesViewModelFactory slippage = slippageConfig.defaultSlippage self.purchaseProvider = purchaseProvider @@ -129,6 +126,10 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { return } + fee = nil + provideFeeViewModel() + provideNotification() + feeIdentifier = newIdentifier interactor.calculateFee(args: args) } @@ -259,6 +260,20 @@ extension SwapSetupPresenter { return input.absoluteValue(from: maxAmount ?? 0) } + func getIssueParams() -> SwapIssueCheckParams { + .init( + payChainAsset: payChainAsset, + receiveChainAsset: receiveChainAsset, + payAmount: getSpendingInputAmount(), + receiveAmount: receiveAmountInput, + payAssetBalance: payAssetBalance, + receiveAssetBalance: receiveAssetBalance, + payAssetExistense: payAssetBalanceExistense, + receiveAssetExistense: receiveAssetBalanceExistense, + quoteResult: quoteResult + ) + } + private func providePayTitle() { let payTitleViewModel = viewModelFactory.payTitleViewModel( assetDisplayInfo: payChainAsset?.assetDisplayInfo, @@ -372,12 +387,7 @@ extension SwapSetupPresenter { } private func provideButtonState() { - let buttonState = viewModelFactory.buttonState( - assetIn: payChainAsset?.chainAssetId, - assetOut: receiveChainAsset?.chainAssetId, - amountIn: getPayAmount(for: payAmountInput), - amountOut: receiveAmountInput - ) + let buttonState = viewModelFactory.buttonState(for: getIssueParams()) view?.didReceiveButtonState( title: buttonState.title.value(for: selectedLocale), @@ -431,21 +441,7 @@ extension SwapSetupPresenter { } private func provideIssues() { - let issues = issuesViewModelFactory.detectIssues( - in: .init( - payChainAsset: payChainAsset, - receiveChainAsset: receiveChainAsset, - payAmount: getSpendingInputAmount(), - receiveAmount: receiveAmountInput, - payAssetBalance: payAssetBalance, - receiveAssetBalance: receiveAssetBalance, - payAssetExistense: payAssetBalanceExistense, - receiveAssetExistense: receiveAssetBalanceExistense, - quoteResult: quoteResult - ), - locale: selectedLocale - ) - + let issues = viewModelFactory.detectIssues(in: getIssueParams(), locale: selectedLocale) view?.didReceive(issues: issues) } @@ -478,6 +474,7 @@ extension SwapSetupPresenter { if forceUpdate { quoteResult = nil + fee = nil } switch direction { @@ -515,10 +512,13 @@ extension SwapSetupPresenter { interactor.calculateQuote(for: quoteArgs) } else { quoteArgs = nil + if forceUpdate { payAmountInput = nil providePayAmountInputViewModel() provideIssues() + provideFeeViewModel() + provideNotification() } else { refreshQuote(direction: .sell) } @@ -538,11 +538,14 @@ extension SwapSetupPresenter { interactor.calculateQuote(for: quoteArgs) } else { quoteArgs = nil + if forceUpdate { receiveAmountInput = nil provideReceiveAmountInputViewModel() provideReceiveInputPriceViewModel() provideIssues() + provideFeeViewModel() + provideNotification() } else { refreshQuote(direction: .buy) } @@ -556,6 +559,7 @@ extension SwapSetupPresenter { fee = nil provideFeeViewModel() + provideNotification() estimateFee() } @@ -629,6 +633,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { refreshQuote(direction: .sell) provideButtonState() provideIssues() + provideNotification() } func updateReceiveAmount(_ amount: Decimal?) { @@ -636,6 +641,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { refreshQuote(direction: .buy) provideButtonState() provideIssues() + provideNotification() } func flip(currentFocus: TextFieldFocus?) { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index bd60661e35..bf1bc24a5a 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -32,17 +32,18 @@ struct SwapSetupViewFactory { state: generalLocalSubscriptionFactory ) + let issuesViewModelFactory = SwapIssueViewModelFactory( + balanceViewModelFactoryFacade: balanceViewModelFactoryFacade + ) + let viewModelFactory = SwapsSetupViewModelFactory( balanceViewModelFactoryFacade: balanceViewModelFactoryFacade, + issuesViewModelFactory: issuesViewModelFactory, networkViewModelFactory: NetworkViewModelFactory(), percentForamatter: NumberFormatter.percentSingle.localizableResource(), locale: LocalizationManager.shared.selectedLocale ) - let issuesViewModelFactory = SwapIssueViewModelFactory( - balanceViewModelFactoryFacade: balanceViewModelFactoryFacade - ) - let dataValidatingFactory = SwapDataValidatorFactory( presentable: wireframe, balanceViewModelFactoryFacade: balanceViewModelFactoryFacade @@ -53,7 +54,6 @@ struct SwapSetupViewFactory { interactor: interactor, wireframe: wireframe, viewModelFactory: viewModelFactory, - issuesViewModelFactory: issuesViewModelFactory, dataValidatingFactory: dataValidatingFactory, localizationManager: LocalizationManager.shared, selectedWallet: selectedWallet, From 237f6ca35c534b3c8a000666a6af52aa351dcbbf Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 8 Nov 2023 10:14:40 +0100 Subject: [PATCH 132/204] refactor validations --- .../Modules/Swaps/Validation/SwapModel.swift | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/novawallet/Modules/Swaps/Validation/SwapModel.swift b/novawallet/Modules/Swaps/Validation/SwapModel.swift index b12f56ee87..3e5d2b7f1b 100644 --- a/novawallet/Modules/Swaps/Validation/SwapModel.swift +++ b/novawallet/Modules/Swaps/Validation/SwapModel.swift @@ -119,14 +119,16 @@ struct SwapModel { let swapAmount = spendingAmountInPlank ?? 0 let totalSpending = swapAmount + fee + + let isViolatingConsumers = !notViolatingConsumers - guard balance < totalSpending else { + guard balance < totalSpending || isViolatingConsumers else { return nil } if balance < swapAmount { return .amountToHigh(.init(available: balance.decimal(precision: payChainAsset.asset.precision))) - } else if !notViolatingConsumers { + } else if isViolatingConsumers { let minBalance = utilityAssetExistense?.minBalance ?? 0 let precision = (utilityChainAsset ?? feeChainAsset).asset.precision let fee = feeModel?.totalFee.targetAmount ?? 0 @@ -171,18 +173,6 @@ struct SwapModel { } } - var notViolatingExistenseAfterFee: Bool { - guard feeChainAsset.isUtilityAsset else { - return true - } - - let totalBalance = utilityAssetBalance?.freeInPlank ?? 0 - let minBalance = utilityAssetExistense?.minBalance ?? 0 - let fee = feeModel?.totalFee.targetAmount ?? 0 - - return totalBalance >= minBalance + fee - } - var accountWillBeKilled: Bool { let balance: BigUInt From b7ac5ca94bfd8ad278731a05d1b29c4a783df006 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 8 Nov 2023 10:57:42 +0100 Subject: [PATCH 133/204] fix account info set --- novawallet/Modules/Swaps/Base/SwapBasePresenter.swift | 4 +++- novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift | 2 ++ novawallet/Modules/Swaps/Validation/SwapModel.swift | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift index 1ca7a73947..b4d5e3b003 100644 --- a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift +++ b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift @@ -123,7 +123,7 @@ class SwapBasePresenter { feeModel: fee, payAssetExistense: payAssetBalanceExistense, receiveAssetExistense: receiveAssetBalanceExistense, - accountInfo: nil + accountInfo: accountInfo ) } @@ -360,6 +360,8 @@ extension SwapBasePresenter: SwapBaseInteractorOutputProtocol { logger.debug("New account info: \(String(describing: accountInfo))") + self.accountInfo = accountInfo + handleNewAccountInfo(accountInfo, chainId: chainId) } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 35f5dbe506..3f78d3919d 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -740,6 +740,8 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { DataValidationRunner(validators: validators).runValidation( notifyingOnSuccess: { [weak self] in + self?.view?.didStopLoading() + guard let slippage = self?.slippage, let quote = self?.quote, let quoteArgs = self?.quoteArgs else { diff --git a/novawallet/Modules/Swaps/Validation/SwapModel.swift b/novawallet/Modules/Swaps/Validation/SwapModel.swift index 3e5d2b7f1b..56f4b7c10a 100644 --- a/novawallet/Modules/Swaps/Validation/SwapModel.swift +++ b/novawallet/Modules/Swaps/Validation/SwapModel.swift @@ -119,7 +119,7 @@ struct SwapModel { let swapAmount = spendingAmountInPlank ?? 0 let totalSpending = swapAmount + fee - + let isViolatingConsumers = !notViolatingConsumers guard balance < totalSpending || isViolatingConsumers else { From 7700684953e1f79a364be1067079f9165ce34ddc Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 8 Nov 2023 13:41:36 +0100 Subject: [PATCH 134/204] fix updating data after operation --- novawallet/Modules/Swaps/Base/SwapBasePresenter.swift | 2 +- novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift | 6 ++++-- .../Modules/Swaps/Validation/SwapDataValidatorFactory.swift | 3 +-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift index b4d5e3b003..5b6cff50f5 100644 --- a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift +++ b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift @@ -361,7 +361,7 @@ extension SwapBasePresenter: SwapBaseInteractorOutputProtocol { logger.debug("New account info: \(String(describing: accountInfo))") self.accountInfo = accountInfo - + handleNewAccountInfo(accountInfo, chainId: chainId) } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 3f78d3919d..bbb3815e21 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -122,7 +122,7 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { feeChainAssetId: feeChainAsset?.chainAssetId ) - guard newIdentifier != feeIdentifier else { + guard newIdentifier != feeIdentifier || fee == nil else { return } @@ -194,6 +194,9 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { if case .rate = payAmountInput { providePayInputPriceViewModel() providePayAmountInputViewModel() + + // as fee changes the max amount we might also refresh the quote + refreshQuote(direction: quoteArgs?.direction ?? .sell, forceUpdate: false) } provideButtonState() @@ -474,7 +477,6 @@ extension SwapSetupPresenter { if forceUpdate { quoteResult = nil - fee = nil } switch direction { diff --git a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift index 878aef0641..fc2ba127b8 100644 --- a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift +++ b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift @@ -274,8 +274,6 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { switch reason { case let .rateChange(rateUpdate): - onQuoteUpdate(rateUpdate.newQuote) - let oldRate = Decimal.rateFromSubstrate( amount1: rateUpdate.oldQuote.amountIn, amount2: rateUpdate.oldQuote.amountOut, @@ -307,6 +305,7 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { oldRate: oldRateString, newRate: newRateString, onConfirm: { + onQuoteUpdate(rateUpdate.newQuote) delegate.didCompleteAsyncHandling() }, locale: locale From 8f6944bf4398d44373c38c3c6f56ef36c1bd7500 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 8 Nov 2023 16:54:16 +0100 Subject: [PATCH 135/204] fix swaps --- novawallet.xcodeproj/project.pbxproj | 4 + .../Base/Model/SwapBaseViewModelFactory.swift | 95 ++++++++++ .../Model/SwapsSetupViewModelFactory.swift | 165 +++++------------- .../Swaps/Setup/SwapSetupPresenter.swift | 75 +++++--- 4 files changed, 199 insertions(+), 140 deletions(-) create mode 100644 novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 1b09839a93..8c2c0417ac 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -79,6 +79,7 @@ 0C13DFDB2AFA691400E5F355 /* SlippageConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFDA2AFA691400E5F355 /* SlippageConfig.swift */; }; 0C13DFDD2AFA82BA00E5F355 /* AmountInputViewModel+Slippage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFDC2AFA82BA00E5F355 /* AmountInputViewModel+Slippage.swift */; }; 0C13DFDF2AFA89EF00E5F355 /* Decimal+Percents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFDE2AFA89EF00E5F355 /* Decimal+Percents.swift */; }; + 0C13DFE12AFBBAF600E5F355 /* SwapBaseViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13DFE02AFBBAF600E5F355 /* SwapBaseViewModelFactory.swift */; }; 0C154A0E2A45995500932C3F /* CompoundComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C154A0D2A45995500932C3F /* CompoundComparator.swift */; }; 0C17BD972A42F162004AF9E7 /* WalletViewModelObserverContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */; }; 0C17BD992A42F1BE004AF9E7 /* MoneyPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */; }; @@ -4141,6 +4142,7 @@ 0C13DFDA2AFA691400E5F355 /* SlippageConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlippageConfig.swift; sourceTree = ""; }; 0C13DFDC2AFA82BA00E5F355 /* AmountInputViewModel+Slippage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AmountInputViewModel+Slippage.swift"; sourceTree = ""; }; 0C13DFDE2AFA89EF00E5F355 /* Decimal+Percents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+Percents.swift"; sourceTree = ""; }; + 0C13DFE02AFBBAF600E5F355 /* SwapBaseViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapBaseViewModelFactory.swift; sourceTree = ""; }; 0C154A0D2A45995500932C3F /* CompoundComparator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompoundComparator.swift; sourceTree = ""; }; 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletViewModelObserverContainer.swift; sourceTree = ""; }; 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyPresentable.swift; sourceTree = ""; }; @@ -8477,6 +8479,7 @@ 0C2FDF182AEB82AA006A6C59 /* AssetHubFeeModelBuilder.swift */, 0C13DFCC2AF8A5A300E5F355 /* SwapMaxModel.swift */, 0C13DFDA2AFA691400E5F355 /* SlippageConfig.swift */, + 0C13DFE02AFBBAF600E5F355 /* SwapBaseViewModelFactory.swift */, ); path = Model; sourceTree = ""; @@ -19995,6 +19998,7 @@ 2AD0A19525D3D3EC00312428 /* GitHubOperationFactory.swift in Sources */, 845B07FF2916F529005785D3 /* Gov1SubscriptionFactory.swift in Sources */, 888A3B6528F73DC300E15BD2 /* ReferendumVotingStatusView.swift in Sources */, + 0C13DFE12AFBBAF600E5F355 /* SwapBaseViewModelFactory.swift in Sources */, 843E9B3627C8B915009C143A /* NftFileDownloadService.swift in Sources */, 842B17FF28649CCD0014CC57 /* CrossChainDestinationSelectionState.swift in Sources */, 8437F7C12924FF6400DB6366 /* EvmSubscriptionMessage.swift in Sources */, diff --git a/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift b/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift new file mode 100644 index 0000000000..927c6b6ed9 --- /dev/null +++ b/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift @@ -0,0 +1,95 @@ +import Foundation +import BigInt + +struct RateParams { + let assetDisplayInfoIn: AssetBalanceDisplayInfo + let assetDisplayInfoOut: AssetBalanceDisplayInfo + let amountIn: BigUInt + let amountOut: BigUInt +} + +protocol SwapBaseViewModelFactoryProtocol { + func rateViewModel(from params: RateParams, locale: Locale) -> String + + func minimalBalanceSwapForFeeMessage( + for networkFeeAddition: AssetConversion.AmountWithNative, + feeChainAsset: ChainAsset, + utilityChainAsset: ChainAsset, + utilityPriceData: PriceData?, + locale: Locale + ) -> String +} + +class SwapBaseViewModelFactory { + let balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol + + init(balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol) { + self.balanceViewModelFactoryFacade = balanceViewModelFactoryFacade + } +} + +extension SwapBaseViewModelFactory: SwapBaseViewModelFactoryProtocol { + func rateViewModel(from params: RateParams, locale: Locale) -> String { + guard + let rate = Decimal.rateFromSubstrate( + amount1: params.amountIn, + amount2: params.amountOut, + precision1: params.assetDisplayInfoIn.assetPrecision, + precision2: params.assetDisplayInfoOut.assetPrecision + ) else { + return "" + } + + let amountIn = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: params.assetDisplayInfoIn, + value: 1 + ).value(for: locale) + let amountOut = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: params.assetDisplayInfoOut, + value: rate + ).value(for: locale) + + return amountIn.estimatedEqual(to: amountOut) + } + + func minimalBalanceSwapForFeeMessage( + for networkFeeAddition: AssetConversion.AmountWithNative, + feeChainAsset: ChainAsset, + utilityChainAsset: ChainAsset, + utilityPriceData: PriceData?, + locale: Locale + ) -> String { + let targetAmount = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: feeChainAsset.assetDisplayInfo, + value: networkFeeAddition.targetAmount.decimal(precision: feeChainAsset.asset.precision) + ).value(for: locale) + + let nativeAmountDecimal = networkFeeAddition.nativeAmount.decimal(precision: utilityChainAsset.asset.precision) + let nativeAmountWithoutPrice = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: utilityChainAsset.assetDisplayInfo, + value: nativeAmountDecimal + ).value(for: locale) + + let nativeAmount: String + + if let priceData = utilityPriceData { + let price = balanceViewModelFactoryFacade.priceFromAmount( + targetAssetInfo: utilityChainAsset.assetDisplayInfo, + amount: nativeAmountDecimal, + priceData: priceData + ).value(for: locale) + + nativeAmount = "\(nativeAmountWithoutPrice) \(price.inParenthesis())" + } else { + nativeAmount = nativeAmountWithoutPrice + } + + return R.string.localizable.swapsPayAssetFeeEdMessage( + feeChainAsset.asset.symbol, + targetAmount, + nativeAmount, + utilityChainAsset.asset.symbol, + preferredLanguages: locale.rLanguages + ) + } +} diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift index 128173215c..16db23d908 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift @@ -1,76 +1,64 @@ import SoraFoundation import BigInt -struct RateParams { - let assetDisplayInfoIn: AssetBalanceDisplayInfo - let assetDisplayInfoOut: AssetBalanceDisplayInfo - let amountIn: BigUInt - let amountOut: BigUInt -} - -protocol SwapsSetupViewModelFactoryProtocol: SwapPriceDifferenceViewModelFactoryProtocol, SwapIssueViewModelFactoryProtocol { - var locale: Locale { get set } - - func buttonState(for issueParams: SwapIssueCheckParams) -> ButtonState +protocol SwapsSetupViewModelFactoryProtocol: SwapBaseViewModelFactoryProtocol, + SwapPriceDifferenceViewModelFactoryProtocol, + SwapIssueViewModelFactoryProtocol { + func buttonState(for issueParams: SwapIssueCheckParams, locale: Locale) -> ButtonState func payTitleViewModel( assetDisplayInfo: AssetBalanceDisplayInfo?, - maxValue: BigUInt? + maxValue: BigUInt?, + locale: Locale ) -> TitleHorizontalMultiValueView.Model - func payAssetViewModel(chainAsset: ChainAsset?) -> SwapAssetInputViewModel + + func payAssetViewModel(chainAsset: ChainAsset?, locale: Locale) -> SwapAssetInputViewModel + func inputPriceViewModel( assetDisplayInfo: AssetBalanceDisplayInfo, amount: Decimal?, - priceData: PriceData? + priceData: PriceData?, + locale: Locale ) -> String? - func receiveTitleViewModel() -> TitleHorizontalMultiValueView.Model - func receiveAssetViewModel(chainAsset: ChainAsset?) -> SwapAssetInputViewModel - func amountInputViewModel(chainAsset: ChainAsset, amount: Decimal?) -> AmountInputViewModelProtocol - func rateViewModel(from params: RateParams) -> String + + func receiveTitleViewModel(for locale: Locale) -> TitleHorizontalMultiValueView.Model + func receiveAssetViewModel(chainAsset: ChainAsset?, locale: Locale) -> SwapAssetInputViewModel + + func amountInputViewModel( + chainAsset: ChainAsset, + amount: Decimal?, + locale: Locale + ) -> AmountInputViewModelProtocol + func feeViewModel( amount: BigUInt, assetDisplayInfo: AssetBalanceDisplayInfo, isEditable: Bool, - priceData: PriceData? + priceData: PriceData?, + locale: Locale ) -> SwapFeeViewModel - func minimalBalanceSwapForFeeMessage( - for networkFeeAddition: AssetConversion.AmountWithNative, - feeChainAsset: ChainAsset, - utilityChainAsset: ChainAsset, - utilityPriceData: PriceData? - ) -> String - - func amountFromValue(_ decimal: Decimal, chainAsset: ChainAsset) -> String + func amountFromValue(_ decimal: Decimal, chainAsset: ChainAsset, locale: Locale) -> String } -final class SwapsSetupViewModelFactory { - let balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol +final class SwapsSetupViewModelFactory: SwapBaseViewModelFactory { let issuesViewModelFactory: SwapIssueViewModelFactoryProtocol let networkViewModelFactory: NetworkViewModelFactoryProtocol let percentForamatter: LocalizableResource - private(set) var localizedPercentForamatter: NumberFormatter - private(set) var priceDifferenceWarningRange: (start: Decimal, end: Decimal) = (start: 0.1, end: 0.2) - var locale: Locale { - didSet { - localizedPercentForamatter = percentForamatter.value(for: locale) - } - } + private(set) var priceDifferenceWarningRange: (start: Decimal, end: Decimal) = (start: 0.1, end: 0.2) init( balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol, issuesViewModelFactory: SwapIssueViewModelFactoryProtocol, networkViewModelFactory: NetworkViewModelFactoryProtocol, - percentForamatter: LocalizableResource, - locale: Locale + percentForamatter: LocalizableResource ) { - self.balanceViewModelFactoryFacade = balanceViewModelFactoryFacade self.issuesViewModelFactory = issuesViewModelFactory self.networkViewModelFactory = networkViewModelFactory self.percentForamatter = percentForamatter - self.locale = locale - localizedPercentForamatter = percentForamatter.value(for: locale) + + super.init(balanceViewModelFactoryFacade: balanceViewModelFactoryFacade) } private static func buttonTitle( @@ -104,7 +92,7 @@ final class SwapsSetupViewModelFactory { ) } - private func emptyPayAssetViewModel() -> EmptySwapsAssetViewModel { + private func emptyPayAssetViewModel(for locale: Locale) -> EmptySwapsAssetViewModel { EmptySwapsAssetViewModel( imageViewModel: StaticImageViewModel(image: R.image.iconAddSwapAmount()!), title: R.string.localizable.swapsSetupAssetPayTitle(preferredLanguages: locale.rLanguages), @@ -112,7 +100,7 @@ final class SwapsSetupViewModelFactory { ) } - private func emptyReceiveAssetViewModel() -> EmptySwapsAssetViewModel { + private func emptyReceiveAssetViewModel(for locale: Locale) -> EmptySwapsAssetViewModel { EmptySwapsAssetViewModel( imageViewModel: StaticImageViewModel(image: R.image.iconAddSwapAmount()!), title: R.string.localizable.swapsSetupAssetReceiveTitle(preferredLanguages: locale.rLanguages), @@ -122,7 +110,7 @@ final class SwapsSetupViewModelFactory { } extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { - func buttonState(for issueParams: SwapIssueCheckParams) -> ButtonState { + func buttonState(for issueParams: SwapIssueCheckParams, locale: Locale) -> ButtonState { let dataFullFilled = issueParams.payChainAsset != nil && issueParams.receiveChainAsset != nil && issueParams.payAmount != nil && issueParams.receiveAmount != nil @@ -143,7 +131,8 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { func payTitleViewModel( assetDisplayInfo: AssetBalanceDisplayInfo?, - maxValue: BigUInt? + maxValue: BigUInt?, + locale: Locale ) -> TitleHorizontalMultiValueView.Model { let title = R.string.localizable.swapsSetupAssetSelectPayTitle( preferredLanguages: locale.rLanguages @@ -179,14 +168,16 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { } } - func payAssetViewModel(chainAsset: ChainAsset?) -> SwapAssetInputViewModel { - chainAsset.map { .asset(assetViewModel(chainAsset: $0)) } ?? .empty(emptyPayAssetViewModel()) + func payAssetViewModel(chainAsset: ChainAsset?, locale: Locale) -> SwapAssetInputViewModel { + chainAsset.map { .asset(assetViewModel(chainAsset: $0)) } ?? + .empty(emptyPayAssetViewModel(for: locale)) } func inputPriceViewModel( assetDisplayInfo: AssetBalanceDisplayInfo, amount: Decimal?, - priceData: PriceData? + priceData: PriceData?, + locale: Locale ) -> String? { guard let amount = amount, @@ -200,7 +191,7 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { ).value(for: locale) } - func receiveTitleViewModel() -> TitleHorizontalMultiValueView.Model { + func receiveTitleViewModel(for locale: Locale) -> TitleHorizontalMultiValueView.Model { TitleHorizontalMultiValueView.Model( title: R.string.localizable.swapsSetupAssetSelectReceiveTitle(preferredLanguages: locale.rLanguages), @@ -209,36 +200,15 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { ) } - func receiveAssetViewModel(chainAsset: ChainAsset?) -> SwapAssetInputViewModel { - chainAsset.map { .asset(assetViewModel(chainAsset: $0)) } ?? .empty(emptyReceiveAssetViewModel()) - } - - func rateViewModel(from params: RateParams) -> String { - guard - let rate = Decimal.rateFromSubstrate( - amount1: params.amountIn, - amount2: params.amountOut, - precision1: params.assetDisplayInfoIn.assetPrecision, - precision2: params.assetDisplayInfoOut.assetPrecision - ) else { - return "" - } - - let amountIn = balanceViewModelFactoryFacade.amountFromValue( - targetAssetInfo: params.assetDisplayInfoIn, - value: 1 - ).value(for: locale) - let amountOut = balanceViewModelFactoryFacade.amountFromValue( - targetAssetInfo: params.assetDisplayInfoOut, - value: rate - ).value(for: locale) - - return amountIn.estimatedEqual(to: amountOut) + func receiveAssetViewModel(chainAsset: ChainAsset?, locale: Locale) -> SwapAssetInputViewModel { + chainAsset.map { .asset(assetViewModel(chainAsset: $0)) } ?? + .empty(emptyReceiveAssetViewModel(for: locale)) } func amountInputViewModel( chainAsset: ChainAsset, - amount: Decimal? + amount: Decimal?, + locale: Locale ) -> AmountInputViewModelProtocol { balanceViewModelFactoryFacade.createBalanceInputViewModel( targetAssetInfo: chainAsset.assetDisplayInfo, @@ -250,7 +220,8 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { amount: BigUInt, assetDisplayInfo: AssetBalanceDisplayInfo, isEditable: Bool, - priceData: PriceData? + priceData: PriceData?, + locale: Locale ) -> SwapFeeViewModel { let amountDecimal = Decimal.fromSubstrateAmount( amount, @@ -265,47 +236,7 @@ extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { return .init(isEditable: isEditable, balanceViewModel: balanceViewModel) } - func minimalBalanceSwapForFeeMessage( - for networkFeeAddition: AssetConversion.AmountWithNative, - feeChainAsset: ChainAsset, - utilityChainAsset: ChainAsset, - utilityPriceData: PriceData? - ) -> String { - let targetAmount = balanceViewModelFactoryFacade.amountFromValue( - targetAssetInfo: feeChainAsset.assetDisplayInfo, - value: networkFeeAddition.targetAmount.decimal(precision: feeChainAsset.asset.precision) - ).value(for: locale) - - let nativeAmountDecimal = networkFeeAddition.nativeAmount.decimal(precision: utilityChainAsset.asset.precision) - let nativeAmountWithoutPrice = balanceViewModelFactoryFacade.amountFromValue( - targetAssetInfo: utilityChainAsset.assetDisplayInfo, - value: nativeAmountDecimal - ).value(for: locale) - - let nativeAmount: String - - if let priceData = utilityPriceData { - let price = balanceViewModelFactoryFacade.priceFromAmount( - targetAssetInfo: utilityChainAsset.assetDisplayInfo, - amount: nativeAmountDecimal, - priceData: priceData - ).value(for: locale) - - nativeAmount = "\(nativeAmountWithoutPrice) \(price.inParenthesis())" - } else { - nativeAmount = nativeAmountWithoutPrice - } - - return R.string.localizable.swapsPayAssetFeeEdMessage( - feeChainAsset.asset.symbol, - targetAmount, - nativeAmount, - utilityChainAsset.asset.symbol, - preferredLanguages: locale.rLanguages - ) - } - - func amountFromValue(_ decimal: Decimal, chainAsset: ChainAsset) -> String { + func amountFromValue(_ decimal: Decimal, chainAsset: ChainAsset, locale: Locale) -> String { balanceViewModelFactoryFacade.amountFromValue( targetAssetInfo: chainAsset.assetDisplayInfo, value: decimal diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index bbb3815e21..151ef1acd8 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -280,13 +280,18 @@ extension SwapSetupPresenter { private func providePayTitle() { let payTitleViewModel = viewModelFactory.payTitleViewModel( assetDisplayInfo: payChainAsset?.assetDisplayInfo, - maxValue: payAssetBalance?.transferable + maxValue: payAssetBalance?.transferable, + locale: selectedLocale ) view?.didReceiveTitle(payViewModel: payTitleViewModel) } private func providePayAssetViewModel() { - let payAssetViewModel = viewModelFactory.payAssetViewModel(chainAsset: payChainAsset) + let payAssetViewModel = viewModelFactory.payAssetViewModel( + chainAsset: payChainAsset, + locale: selectedLocale + ) + view?.didReceiveInputChainAsset(payViewModel: payAssetViewModel) } @@ -294,10 +299,13 @@ extension SwapSetupPresenter { guard let payChainAsset = payChainAsset else { return } + let amountInputViewModel = viewModelFactory.amountInputViewModel( chainAsset: payChainAsset, - amount: getPayAmount(for: payAmountInput) + amount: getPayAmount(for: payAmountInput), + locale: selectedLocale ) + view?.didReceiveAmount(payInputViewModel: amountInputViewModel) } @@ -310,21 +318,27 @@ extension SwapSetupPresenter { let inputPriceViewModel = viewModelFactory.inputPriceViewModel( assetDisplayInfo: assetDisplayInfo, amount: getPayAmount(for: payAmountInput), - priceData: payAssetPriceData + priceData: payAssetPriceData, + locale: selectedLocale ) view?.didReceiveAmountInputPrice(payViewModel: inputPriceViewModel) } private func provideReceiveTitle() { - let receiveTitleViewModel = viewModelFactory.receiveTitleViewModel() + let receiveTitleViewModel = viewModelFactory.receiveTitleViewModel( + for: selectedLocale + ) + view?.didReceiveTitle(receiveViewModel: receiveTitleViewModel) } private func provideReceiveAssetViewModel() { let receiveAssetViewModel = viewModelFactory.receiveAssetViewModel( - chainAsset: receiveChainAsset + chainAsset: receiveChainAsset, + locale: selectedLocale ) + view?.didReceiveInputChainAsset(receiveViewModel: receiveAssetViewModel) } @@ -334,8 +348,10 @@ extension SwapSetupPresenter { } let amountInputViewModel = viewModelFactory.amountInputViewModel( chainAsset: receiveChainAsset, - amount: receiveAmountInput + amount: receiveAmountInput, + locale: selectedLocale ) + view?.didReceiveAmount(receiveInputViewModel: amountInputViewModel) } @@ -348,7 +364,8 @@ extension SwapSetupPresenter { let inputPriceViewModel = viewModelFactory.inputPriceViewModel( assetDisplayInfo: assetDisplayInfo, amount: receiveAmountInput, - priceData: receiveAssetPriceData + priceData: receiveAssetPriceData, + locale: selectedLocale ) let differenceViewModel: DifferenceViewModel? @@ -390,7 +407,10 @@ extension SwapSetupPresenter { } private func provideButtonState() { - let buttonState = viewModelFactory.buttonState(for: getIssueParams()) + let buttonState = viewModelFactory.buttonState( + for: getIssueParams(), + locale: selectedLocale + ) view?.didReceiveButtonState( title: buttonState.title.value(for: selectedLocale), @@ -414,12 +434,15 @@ extension SwapSetupPresenter { view?.didReceiveRate(viewModel: .loading) return } - let rateViewModel = viewModelFactory.rateViewModel(from: .init( - assetDisplayInfoIn: assetDisplayInfoIn, - assetDisplayInfoOut: assetDisplayInfoOut, - amountIn: quote.amountIn, - amountOut: quote.amountOut - )) + let rateViewModel = viewModelFactory.rateViewModel( + from: .init( + assetDisplayInfoIn: assetDisplayInfoIn, + assetDisplayInfoOut: assetDisplayInfoOut, + amountIn: quote.amountIn, + amountOut: quote.amountOut + ), + locale: selectedLocale + ) view?.didReceiveRate(viewModel: .loaded(value: rateViewModel)) } @@ -437,7 +460,8 @@ extension SwapSetupPresenter { amount: fee, assetDisplayInfo: feeChainAsset.assetDisplayInfo, isEditable: isEditable, - priceData: feeAssetPriceData + priceData: feeAssetPriceData, + locale: selectedLocale ) view?.didReceiveNetworkFee(viewModel: .loaded(value: viewModel)) @@ -462,7 +486,8 @@ extension SwapSetupPresenter { for: networkFeeAddition, feeChainAsset: feeChainAsset, utilityChainAsset: utilityChainAsset, - utilityPriceData: prices[utilityChainAsset.chainAssetId] + utilityPriceData: prices[utilityChainAsset.chainAssetId], + locale: selectedLocale ) view?.didSetNotification(message: message) @@ -565,16 +590,21 @@ extension SwapSetupPresenter { estimateFee() } -} -extension SwapSetupPresenter: SwapSetupPresenterProtocol { - func setup() { + private func updateViews() { providePayAssetViews() provideReceiveAssetViews() - provideDetailsViewModel(isAvailable: false) + provideDetailsViewModel(isAvailable: quoteArgs != nil) provideButtonState() provideSettingsState() provideIssues() + provideNotification() + } +} + +extension SwapSetupPresenter: SwapSetupPresenterProtocol { + func setup() { + updateViews() interactor.setup() interactor.update(payChainAsset: payChainAsset) @@ -873,8 +903,7 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { extension SwapSetupPresenter: Localizable { func applyLocalization() { if view?.isSetup == true { - setup() - viewModelFactory.locale = selectedLocale + updateViews() } } } From 08289dce1c8359b072306460051104f4f2eb103f Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 9 Nov 2023 01:15:22 +0300 Subject: [PATCH 136/204] swap details --- novawallet.xcodeproj/project.pbxproj | 28 ++- .../TransactionHistoryItem+Subscription.swift | 4 +- ...sactionHistoryItem+CoreDataDecodable.swift | 24 ++- .../CalculatorFactory.swift | 16 ++ .../ChainModel+historyId.swift | 12 ++ .../Migration/SubstrateStorageVersion.swift | 3 + .../Common/Model/TransactionHistoryItem.swift | 18 ++ novawallet/Common/Model/TransactionType.swift | 1 + .../Common/Model/WalletHistoryFilter.swift | 3 +- .../ERC20/EtherscanERC20HistoryResponse.swift | 4 +- .../EtherscanNativeOperationFactory.swift | 2 + .../Native/EtherscanTxHistoryResponse.swift | 4 +- .../Subquery/Models/SubqueryHistory.swift | 13 ++ .../Subquery/SubqueryHistory+Wallet.swift | 59 ++++- .../SubqueryHistoryOperationFactory.swift | 9 + .../PersistExtrinsicFactory.swift | 8 +- .../ContractTransactionHistoryUpdater.swift | 4 +- .../EvmNativeTransactionHistoryUpdater.swift | 4 +- .../.xccurrentversion | 2 +- .../SubstrateDataModel20.xcdatamodel/contents | 201 ++++++++++++++++++ .../Storage/SubstrateDataStorageFacade.swift | 2 +- .../Substrate/Types/CallCodingPath.swift | 12 ++ .../Model/OperationDetailsModel.swift | 1 + .../Model/OperationSwapModel.swift | 18 ++ .../OperationDetailsContractProvider.swift | 6 +- .../OperationDetailsDataProviderFactory.swift | 6 + ...OperationDetailsDataProviderProtocol.swift | 3 +- ...perationDetailsDirectStakingProvider.swift | 4 +- .../OperationDetailsExtrinsicProvider.swift | 4 +- .../OperationDetailsPoolStakingProvider.swift | 4 +- .../OperationDetailsSwapProvider.swift | 93 ++++++++ .../OperationDetailsTransferProvider.swift | 5 +- .../OperationDetailsBaseInteractor.swift | 157 ++++++++++++++ .../OperationDetailsInteractor.swift | 190 ++--------------- .../OperationDetailsPresenter.swift | 5 +- .../OperationDetailsViewFactory.swift | 87 +++++--- .../OperationDetailsViewLayout.swift | 4 +- .../OperationSwapDetailsInteractor.swift | 23 ++ .../View/OperationDetailsSwapView.swift | 59 +++-- .../OperationDetailsViewModelFactory.swift | 150 +++++++++++-- .../TransactionHistoryViewModelFactory.swift | 52 ++++- .../WalletRemoteHistoryProtocols.swift | 1 + .../View/HistoryItemTableViewCell.swift | 4 +- 43 files changed, 1041 insertions(+), 268 deletions(-) create mode 100644 novawallet/Common/Helpers/TransactionHistory/CalculatorFactory.swift create mode 100644 novawallet/Common/Helpers/TransactionHistory/ChainModel+historyId.swift create mode 100644 novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel20.xcdatamodel/contents create mode 100644 novawallet/Modules/OperationDetails/Model/OperationSwapModel.swift create mode 100644 novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift create mode 100644 novawallet/Modules/OperationDetails/OperationDetailsBaseInteractor.swift create mode 100644 novawallet/Modules/OperationDetails/OperationSwapDetailsInteractor.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index ab346a8746..6d812a3d33 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -794,6 +794,12 @@ 77A6F5CF2A31C4D4004AFD1A /* Web3TransferRecipientRepositoryFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5CE2A31C4D4004AFD1A /* Web3TransferRecipientRepositoryFactory.swift */; }; 77A6F5D22A31DB8C004AFD1A /* JsonCanonicalizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5D12A31DB8C004AFD1A /* JsonCanonicalizer.swift */; }; 77A6F5D52A31E046004AFD1A /* JsonCanonicalizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5D42A31E046004AFD1A /* JsonCanonicalizerTests.swift */; }; + 77AAE2202AFB00CB006872CC /* OperationSwapModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AAE21F2AFB00CB006872CC /* OperationSwapModel.swift */; }; + 77AAE2222AFB026E006872CC /* OperationDetailsSwapProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AAE2212AFB026E006872CC /* OperationDetailsSwapProvider.swift */; }; + 77AAE2242AFB67BE006872CC /* OperationSwapDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AAE2232AFB67BE006872CC /* OperationSwapDetailsInteractor.swift */; }; + 77AAE2262AFC10EE006872CC /* CalculatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AAE2252AFC10EE006872CC /* CalculatorFactory.swift */; }; + 77AAE2282AFC1167006872CC /* ChainModel+historyId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AAE2272AFC1167006872CC /* ChainModel+historyId.swift */; }; + 77AAE22A2AFC36EE006872CC /* OperationDetailsBaseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AAE2292AFC36EE006872CC /* OperationDetailsBaseInteractor.swift */; }; 77AB55592AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */; }; 77AB555B2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */; }; 77AB555D2AA24BA90058814E /* OperationDetailsPoolRewardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */; }; @@ -4850,6 +4856,13 @@ 77A6F5CE2A31C4D4004AFD1A /* Web3TransferRecipientRepositoryFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Web3TransferRecipientRepositoryFactory.swift; sourceTree = ""; }; 77A6F5D12A31DB8C004AFD1A /* JsonCanonicalizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonCanonicalizer.swift; sourceTree = ""; }; 77A6F5D42A31E046004AFD1A /* JsonCanonicalizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonCanonicalizerTests.swift; sourceTree = ""; }; + 77AAE21C2AF56C88006872CC /* SubstrateDataModel20.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel20.xcdatamodel; sourceTree = ""; }; + 77AAE21F2AFB00CB006872CC /* OperationSwapModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationSwapModel.swift; sourceTree = ""; }; + 77AAE2212AFB026E006872CC /* OperationDetailsSwapProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsSwapProvider.swift; sourceTree = ""; }; + 77AAE2232AFB67BE006872CC /* OperationSwapDetailsInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationSwapDetailsInteractor.swift; sourceTree = ""; }; + 77AAE2252AFC10EE006872CC /* CalculatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorFactory.swift; sourceTree = ""; }; + 77AAE2272AFC1167006872CC /* ChainModel+historyId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChainModel+historyId.swift"; sourceTree = ""; }; + 77AAE2292AFC36EE006872CC /* OperationDetailsBaseInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsBaseInteractor.swift; sourceTree = ""; }; 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashModel.swift; sourceTree = ""; }; 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashViewModel.swift; sourceTree = ""; }; 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsPoolRewardView.swift; sourceTree = ""; }; @@ -8574,6 +8587,7 @@ children = ( 0C59E8EF2AA76361001E11F3 /* OperationDetailsDataProviderProtocol.swift */, 0C59E8F12AA76436001E11F3 /* OperationDetailsTransferProvider.swift */, + 77AAE2212AFB026E006872CC /* OperationDetailsSwapProvider.swift */, 0C59E8F32AA7649E001E11F3 /* OperationDetailsBaseProvider.swift */, 0C59E8F52AA76772001E11F3 /* OperationDetailsExtrinsicProvider.swift */, 0C59E8F72AA76833001E11F3 /* OperationDetailsContractProvider.swift */, @@ -10734,6 +10748,7 @@ 842A736327DB31A3006EE1EA /* OperationRewardOrSlashModel.swift */, 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */, 842A736727DB4883006EE1EA /* OperationTransferModel.swift */, + 77AAE21F2AFB00CB006872CC /* OperationSwapModel.swift */, 84E0C51D29CA40DA000B65C8 /* OperationContractCallModel.swift */, ); path = Model; @@ -14299,6 +14314,8 @@ children = ( 84FD3DB02540C09800A234E3 /* TransactionHistoryMergeManager.swift */, 849B036F2A15EE39009624D9 /* TokenPriceCalculator.swift */, + 77AAE2252AFC10EE006872CC /* CalculatorFactory.swift */, + 77AAE2272AFC1167006872CC /* ChainModel+historyId.swift */, ); path = TransactionHistory; sourceTree = ""; @@ -17443,6 +17460,8 @@ 1DC6917929E4A752B79FE554 /* OperationDetailsWireframe.swift */, 3D2A26EC9537BD4275A03272 /* OperationDetailsPresenter.swift */, 661356CFE77B978610397907 /* OperationDetailsInteractor.swift */, + 77AAE2232AFB67BE006872CC /* OperationSwapDetailsInteractor.swift */, + 77AAE2292AFC36EE006872CC /* OperationDetailsBaseInteractor.swift */, 9BCCD837A377C237C18B117E /* OperationDetailsViewController.swift */, 73D569738955713647612599 /* OperationDetailsViewLayout.swift */, A5F05632A6635A54A9CDA7FC /* OperationDetailsViewFactory.swift */, @@ -20572,6 +20591,7 @@ AEA2C1B62681E9B20069492E /* ValidatorSearchViewFactory.swift in Sources */, 84AE7ABB27D411CE00495267 /* RMRKV2DetailsInteractor.swift in Sources */, 84FFE4572861FFE5002432BB /* FeeWithWeight.swift in Sources */, + 77AAE22A2AFC36EE006872CC /* OperationDetailsBaseInteractor.swift in Sources */, AE4A71D42607B1440017C663 /* NetworkStakingInfoViewModel.swift in Sources */, 8439398A2636E8840087658D /* YourValidatorListViewLayout.swift in Sources */, 84DB4E2325E945E000A6DF41 /* SlashingSpans.swift in Sources */, @@ -20622,6 +20642,7 @@ 8430AAE926022F69005B1066 /* StashState.swift in Sources */, 847ABE3128532E1B00851218 /* ConsesusType.swift in Sources */, 8469529C28528C9C0083E0B4 /* EraCountdownOperationFactoryProtocol.swift in Sources */, + 77AAE2222AFB026E006872CC /* OperationDetailsSwapProvider.swift in Sources */, 8428768424AE046300D91AD8 /* LanguageSelectionInteractor.swift in Sources */, 841E5542282D525F00C8438F /* ParastakingLocalStorageHandler.swift in Sources */, F4A12BF7260B61E900392C33 /* StakingManageOption.swift in Sources */, @@ -21284,6 +21305,7 @@ 8477DAA6288832CB00129B45 /* WatchOnlyPresetRepository.swift in Sources */, AEA2C1BC2681E9CD0069492E /* ValidatorSearchPresenter.swift in Sources */, 845B821D26EF80DB00D25C72 /* ChainAccountModel.swift in Sources */, + 77AAE2242AFB67BE006872CC /* OperationSwapDetailsInteractor.swift in Sources */, F4617415260E23E900E8FA3D /* StakingRewardStatusViewModel.swift in Sources */, 4A520B7081BE2D7604B69354 /* AccountImportWireframe.swift in Sources */, 840B3D6C2899CD5A00DA1DA9 /* ParitySignerScanMatcher.swift in Sources */, @@ -22028,6 +22050,7 @@ 88FB7DD12950720800784E08 /* ContainerProtocols.swift in Sources */, 840AE2E529C9AF9C008FF665 /* EtherscanWalletHistoryDecodable.swift in Sources */, 772B1C7D2A93837800A19061 /* StartStakingCustomValidatorListWireframe.swift in Sources */, + 77AAE2202AFB00CB006872CC /* OperationSwapModel.swift in Sources */, 8465DA35298EC5FB00C7CFF1 /* TitleDetailsSheetLayout.swift in Sources */, AEE5FB1C264A610C002B8FDC /* StakingRewardDestSetupLayout.swift in Sources */, 84350ADB28461E5B0031EF24 /* ParaStkYourCollatorsViewModelFactory.swift in Sources */, @@ -22131,6 +22154,7 @@ AE4C53E5268C6F8300B03CE8 /* ValidatorListFilterSortCell.swift in Sources */, 885A6C3229A374B600B65C1A /* ReferendumVotersLocalWrapperFactory.swift in Sources */, 842B17FB28648FDC0014CC57 /* ChainAssetViewModelFactory.swift in Sources */, + 77AAE2262AFC10EE006872CC /* CalculatorFactory.swift in Sources */, 1062C095BC566A1EA8DE1C06 /* CrowdloanContributionSetupViewController.swift in Sources */, 849C7BDB2A1B236900434621 /* GladingBaseView.swift in Sources */, 84EE2FB32891442F00A98816 /* WalletManageViewFactory.swift in Sources */, @@ -22507,6 +22531,7 @@ 577918C3D4AA22D887F605B5 /* ParaStkStakeSetupPresenter.swift in Sources */, 7050A26051FE62DB06B695F1 /* ParaStkStakeSetupInteractor.swift in Sources */, 0CD1F4D100ED82D137AB9834 /* ParaStkStakeSetupViewController.swift in Sources */, + 77AAE2282AFC1167006872CC /* ChainModel+historyId.swift in Sources */, D9ECCCCF1449EFAFD0FA886E /* ParaStkStakeSetupViewLayout.swift in Sources */, 84981EEC29D4385F00948306 /* TransactionHistoryHybridFetcher.swift in Sources */, A714CEAF7A86292E8D679056 /* ParaStkStakeSetupViewFactory.swift in Sources */, @@ -24135,6 +24160,7 @@ 843910CA253F7E6500E3C217 /* SubstrateDataModel.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 77AAE21C2AF56C88006872CC /* SubstrateDataModel20.xcdatamodel */, 0C59E8C72AA5C3F4001E11F3 /* SubstrateDataModel19.xcdatamodel */, 0CC2E55C2A6AAFFD004092E7 /* SubstrateDataModel18.xcdatamodel */, 0C04290B2A67A42A00C3583A /* SubstrateDataModel17.xcdatamodel */, @@ -24155,7 +24181,7 @@ 88787F0328DB3A7B00B115AB /* SubstrateDataModel2.xcdatamodel */, 843910CB253F7E6500E3C217 /* SubstrateDataModel.xcdatamodel */, ); - currentVersion = 0C59E8C72AA5C3F4001E11F3 /* SubstrateDataModel19.xcdatamodel */; + currentVersion = 77AAE21C2AF56C88006872CC /* SubstrateDataModel20.xcdatamodel */; path = SubstrateDataModel.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/novawallet/Common/Extension/Model/TransactionHistoryItem+Subscription.swift b/novawallet/Common/Extension/Model/TransactionHistoryItem+Subscription.swift index 8ebc4835ac..36797d6a85 100644 --- a/novawallet/Common/Extension/Model/TransactionHistoryItem+Subscription.swift +++ b/novawallet/Common/Extension/Model/TransactionHistoryItem+Subscription.swift @@ -55,10 +55,12 @@ extension TransactionHistoryItem { txHash: txHash, timestamp: timestamp, fee: maybeFee, + feeAssetId: nil, blockNumber: result.blockNumber, txIndex: result.txIndex, callPath: result.processingResult.callPath, - call: encodedCall + call: encodedCall, + swap: nil ) } catch { diff --git a/novawallet/Common/Extension/Storage/CDTransactionHistoryItem+CoreDataDecodable.swift b/novawallet/Common/Extension/Storage/CDTransactionHistoryItem+CoreDataDecodable.swift index ed76f32f8a..cd17f001c7 100644 --- a/novawallet/Common/Extension/Storage/CDTransactionHistoryItem+CoreDataDecodable.swift +++ b/novawallet/Common/Extension/Storage/CDTransactionHistoryItem+CoreDataDecodable.swift @@ -3,7 +3,7 @@ import RobinHood import CoreData extension CDTransactionItem: CoreDataCodable { - public func populate(from decoder: Decoder, using _: NSManagedObjectContext) throws { + public func populate(from decoder: Decoder, using context: NSManagedObjectContext) throws { let container = try decoder.container(keyedBy: TransactionHistoryItem.CodingKeys.self) let identifier = try container.decode(String.self, forKey: .identifier) @@ -28,7 +28,11 @@ extension CDTransactionItem: CoreDataCodable { if let fee = try container.decodeIfPresent(String.self, forKey: .fee) { self.fee = fee } - + if let feeAssetId = try container.decodeIfPresent(UInt32.self, forKey: .feeAssetId) { + self.feeAssetId = NSNumber(value: feeAssetId) + } else { + feeAssetId = nil + } let callPath = try container.decode(CallCodingPath.self, forKey: .callPath) callName = callPath.callName moduleName = callPath.moduleName @@ -46,6 +50,14 @@ extension CDTransactionItem: CoreDataCodable { } else { txIndex = nil } + if let swapContainer = try? container.nestedContainer(keyedBy: SwapHistoryData.CodingKeys.self, forKey: .swap) { + let newSwap = CDTransactionSwapItem(context: context) + newSwap.amountIn = try swapContainer.decode(String.self, forKey: .amountIn) + newSwap.amountOut = try swapContainer.decode(String.self, forKey: .amountOut) + newSwap.assetIdIn = try swapContainer.decodeIfPresent(String.self, forKey: .assetIdIn) + newSwap.assetIdOut = try swapContainer.decodeIfPresent(String.self, forKey: .assetIdOut) + swap = newSwap + } } public func encode(to encoder: Encoder) throws { @@ -71,5 +83,13 @@ extension CDTransactionItem: CoreDataCodable { } try container.encodeIfPresent(call, forKey: .call) + + if let swap = swap { + var nestedSwap = container.nestedContainer(keyedBy: SwapHistoryData.CodingKeys.self, forKey: .swap) + try nestedSwap.encode(swap.amountIn, forKey: .amountIn) + try nestedSwap.encode(swap.amountOut, forKey: .amountOut) + try nestedSwap.encodeIfPresent(swap.assetIdIn, forKey: .assetIdIn) + try nestedSwap.encodeIfPresent(swap.assetIdOut, forKey: .assetIdOut) + } } } diff --git a/novawallet/Common/Helpers/TransactionHistory/CalculatorFactory.swift b/novawallet/Common/Helpers/TransactionHistory/CalculatorFactory.swift new file mode 100644 index 0000000000..324b43b844 --- /dev/null +++ b/novawallet/Common/Helpers/TransactionHistory/CalculatorFactory.swift @@ -0,0 +1,16 @@ +protocol CalculatorFactoryProtocol { + func createPriceProvider(for priceId: String?) -> TokenPriceCalculatorProtocol? +} + +final class CalculatorFactory: CalculatorFactoryProtocol { + var priceHistory: [AssetModel.PriceId: PriceHistory?] = [:] + + func createPriceProvider(for priceId: String?) -> TokenPriceCalculatorProtocol? { + guard let priceId = priceId, + let priceHistory = priceHistory[priceId], + let history = priceHistory else { + return nil + } + return TokenPriceCalculator(history: history) + } +} diff --git a/novawallet/Common/Helpers/TransactionHistory/ChainModel+historyId.swift b/novawallet/Common/Helpers/TransactionHistory/ChainModel+historyId.swift new file mode 100644 index 0000000000..c4dd01af01 --- /dev/null +++ b/novawallet/Common/Helpers/TransactionHistory/ChainModel+historyId.swift @@ -0,0 +1,12 @@ +extension ChainModel { + func asset(byHistoryAssetId assetId: String?) -> AssetModel? { + guard let assetId = assetId else { + return nil + } + return assets.first { asset in + let assetMapper = CustomAssetMapper(type: asset.type, typeExtras: asset.typeExtras) + let historyAssetId = try? assetMapper.historyAssetId() + return historyAssetId == assetId + } + } +} diff --git a/novawallet/Common/Migration/SubstrateStorageVersion.swift b/novawallet/Common/Migration/SubstrateStorageVersion.swift index 0e5c3f3de6..498998e6dc 100644 --- a/novawallet/Common/Migration/SubstrateStorageVersion.swift +++ b/novawallet/Common/Migration/SubstrateStorageVersion.swift @@ -18,6 +18,7 @@ enum SubstrateStorageVersion: String, CaseIterable { case version17 = "SubstrateDataModel17" case version18 = "SubstrateDataModel18" case version19 = "SubstrateDataModel19" + case version20 = "SubstrateDataModel20" static var current: SubstrateStorageVersion { allCases.last! @@ -62,6 +63,8 @@ enum SubstrateStorageVersion: String, CaseIterable { case .version18: return .version19 case .version19: + return .version20 + case .version20: return nil } } diff --git a/novawallet/Common/Model/TransactionHistoryItem.swift b/novawallet/Common/Model/TransactionHistoryItem.swift index 28a3ce51fd..13f6153d49 100644 --- a/novawallet/Common/Model/TransactionHistoryItem.swift +++ b/novawallet/Common/Model/TransactionHistoryItem.swift @@ -21,10 +21,12 @@ struct TransactionHistoryItem: Codable { case txHash case timestamp case fee + case feeAssetId case blockNumber case txIndex case callPath case call + case swap } enum Status: Int16, Codable { @@ -44,10 +46,12 @@ struct TransactionHistoryItem: Codable { let txHash: String let timestamp: Int64 let fee: String? + let feeAssetId: String? let blockNumber: UInt64? let txIndex: UInt16? let callPath: CallCodingPath let call: Data? + let swap: SwapHistoryData? } extension TransactionHistoryItem: Identifiable { @@ -83,3 +87,17 @@ extension TransactionHistoryItemSource { } } } + +struct SwapHistoryData: Codable { + enum CodingKeys: String, CodingKey { + case amountIn + case assetIdIn + case amountOut + case assetIdOut + } + + let amountIn: String + let assetIdIn: String? + let amountOut: String + let assetIdOut: String? +} diff --git a/novawallet/Common/Model/TransactionType.swift b/novawallet/Common/Model/TransactionType.swift index 936318b1ac..e48357e6d9 100644 --- a/novawallet/Common/Model/TransactionType.swift +++ b/novawallet/Common/Model/TransactionType.swift @@ -8,4 +8,5 @@ enum TransactionType: String, CaseIterable, Equatable { case extrinsic = "EXTRINSIC" case poolReward = "POOL REWARD" case poolSlash = "POOL SLASH" + case swap = "SWAP" } diff --git a/novawallet/Common/Model/WalletHistoryFilter.swift b/novawallet/Common/Model/WalletHistoryFilter.swift index 5b766619cc..3a8804b281 100644 --- a/novawallet/Common/Model/WalletHistoryFilter.swift +++ b/novawallet/Common/Model/WalletHistoryFilter.swift @@ -6,7 +6,8 @@ struct WalletHistoryFilter: OptionSet { static let transfers = WalletHistoryFilter(rawValue: 1 << 0) static let rewardsAndSlashes = WalletHistoryFilter(rawValue: 1 << 1) static let extrinsics = WalletHistoryFilter(rawValue: 1 << 2) - static let all: WalletHistoryFilter = [.transfers, .rewardsAndSlashes, .extrinsics] + static let swaps = WalletHistoryFilter(rawValue: 1 << 3) + static let all: WalletHistoryFilter = [.transfers, .rewardsAndSlashes, .extrinsics, .swaps] let rawValue: UInt8 diff --git a/novawallet/Common/Network/Etherscan/ERC20/EtherscanERC20HistoryResponse.swift b/novawallet/Common/Network/Etherscan/ERC20/EtherscanERC20HistoryResponse.swift index bf84a84eae..67b6a199dc 100644 --- a/novawallet/Common/Network/Etherscan/ERC20/EtherscanERC20HistoryResponse.swift +++ b/novawallet/Common/Network/Etherscan/ERC20/EtherscanERC20HistoryResponse.swift @@ -78,10 +78,12 @@ extension EtherscanERC20HistoryResponse.Element: WalletRemoteHistoryItemProtocol txHash: txHash, timestamp: timeStamp, fee: String(feeInPlank), + feeAssetId: nil, blockNumber: itemBlockNumber, txIndex: itemExtrinsicIndex, callPath: .erc20Tranfer, - call: nil + call: nil, + swap: nil ) } } diff --git a/novawallet/Common/Network/Etherscan/Native/EtherscanNativeOperationFactory.swift b/novawallet/Common/Network/Etherscan/Native/EtherscanNativeOperationFactory.swift index 26bca6ece1..6aa9098f9b 100644 --- a/novawallet/Common/Network/Etherscan/Native/EtherscanNativeOperationFactory.swift +++ b/novawallet/Common/Network/Etherscan/Native/EtherscanNativeOperationFactory.swift @@ -63,6 +63,8 @@ final class EtherscanNativeOperationFactory: EtherscanBaseOperationFactory { return filter.contains(.extrinsics) case .rewards, .poolRewards: return filter.contains(.rewardsAndSlashes) + case .swaps: + return filter.contains(.swaps) } } diff --git a/novawallet/Common/Network/Etherscan/Native/EtherscanTxHistoryResponse.swift b/novawallet/Common/Network/Etherscan/Native/EtherscanTxHistoryResponse.swift index 82309cae71..89002d2a31 100644 --- a/novawallet/Common/Network/Etherscan/Native/EtherscanTxHistoryResponse.swift +++ b/novawallet/Common/Network/Etherscan/Native/EtherscanTxHistoryResponse.swift @@ -98,10 +98,12 @@ extension EtherscanTxHistoryResponse.Element: WalletRemoteHistoryItemProtocol { txHash: txHash, timestamp: timeStamp, fee: String(feeInPlank), + feeAssetId: nil, blockNumber: itemBlockNumber, txIndex: itemExtrinsicIndex, callPath: isTransfer ? .evmNativeTransfer : .evmNativeTransaction, - call: callData + call: callData, + swap: nil ) } } diff --git a/novawallet/Common/Network/Subquery/Models/SubqueryHistory.swift b/novawallet/Common/Network/Subquery/Models/SubqueryHistory.swift index f8dfb3fa4d..9f98ebedda 100644 --- a/novawallet/Common/Network/Subquery/Models/SubqueryHistory.swift +++ b/novawallet/Common/Network/Subquery/Models/SubqueryHistory.swift @@ -48,6 +48,17 @@ struct SubqueryPoolRewardOrSlash: Codable { let poolId: Int } +struct SubquerySwap: Codable { + let assetIdIn: String? + let amountIn: String + let assetIdOut: String? + let amountOut: String + let sender: String + let receiver: String + let fee: String + let eventIdx: Int +} + struct SubqueryHistoryElement: Decodable { enum CodingKeys: String, CodingKey { case identifier = "id" @@ -61,6 +72,7 @@ struct SubqueryHistoryElement: Decodable { case transfer case assetTransfer case poolReward + case swap } let identifier: String @@ -74,6 +86,7 @@ struct SubqueryHistoryElement: Decodable { let transfer: SubqueryTransfer? let assetTransfer: SubqueryTransfer? let poolReward: SubqueryPoolRewardOrSlash? + let swap: SubquerySwap? } struct SubqueryHistoryData: Decodable { diff --git a/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift b/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift index 50577fe735..6d6ab14cfc 100644 --- a/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift +++ b/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift @@ -31,6 +31,8 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { return .rewards } else if poolReward != nil { return .poolRewards + } else if swap != nil { + return .swaps } else { return .extrinsics } @@ -45,6 +47,12 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { chainAssetId: chainAsset.chainAssetId, chainFormat: chainAsset.chain.chainFormat ) + } else if let swap = swap { + return createTransactionFromSwap( + swap, + chainAssetId: chainAsset.chainAssetId, + chainFormat: chainAsset.chain.chainFormat + ) } else if let reward = reward { return createTransactionFromReward( reward, @@ -93,10 +101,47 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { txHash: extrinsicHash ?? identifier, timestamp: itemTimestamp, fee: transfer.fee, + feeAssetId: nil, blockNumber: blockNumber, txIndex: nil, callPath: CallCodingPath.transfer, - call: nil + call: nil, + swap: nil + ) + } + + private func createTransactionFromSwap( + _ swap: SubquerySwap, + chainAssetId: ChainAssetId, + chainFormat: ChainFormat + ) -> TransactionHistoryItem { + let source = TransactionHistoryItemSource.substrate + let remoteIdentifier = TransactionHistoryItem.createIdentifier(from: identifier, source: source) + return .init( + identifier: remoteIdentifier, + source: source, + chainId: chainAssetId.chainId, + assetId: chainAssetId.assetId, + sender: swap.sender.normalize(for: chainFormat) ?? swap.sender, + receiver: swap.receiver.normalize(for: chainFormat) ?? swap.receiver, + amountInPlank: nil, + // TODO: Status decoding + status: .success, + txHash: extrinsicHash ?? identifier, + timestamp: itemTimestamp, + fee: swap.fee, + // TODO: feeAssetId decoding + feeAssetId: nil, + blockNumber: blockNumber, + txIndex: UInt16(swap.eventIdx), + callPath: CallCodingPath.swap, + call: nil, + swap: .init( + amountIn: swap.amountIn, + assetIdIn: swap.assetIdIn, + amountOut: swap.amountOut, + assetIdOut: swap.assetIdOut + ) ) } @@ -126,10 +171,12 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { txHash: extrinsicHash ?? identifier, timestamp: itemTimestamp, fee: nil, + feeAssetId: nil, blockNumber: blockNumber, txIndex: nil, callPath: reward.isReward ? .reward : .slash, - call: try? JSONEncoder().encode(context) + call: try? JSONEncoder().encode(context), + swap: nil ) } @@ -158,10 +205,12 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { txHash: extrinsicHash ?? identifier, timestamp: itemTimestamp, fee: nil, + feeAssetId: nil, blockNumber: blockNumber, txIndex: nil, callPath: reward.isReward ? .poolReward : .poolSlash, - call: try? JSONEncoder().encode(context) + call: try? JSONEncoder().encode(context), + swap: nil ) } @@ -185,10 +234,12 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { txHash: extrinsicHash ?? identifier, timestamp: itemTimestamp, fee: extrinsic.fee, + feeAssetId: nil, blockNumber: blockNumber, txIndex: nil, callPath: CallCodingPath(moduleName: extrinsic.module, callName: extrinsic.call), - call: extrinsic.call.data(using: .utf8) + call: extrinsic.call.data(using: .utf8), + swap: nil ) } diff --git a/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift b/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift index 6382121d2a..eccf3d46c4 100644 --- a/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift +++ b/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift @@ -87,6 +87,14 @@ final class SubqueryHistoryOperationFactory { } } + if filter.contains(.swaps) { + if let assetId = assetId { + filterStrings.append(prepareAssetIdFilter(assetId)) + } else { + filterStrings.append("{ swap: { isNull: false } }") + } + } + return filterStrings.joined(separator: ",") } @@ -127,6 +135,7 @@ final class SubqueryHistoryOperationFactory { extrinsic \(transferField) \(poolRewardField) + swap } } } diff --git a/novawallet/Common/Services/PersistExtrinsicService/PersistExtrinsicFactory.swift b/novawallet/Common/Services/PersistExtrinsicService/PersistExtrinsicFactory.swift index a0a69df69d..f6f84d0ac0 100644 --- a/novawallet/Common/Services/PersistExtrinsicService/PersistExtrinsicFactory.swift +++ b/novawallet/Common/Services/PersistExtrinsicService/PersistExtrinsicFactory.swift @@ -46,10 +46,12 @@ final class PersistExtrinsicFactory: PersistExtrinsicFactoryProtocol { txHash: txHash, timestamp: timestamp, fee: feeString, + feeAssetId: nil, blockNumber: nil, txIndex: nil, callPath: details.callPath, - call: nil + call: nil, + swap: nil ) let operation = repository.saveOperation({ [transferItem] }, { [] }) @@ -80,10 +82,12 @@ final class PersistExtrinsicFactory: PersistExtrinsicFactoryProtocol { txHash: txHash, timestamp: timestamp, fee: feeString, + feeAssetId: nil, blockNumber: nil, txIndex: nil, callPath: details.callPath, - call: nil + call: nil, + swap: nil ) let operation = repository.saveOperation({ [item] }, { [] }) diff --git a/novawallet/Common/Services/RemoteSubscription/Evm/ERC20/ContractTransactionHistoryUpdater.swift b/novawallet/Common/Services/RemoteSubscription/Evm/ERC20/ContractTransactionHistoryUpdater.swift index 0f8425efbe..5d1821cff3 100644 --- a/novawallet/Common/Services/RemoteSubscription/Evm/ERC20/ContractTransactionHistoryUpdater.swift +++ b/novawallet/Common/Services/RemoteSubscription/Evm/ERC20/ContractTransactionHistoryUpdater.swift @@ -85,10 +85,12 @@ final class ContractTransactionHistoryUpdater { txHash: transactionHashString, timestamp: Int64(Date().timeIntervalSince1970), fee: fee, + feeAssetId: nil, blockNumber: UInt64(event.blockNumber), txIndex: nil, callPath: CallCodingPath.erc20Tranfer, - call: nil + call: nil, + swap: nil ) return [historyItem] diff --git a/novawallet/Common/Services/RemoteSubscription/Evm/Native/EvmNativeTransactionHistoryUpdater.swift b/novawallet/Common/Services/RemoteSubscription/Evm/Native/EvmNativeTransactionHistoryUpdater.swift index e3a2efdbed..d19940f6e8 100644 --- a/novawallet/Common/Services/RemoteSubscription/Evm/Native/EvmNativeTransactionHistoryUpdater.swift +++ b/novawallet/Common/Services/RemoteSubscription/Evm/Native/EvmNativeTransactionHistoryUpdater.swift @@ -70,10 +70,12 @@ final class EvmNativeTransactionHistoryUpdater { txHash: txHash, timestamp: timestamp, fee: fee, + feeAssetId: nil, blockNumber: UInt64(blockNumber), txIndex: nil, callPath: transaction.isNativeTransfer ? .evmNativeTransfer : .evmNativeTransaction, - call: nil + call: nil, + swap: nil ) return [historyItem] diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion index 46d2cdef68..716e4bb7fc 100644 --- a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - SubstrateDataModel19.xcdatamodel + SubstrateDataModel20.xcdatamodel diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel20.xcdatamodel/contents b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel20.xcdatamodel/contents new file mode 100644 index 0000000000..7ac17082ab --- /dev/null +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel20.xcdatamodel/contents @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/novawallet/Common/Storage/SubstrateDataStorageFacade.swift b/novawallet/Common/Storage/SubstrateDataStorageFacade.swift index 6b6e13d478..a8ccd5b55f 100644 --- a/novawallet/Common/Storage/SubstrateDataStorageFacade.swift +++ b/novawallet/Common/Storage/SubstrateDataStorageFacade.swift @@ -4,7 +4,7 @@ import CoreData enum SubstrateStorageParams { static let databaseName = "SubstrateDataModel.sqlite" static let modelDirectory: String = "SubstrateDataModel.momd" - static let modelVersion: SubstrateStorageVersion = .version19 + static let modelVersion: SubstrateStorageVersion = .version20 static let storageDirectoryURL: URL = { let baseURL = FileManager.default.urls( diff --git a/novawallet/Common/Substrate/Types/CallCodingPath.swift b/novawallet/Common/Substrate/Types/CallCodingPath.swift index b6a928e61b..068f93a8c7 100644 --- a/novawallet/Common/Substrate/Types/CallCodingPath.swift +++ b/novawallet/Common/Substrate/Types/CallCodingPath.swift @@ -119,9 +119,17 @@ extension CallCodingPath { CallCodingPath(moduleName: "Substrate", callName: "poolSlash") } + static var swap: CallCodingPath { + CallCodingPath(moduleName: "Substrate", callName: "swap") + } + var isAnyStakingRewardOrSlash: Bool { [.slash, .reward, .poolReward, .poolSlash].contains(self) } + + var isSwap: Bool { + [.swap].contains(self) + } } // MARK: Filter @@ -140,6 +148,10 @@ extension CallCodingPath { return false } + if !filter.contains(.swaps), !isSwap { + return false + } + return true } } diff --git a/novawallet/Modules/OperationDetails/Model/OperationDetailsModel.swift b/novawallet/Modules/OperationDetails/Model/OperationDetailsModel.swift index 8f33115776..e00dd0f81e 100644 --- a/novawallet/Modules/OperationDetails/Model/OperationDetailsModel.swift +++ b/novawallet/Modules/OperationDetails/Model/OperationDetailsModel.swift @@ -15,6 +15,7 @@ struct OperationDetailsModel { case contract(_ model: OperationContractCallModel) case poolReward(_ model: OperationPoolRewardOrSlashModel) case poolSlash(_ model: OperationPoolRewardOrSlashModel) + case swap(_ model: OperationSwapModel) } let time: Date diff --git a/novawallet/Modules/OperationDetails/Model/OperationSwapModel.swift b/novawallet/Modules/OperationDetails/Model/OperationSwapModel.swift new file mode 100644 index 0000000000..fd257617e3 --- /dev/null +++ b/novawallet/Modules/OperationDetails/Model/OperationSwapModel.swift @@ -0,0 +1,18 @@ +import Foundation +import BigInt + +struct OperationSwapModel { + let txHash: String + let chain: ChainModel + let assetIn: AssetModel + let amountIn: BigUInt + let priceIn: PriceData? + let assetOut: AssetModel + let amountOut: BigUInt + let priceOut: PriceData? + let fee: BigUInt + let feePrice: PriceData? + let feeAsset: AssetModel + let wallet: WalletDisplayAddress + let isOutgoing: Bool +} diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsContractProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsContractProvider.swift index fae701845f..df18817637 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsContractProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsContractProvider.swift @@ -6,10 +6,12 @@ final class OperationDetailsContractProvider: OperationDetailsBaseProvider {} extension OperationDetailsContractProvider: OperationDetailsDataProviderProtocol { func extractOperationData( replacingWith newFee: BigUInt?, - priceCalculator _: TokenPriceCalculatorProtocol?, - feePriceCalculator: TokenPriceCalculatorProtocol?, + calculatorFactory: CalculatorFactoryProtocol, progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void ) { + let priceCalculator = calculatorFactory.createPriceProvider(for: chainAsset.asset.priceId) + let feePriceCalculator = calculatorFactory.createPriceProvider(for: chainAsset.chain.utilityAsset()?.priceId) + let fee: BigUInt = newFee ?? transaction.feeInPlankIntOrZero let feePriceData = feePriceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { PriceData.amount($0) diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderFactory.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderFactory.swift index 01b16b4cb7..36e258c5f4 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderFactory.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderFactory.swift @@ -92,6 +92,12 @@ extension OperationDetailsDataProviderFactory: OperationDetailsDataProviderFacto runtimeService: runtimeService, operationQueue: operationQueue ) + case .swap: + return OperationDetailsSwapProvider( + selectedAccount: selectedAccount, + chainAsset: chainAsset, + transaction: transaction + ) } } } diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderProtocol.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderProtocol.swift index 5f57292f28..88aa21e0dc 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderProtocol.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderProtocol.swift @@ -4,8 +4,7 @@ import BigInt protocol OperationDetailsDataProviderProtocol { func extractOperationData( replacingWith newFee: BigUInt?, - priceCalculator: TokenPriceCalculatorProtocol?, - feePriceCalculator: TokenPriceCalculatorProtocol?, + calculatorFactory: CalculatorFactoryProtocol, progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void ) } diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDirectStakingProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDirectStakingProvider.swift index e8a816a78a..b6ac61b08e 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDirectStakingProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDirectStakingProvider.swift @@ -52,14 +52,14 @@ final class OperationDetailsDirectStakingProvider: OperationDetailsBaseProvider, extension OperationDetailsDirectStakingProvider: OperationDetailsDataProviderProtocol { func extractOperationData( replacingWith _: BigUInt?, - priceCalculator: TokenPriceCalculatorProtocol?, - feePriceCalculator _: TokenPriceCalculatorProtocol?, + calculatorFactory: CalculatorFactoryProtocol, progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void ) { let context = try? transaction.call.map { try JSONDecoder().decode(HistoryRewardContext.self, from: $0) } + let priceCalculator = calculatorFactory.createPriceProvider(for: chainAsset.asset.priceId) let eventId = getEventId(from: context) ?? transaction.txHash let amount = transaction.amountInPlankIntOrZero diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsExtrinsicProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsExtrinsicProvider.swift index fcb1a38b41..edf8f1cd9a 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsExtrinsicProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsExtrinsicProvider.swift @@ -6,8 +6,7 @@ final class OperationDetailsExtrinsicProvider: OperationDetailsBaseProvider {} extension OperationDetailsExtrinsicProvider: OperationDetailsDataProviderProtocol { func extractOperationData( replacingWith newFee: BigUInt?, - priceCalculator _: TokenPriceCalculatorProtocol?, - feePriceCalculator: TokenPriceCalculatorProtocol?, + calculatorFactory: CalculatorFactoryProtocol, progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void ) { guard let accountAddress = accountAddress else { @@ -15,6 +14,7 @@ extension OperationDetailsExtrinsicProvider: OperationDetailsDataProviderProtoco return } + let feePriceCalculator = calculatorFactory.createPriceProvider(for: chain.utilityAsset()?.priceId) let fee = newFee ?? transaction.feeInPlankIntOrZero let feePriceData = feePriceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { PriceData.amount($0) diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsPoolStakingProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsPoolStakingProvider.swift index 2be201ea03..3d63a36405 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsPoolStakingProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsPoolStakingProvider.swift @@ -133,14 +133,14 @@ final class OperationDetailsPoolStakingProvider: OperationDetailsBaseProvider, A extension OperationDetailsPoolStakingProvider: OperationDetailsDataProviderProtocol { func extractOperationData( replacingWith _: BigUInt?, - priceCalculator: TokenPriceCalculatorProtocol?, - feePriceCalculator _: TokenPriceCalculatorProtocol?, + calculatorFactory: CalculatorFactoryProtocol, progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void ) { let optContext = try? transaction.call.map { try JSONDecoder().decode(HistoryPoolRewardContext.self, from: $0) } + let priceCalculator = calculatorFactory.createPriceProvider(for: chainAsset.asset.priceId) let eventId = getEventId(from: optContext) ?? transaction.txHash let amount = transaction.amountInPlankIntOrZero let priceData = priceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift new file mode 100644 index 0000000000..755b638606 --- /dev/null +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift @@ -0,0 +1,93 @@ +import Foundation +import BigInt +import RobinHood + +final class OperationDetailsSwapProvider { + let selectedAccount: MetaChainAccountResponse + let chainAsset: ChainAsset + let transaction: TransactionHistoryItem + var chain: ChainModel { chainAsset.chain } + + init( + selectedAccount: MetaChainAccountResponse, + chainAsset: ChainAsset, + transaction: TransactionHistoryItem + ) { + self.selectedAccount = selectedAccount + self.chainAsset = chainAsset + self.transaction = transaction + } +} + +extension OperationDetailsSwapProvider: OperationDetailsDataProviderProtocol { + func extractOperationData( + replacingWith newFee: BigUInt?, + calculatorFactory: CalculatorFactoryProtocol, + progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void + ) { + guard + let swap = transaction.swap, + let assetIn = chain.asset(byHistoryAssetId: swap.assetIdIn) ?? chain.utilityAsset(), + let assetOut = chain.asset(byHistoryAssetId: swap.assetIdOut) ?? chain.utilityAsset(), + let feeAsset = chain.asset(byHistoryAssetId: transaction.feeAssetId) ?? chain.utilityAsset(), + let wallet = WalletDisplayAddress(response: selectedAccount) else { + progressClosure(nil) + return + } + + let isOutgoing = assetIn.assetId == chainAsset.asset.assetId + let timestamp = UInt64(bitPattern: transaction.timestamp) + + let priceIn = calculatePrice( + calculatorFactory: calculatorFactory, + assetModel: assetIn, + timestamp: timestamp + ) + + let priceOut = calculatePrice( + calculatorFactory: calculatorFactory, + assetModel: assetOut, + timestamp: timestamp + ) + + let feePriceData = calculatePrice( + calculatorFactory: calculatorFactory, + assetModel: feeAsset, + timestamp: timestamp + ) + + let fee = newFee ?? transaction.feeInPlankIntOrZero + let txId = transaction.txHash + + let model = OperationSwapModel( + txHash: txId, + chain: chain, + assetIn: assetIn, + amountIn: BigUInt(swap.amountIn) ?? 0, + priceIn: priceIn, + assetOut: assetOut, + amountOut: BigUInt(swap.amountOut) ?? 0, + priceOut: priceOut, + fee: fee, + feePrice: feePriceData, + feeAsset: feeAsset, + wallet: wallet, + isOutgoing: isOutgoing + ) + progressClosure(.swap(model)) + } + + private func calculatePrice( + calculatorFactory: CalculatorFactoryProtocol, + assetModel: AssetModel?, + timestamp: UInt64 + ) -> PriceData? { + guard let priceId = assetModel?.priceId else { + return nil + } + let provider = calculatorFactory.createPriceProvider(for: priceId) + return provider?.calculatePrice(for: timestamp).map { + PriceData.amount($0) + } + } +} diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsTransferProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsTransferProvider.swift index 546002dd27..262d02640e 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsTransferProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsTransferProvider.swift @@ -27,14 +27,15 @@ final class OperationDetailsTransferProvider: OperationDetailsBaseProvider, Acco extension OperationDetailsTransferProvider: OperationDetailsDataProviderProtocol { func extractOperationData( replacingWith newFee: BigUInt?, - priceCalculator: TokenPriceCalculatorProtocol?, - feePriceCalculator: TokenPriceCalculatorProtocol?, + calculatorFactory: CalculatorFactoryProtocol, progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void ) { guard let accountAddress = accountAddress else { progressClosure(nil) return } + let priceCalculator = calculatorFactory.createPriceProvider(for: chainAsset.asset.priceId) + let feePriceCalculator = calculatorFactory.createPriceProvider(for: chainAsset.chain.utilityAsset()?.priceId) let peerAddress = (transaction.sender == accountAddress ? transaction.receiver : transaction.sender) ?? transaction.sender let accountId = try? peerAddress.toAccountId(using: chain.chainFormat) diff --git a/novawallet/Modules/OperationDetails/OperationDetailsBaseInteractor.swift b/novawallet/Modules/OperationDetails/OperationDetailsBaseInteractor.swift new file mode 100644 index 0000000000..3c42a60d4d --- /dev/null +++ b/novawallet/Modules/OperationDetails/OperationDetailsBaseInteractor.swift @@ -0,0 +1,157 @@ +import Foundation +import BigInt +import RobinHood + +enum OperationDetailsInteractorError: Error { + case unsupportTxType +} + +class OperationDetailsBaseInteractor: AccountFetching, AnyCancellableCleaning { + weak var presenter: OperationDetailsInteractorOutputProtocol? + + let transaction: TransactionHistoryItem + let chainAsset: ChainAsset + + var chain: ChainModel { chainAsset.chain } + + let transactionLocalSubscriptionFactory: TransactionLocalSubscriptionFactoryProtocol + let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol + let operationDataProvider: OperationDetailsDataProviderProtocol + var priceProviders: [AssetModel.PriceId: AnySingleValueProvider] = [:] + + private var transactionProvider: StreamableProvider? + private var priceCalculators: [AssetModel.PriceId: TokenPriceCalculatorProtocol] = [:] + private var calculatorFactory = CalculatorFactory() + + init( + transaction: TransactionHistoryItem, + chainAsset: ChainAsset, + transactionLocalSubscriptionFactory: TransactionLocalSubscriptionFactoryProtocol, + currencyManager: CurrencyManagerProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + operationDataProvider: OperationDetailsDataProviderProtocol + ) { + self.transaction = transaction + self.chainAsset = chainAsset + self.transactionLocalSubscriptionFactory = transactionLocalSubscriptionFactory + self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory + self.operationDataProvider = operationDataProvider + self.currencyManager = currencyManager + } + + private func extractStatus( + overridingBy newStatus: OperationDetailsModel.Status? + ) -> OperationDetailsModel.Status { + if let newStatus = newStatus { + return newStatus + } else { + switch transaction.status { + case .success: + return .completed + case .pending: + return .pending + case .failed: + return .failed + } + } + } + + private func extractOperationData( + replacingIfExists newFee: BigUInt?, + _ completion: @escaping (OperationDetailsModel.OperationData?) -> Void + ) { + operationDataProvider.extractOperationData( + replacingWith: newFee, + calculatorFactory: calculatorFactory, + progressClosure: completion + ) + } + + private func provideModel( + for operationData: OperationDetailsModel.OperationData, + overridingBy newStatus: OperationDetailsModel.Status? + ) { + let time = Date(timeIntervalSince1970: TimeInterval(transaction.timestamp)) + let status = extractStatus(overridingBy: newStatus) + + let details = OperationDetailsModel( + time: time, + status: status, + operation: operationData + ) + + presenter?.didReceiveDetails(result: .success(details)) + } + + private func provideModel( + overridingBy newStatus: OperationDetailsModel.Status?, + newFee: BigUInt? + ) { + extractOperationData(replacingIfExists: newFee) { [weak self] operationData in + if let operationData = operationData { + self?.provideModel(for: operationData, overridingBy: newStatus) + } else { + let error = OperationDetailsInteractorError.unsupportTxType + self?.presenter?.didReceiveDetails(result: .failure(error)) + } + } + } + + func setupPriceHistorySubscription() { + fatalError("This function should be overriden") + } +} + +extension OperationDetailsBaseInteractor: OperationDetailsInteractorInputProtocol { + func setup() { + provideModel(overridingBy: nil, newFee: nil) + + transactionProvider = subscribeToTransaction(for: transaction.identifier, chainId: chain.chainId) + + setupPriceHistorySubscription() + } +} + +extension OperationDetailsBaseInteractor: TransactionLocalStorageSubscriber, + TransactionLocalSubscriptionHandler { + func handleTransactions(result: Result<[DataProviderChange], Error>) { + switch result { + case let .success(changes): + if let transaction = changes.reduceToLastChange() { + let newFee = transaction.fee.flatMap { BigUInt($0) } + switch transaction.status { + case .success: + provideModel(overridingBy: .completed, newFee: newFee) + case .failed: + provideModel(overridingBy: .failed, newFee: newFee) + case .pending: + provideModel(overridingBy: .pending, newFee: newFee) + } + } + case let .failure(error): + presenter?.didReceiveDetails(result: .failure(error)) + } + } +} + +extension OperationDetailsBaseInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { + func handlePriceHistory( + result: Result, + priceId: AssetModel.PriceId + ) { + switch result { + case let .success(history): + calculatorFactory.priceHistory[priceId] = history + case let .failure(error): + presenter?.didReceiveDetails(result: .failure(error)) + } + } +} + +extension OperationDetailsBaseInteractor: SelectedCurrencyDepending { + func applyCurrency() { + if presenter != nil { + setupPriceHistorySubscription() + } + } +} diff --git a/novawallet/Modules/OperationDetails/OperationDetailsInteractor.swift b/novawallet/Modules/OperationDetails/OperationDetailsInteractor.swift index 2be1c8bba8..0eb1a34c75 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsInteractor.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsInteractor.swift @@ -2,183 +2,23 @@ import Foundation import BigInt import RobinHood -enum OperationDetailsInteractorError: Error { - case unsupportTxType -} - -final class OperationDetailsInteractor: AccountFetching, AnyCancellableCleaning { - weak var presenter: OperationDetailsInteractorOutputProtocol? - - let transaction: TransactionHistoryItem - let chainAsset: ChainAsset - - var chain: ChainModel { chainAsset.chain } - - let transactionLocalSubscriptionFactory: TransactionLocalSubscriptionFactoryProtocol - let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol - let operationDataProvider: OperationDetailsDataProviderProtocol - - private var transactionProvider: StreamableProvider? - private var priceProvider: AnySingleValueProvider? - private var feePriceProvider: AnySingleValueProvider? - - private var priceCalculator: TokenPriceCalculatorProtocol? - private var feePriceCalculator: TokenPriceCalculatorProtocol? - - init( - transaction: TransactionHistoryItem, - chainAsset: ChainAsset, - transactionLocalSubscriptionFactory: TransactionLocalSubscriptionFactoryProtocol, - currencyManager: CurrencyManagerProtocol, - priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, - operationDataProvider: OperationDetailsDataProviderProtocol - ) { - self.transaction = transaction - self.chainAsset = chainAsset - self.transactionLocalSubscriptionFactory = transactionLocalSubscriptionFactory - self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory - self.operationDataProvider = operationDataProvider - self.currencyManager = currencyManager - } - - private func extractStatus( - overridingBy newStatus: OperationDetailsModel.Status? - ) -> OperationDetailsModel.Status { - if let newStatus = newStatus { - return newStatus - } else { - switch transaction.status { - case .success: - return .completed - case .pending: - return .pending - case .failed: - return .failed - } - } - } - - private func extractOperationData( - replacingIfExists newFee: BigUInt?, - _ completion: @escaping (OperationDetailsModel.OperationData?) -> Void - ) { - operationDataProvider.extractOperationData( - replacingWith: newFee, - priceCalculator: priceCalculator, - feePriceCalculator: feePriceCalculator, - progressClosure: completion - ) - } - - private func provideModel( - for operationData: OperationDetailsModel.OperationData, - overridingBy newStatus: OperationDetailsModel.Status? - ) { - let time = Date(timeIntervalSince1970: TimeInterval(transaction.timestamp)) - let status = extractStatus(overridingBy: newStatus) - - let details = OperationDetailsModel( - time: time, - status: status, - operation: operationData - ) - - presenter?.didReceiveDetails(result: .success(details)) - } - - private func provideModel( - overridingBy newStatus: OperationDetailsModel.Status?, - newFee: BigUInt? - ) { - extractOperationData(replacingIfExists: newFee) { [weak self] operationData in - if let operationData = operationData { - self?.provideModel(for: operationData, overridingBy: newStatus) - } else { - let error = OperationDetailsInteractorError.unsupportTxType - self?.presenter?.didReceiveDetails(result: .failure(error)) - } +final class OperationDetailsInteractor: OperationDetailsBaseInteractor { + override func setupPriceHistorySubscription() { + let priceId = chainAsset.asset.priceId + + if let priceId = priceId { + priceProviders[priceId] = subscribeToPriceHistory( + for: priceId, + currency: selectedCurrency + ) } - } - - private func setupPriceHistorySubscription() { - priceProvider = priceHistoryProvider(for: chainAsset.asset) - - let utilityAsset = chainAsset.chain.utilityAsset() - feePriceProvider = utilityAsset?.priceId == chainAsset.asset.priceId ? - priceProvider : priceHistoryProvider(for: utilityAsset) - } - - private func priceHistoryProvider(for asset: AssetModel?) -> AnySingleValueProvider? { - guard let asset = asset else { - return nil - } - guard let priceId = asset.priceId else { - return nil - } - - return subscribeToPriceHistory(for: priceId, currency: selectedCurrency) - } -} - -extension OperationDetailsInteractor: OperationDetailsInteractorInputProtocol { - func setup() { - provideModel(overridingBy: nil, newFee: nil) - - transactionProvider = subscribeToTransaction(for: transaction.identifier, chainId: chain.chainId) - - setupPriceHistorySubscription() - } -} - -extension OperationDetailsInteractor: TransactionLocalStorageSubscriber, - TransactionLocalSubscriptionHandler { - func handleTransactions(result: Result<[DataProviderChange], Error>) { - switch result { - case let .success(changes): - if let transaction = changes.reduceToLastChange() { - let newFee = transaction.fee.flatMap { BigUInt($0) } - switch transaction.status { - case .success: - provideModel(overridingBy: .completed, newFee: newFee) - case .failed: - provideModel(overridingBy: .failed, newFee: newFee) - case .pending: - provideModel(overridingBy: .pending, newFee: newFee) - } - } - case let .failure(error): - presenter?.didReceiveDetails(result: .failure(error)) - } - } -} - -extension OperationDetailsInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { - func handlePriceHistory( - result: Result, - priceId: AssetModel.PriceId - ) { - switch result { - case let .success(history): - if let history = history { - if chainAsset.asset.priceId == priceId { - priceCalculator = TokenPriceCalculator(history: history) - } - if chainAsset.chain.utilityAsset()?.priceId == priceId { - feePriceCalculator = TokenPriceCalculator(history: history) - } - provideModel(overridingBy: nil, newFee: nil) - } - - case let .failure(error): - presenter?.didReceiveDetails(result: .failure(error)) - } - } -} -extension OperationDetailsInteractor: SelectedCurrencyDepending { - func applyCurrency() { - if presenter != nil { - setupPriceHistorySubscription() + if let utilityAssetPriceId = chainAsset.chain.utilityAsset()?.priceId, + utilityAssetPriceId != priceId { + priceProviders[utilityAssetPriceId] = subscribeToPriceHistory( + for: utilityAssetPriceId, + currency: selectedCurrency + ) } } } diff --git a/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift b/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift index 9cef0aa98e..555bbca7d9 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift @@ -105,6 +105,8 @@ extension OperationDetailsPresenter: OperationDetailsPresenterProtocol { return } presentAddressOptions(address) + case let .swap(model): + presentAddressOptions(model.wallet.address) case .none: break } @@ -135,7 +137,8 @@ extension OperationDetailsPresenter: OperationDetailsPresenterProtocol { presentTransactionHashOptions(contractModel.txHash) case let .poolReward(poolRewardOrSlashModel), let .poolSlash(poolRewardOrSlashModel): presentEventIdOptions(poolRewardOrSlashModel.eventId) - case .none: + // TODO: repeat swap action + case .none, .swap: break } } diff --git a/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift b/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift index 08cb0360b3..d91bf5b2cb 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift @@ -37,42 +37,37 @@ struct OperationDetailsViewFactory { storageFacade: SubstrateDataStorageFacade.shared, operationQueue: OperationManagerFacade.sharedDefaultQueue ) - - let interactor = OperationDetailsInteractor( - transaction: transaction, - chainAsset: chainAsset, - transactionLocalSubscriptionFactory: transactionLocalSubscriptionFactory, - currencyManager: currencyManager, - priceLocalSubscriptionFactory: PriceProviderFactory.shared, - operationDataProvider: operationDetailsDataProvider - ) + let interactor: OperationDetailsBaseInteractor + + if transaction.swap != nil { + interactor = createSwapInteractor( + transaction: transaction, + chainAsset: chainAsset, + transactionLocalSubscriptionFactory: transactionLocalSubscriptionFactory, + currencyManager: currencyManager, + priceLocalSubscriptionFactory: PriceProviderFactory.shared, + operationDataProvider: operationDetailsDataProvider + ) + } else { + interactor = createInteractor( + transaction: transaction, + chainAsset: chainAsset, + transactionLocalSubscriptionFactory: transactionLocalSubscriptionFactory, + currencyManager: currencyManager, + priceLocalSubscriptionFactory: PriceProviderFactory.shared, + operationDataProvider: operationDetailsDataProvider + ) + } let wireframe = OperationDetailsWireframe() let localizationManager = LocalizationManager.shared let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) - let balanceViewModelFactory = BalanceViewModelFactory( - targetAssetInfo: chainAsset.assetDisplayInfo, - priceAssetInfoFactory: priceAssetInfoFactory - ) - - let feeViewModelFactory: BalanceViewModelFactoryProtocol? - - if - let utilityAsset = chainAsset.chain.utilityAssets().first, - utilityAsset.assetId != chainAsset.asset.assetId { - feeViewModelFactory = BalanceViewModelFactory( - targetAssetInfo: utilityAsset.displayInfo(with: chainAsset.chain.icon), - priceAssetInfoFactory: priceAssetInfoFactory - ) - } else { - feeViewModelFactory = nil - } + let balanceViewModelFactoryFacade = BalanceViewModelFactoryFacade(priceAssetInfoFactory: priceAssetInfoFactory) let viewModelFactory = OperationDetailsViewModelFactory( - balanceViewModelFactory: balanceViewModelFactory, - feeViewModelFactory: feeViewModelFactory + balanceViewModelFactoryFacade: balanceViewModelFactoryFacade ) let presenter = OperationDetailsPresenter( @@ -93,4 +88,40 @@ struct OperationDetailsViewFactory { return view } + + static func createSwapInteractor( + transaction: TransactionHistoryItem, + chainAsset: ChainAsset, + transactionLocalSubscriptionFactory: TransactionLocalSubscriptionFactoryProtocol, + currencyManager: CurrencyManagerProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + operationDataProvider: OperationDetailsDataProviderProtocol + ) -> OperationDetailsBaseInteractor { + OperationSwapDetailsInteractor( + transaction: transaction, + chainAsset: chainAsset, + transactionLocalSubscriptionFactory: transactionLocalSubscriptionFactory, + currencyManager: currencyManager, + priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, + operationDataProvider: operationDataProvider + ) + } + + static func createInteractor( + transaction: TransactionHistoryItem, + chainAsset: ChainAsset, + transactionLocalSubscriptionFactory: TransactionLocalSubscriptionFactoryProtocol, + currencyManager: CurrencyManagerProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + operationDataProvider: OperationDetailsDataProviderProtocol + ) -> OperationDetailsBaseInteractor { + OperationDetailsInteractor( + transaction: transaction, + chainAsset: chainAsset, + transactionLocalSubscriptionFactory: transactionLocalSubscriptionFactory, + currencyManager: currencyManager, + priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, + operationDataProvider: operationDataProvider + ) + } } diff --git a/novawallet/Modules/OperationDetails/OperationDetailsViewLayout.swift b/novawallet/Modules/OperationDetails/OperationDetailsViewLayout.swift index edc504326a..a534f8d9df 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsViewLayout.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsViewLayout.swift @@ -23,9 +23,9 @@ final class OperationDetailsViewLayout: UIView { }() let containerView: ScrollableContainerView = { - let view = ScrollableContainerView(axis: .vertical, respectsSafeArea: false) + let view = ScrollableContainerView(axis: .vertical, respectsSafeArea: true) view.stackView.alignment = .center - view.stackView.layoutMargins = UIEdgeInsets(top: 10.0, left: 0.0, bottom: 0.0, right: 0.0) + view.stackView.layoutMargins = UIEdgeInsets(top: 0, left: 0.0, bottom: 0.0, right: 0.0) view.stackView.isLayoutMarginsRelativeArrangement = true return view }() diff --git a/novawallet/Modules/OperationDetails/OperationSwapDetailsInteractor.swift b/novawallet/Modules/OperationDetails/OperationSwapDetailsInteractor.swift new file mode 100644 index 0000000000..d4ed8e13e0 --- /dev/null +++ b/novawallet/Modules/OperationDetails/OperationSwapDetailsInteractor.swift @@ -0,0 +1,23 @@ +import Foundation +import BigInt +import RobinHood + +final class OperationSwapDetailsInteractor: OperationDetailsBaseInteractor { + override func setupPriceHistorySubscription() { + guard let swap = transaction.swap else { + return + } + let priceAssetIn = chain.asset(byHistoryAssetId: swap.assetIdIn)?.priceId + let priceAssetOut = chain.asset(byHistoryAssetId: swap.assetIdOut)?.priceId + let feeAsset = chain.asset(byHistoryAssetId: transaction.feeAssetId) ?? chain.utilityAsset() + let feePriceId = feeAsset?.priceId + let prices = [ + priceAssetIn, + priceAssetOut, + feePriceId + ].compactMap { $0 } + Set(prices).forEach { + priceProviders[$0] = subscribeToPriceHistory(for: $0, currency: selectedCurrency) + } + } +} diff --git a/novawallet/Modules/OperationDetails/View/OperationDetailsSwapView.swift b/novawallet/Modules/OperationDetails/View/OperationDetailsSwapView.swift index 88f58257c4..014f014050 100644 --- a/novawallet/Modules/OperationDetails/View/OperationDetailsSwapView.swift +++ b/novawallet/Modules/OperationDetails/View/OperationDetailsSwapView.swift @@ -1,6 +1,6 @@ import UIKit -final class OperationDetailsSwapView: ScrollableContainerLayoutView, LocalizableViewProtocol { +final class OperationDetailsSwapView: LocalizableView { let senderTableView = StackTableView() let pairsView = SwapPairView() @@ -39,6 +39,18 @@ final class OperationDetailsSwapView: ScrollableContainerLayoutView, Localizable } } + override init(frame: CGRect) { + super.init(frame: frame) + + setupStyle() + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + func bind(viewModel: OperationSwapViewModel) { pairsView.leftAssetView.bind(viewModel: viewModel.assetIn) pairsView.rigthAssetView.bind(viewModel: viewModel.assetOut) @@ -56,6 +68,14 @@ final class OperationDetailsSwapView: ScrollableContainerLayoutView, Localizable imageViewModel: viewModel.wallet.addressIcon )) transactionHashView.bind(details: viewModel.transactionHash) + + if viewModel.isOutgoing { + pairsView.leftAssetView.valueLabel.textColor = R.color.colorTextPositive() + pairsView.rigthAssetView.valueLabel.textColor = R.color.colorTextPrimary() + } else { + pairsView.leftAssetView.valueLabel.textColor = R.color.colorTextPrimary() + pairsView.rigthAssetView.valueLabel.textColor = R.color.colorTextPositive() + } } private func setup(locale: Locale) { @@ -75,30 +95,37 @@ final class OperationDetailsSwapView: ScrollableContainerLayoutView, Localizable ) } - override func setupStyle() { + func setupStyle() { backgroundColor = .clear } - override func setupLayout() { - super.setupLayout() + func setupLayout() { + addSubview(pairsView) + addSubview(detailsTableView) + addSubview(walletTableView) + addSubview(transactionTableView) - stackView.layoutMargins = UIEdgeInsets(top: 12, left: 16, bottom: 0, right: 16) - addArrangedSubview(pairsView, spacingAfter: 8) - addArrangedSubview(detailsTableView, spacingAfter: 8) - addArrangedSubview(walletTableView, spacingAfter: 8) - addArrangedSubview(transactionTableView) + pairsView.snp.makeConstraints { + $0.leading.trailing.top.equalToSuperview() + } + detailsTableView.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + $0.top.equalTo(pairsView.snp.bottom).offset(8) + } + walletTableView.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + $0.top.equalTo(detailsTableView.snp.bottom).offset(8) + } + transactionTableView.snp.makeConstraints { + $0.leading.trailing.bottom.equalToSuperview() + $0.top.equalTo(walletTableView.snp.bottom).offset(8) + $0.bottom.equalToSuperview().offset(24) + } detailsTableView.addArrangedSubview(rateCell) detailsTableView.addArrangedSubview(networkFeeCell) walletTableView.addArrangedSubview(walletCell) walletTableView.addArrangedSubview(accountCell) transactionTableView.addArrangedSubview(transactionHashView) - - addSubview(actionButton) - actionButton.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) - make.bottom.equalTo(safeAreaLayoutGuide).inset(UIConstants.actionBottomInset) - make.height.equalTo(UIConstants.actionHeight) - } } } diff --git a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift index 4a7a2f9397..81e553e413 100644 --- a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift +++ b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift @@ -11,30 +11,28 @@ protocol OperationDetailsViewModelFactoryProtocol { } final class OperationDetailsViewModelFactory { - let balanceViewModelFactory: BalanceViewModelFactoryProtocol - let feeViewModelFactory: BalanceViewModelFactoryProtocol? let dateFormatter: LocalizableResource let networkViewModelFactory: NetworkViewModelFactoryProtocol let displayAddressViewModelFactory: DisplayAddressViewModelFactoryProtocol let quantityFormatter: LocalizableResource lazy var poolIconFactory: NominationPoolsIconFactoryProtocol = NominationPoolsIconFactory() + lazy var walletViewModelFactory = WalletAccountViewModelFactory() + let balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol init( - balanceViewModelFactory: BalanceViewModelFactoryProtocol, - feeViewModelFactory: BalanceViewModelFactoryProtocol?, dateFormatter: LocalizableResource = DateFormatter.txDetails, networkViewModelFactory: NetworkViewModelFactoryProtocol = NetworkViewModelFactory(), displayAddressViewModelFactory: DisplayAddressViewModelFactoryProtocol = DisplayAddressViewModelFactory(), quantityFormatter: LocalizableResource = - NumberFormatter.quantity.localizableResource() + NumberFormatter.quantity.localizableResource(), + balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol ) { - self.balanceViewModelFactory = balanceViewModelFactory - self.feeViewModelFactory = feeViewModelFactory self.dateFormatter = dateFormatter self.networkViewModelFactory = networkViewModelFactory self.displayAddressViewModelFactory = displayAddressViewModelFactory self.quantityFormatter = quantityFormatter + self.balanceViewModelFactoryFacade = balanceViewModelFactoryFacade } private func createIconViewModel( @@ -57,6 +55,9 @@ final class OperationDetailsViewModelFactory { } else { return nil } + case .swap: + let image = R.image.iconActionSwap()! + return StaticImageViewModel(image: image) } } @@ -68,6 +69,7 @@ final class OperationDetailsViewModelFactory { let amount: BigUInt let priceData: PriceData? let prefix: String + var precision = assetInfo.assetPrecision switch model { case let .transfer(model): @@ -98,17 +100,29 @@ final class OperationDetailsViewModelFactory { amount = model.amount priceData = model.priceData prefix = "-" + case let .swap(model): + if model.isOutgoing { + amount = model.amountIn + priceData = model.priceIn + prefix = "-" + precision = model.assetIn.displayInfo.assetPrecision + } else { + amount = model.amountOut + priceData = model.priceOut + prefix = "+" + precision = model.assetOut.displayInfo.assetPrecision + } } return Decimal.fromSubstrateAmount( amount, - precision: assetInfo.assetPrecision + precision: precision ).map { amountDecimal in - let amountViewModel = balanceViewModelFactory.balanceFromPrice( - amountDecimal, + let amountViewModel = balanceViewModelFactoryFacade.balanceFromPrice( + targetAssetInfo: assetInfo, + amount: amountDecimal, priceData: priceData ).value(for: locale) - return BalanceViewModel(amount: prefix + amountViewModel.amount, price: amountViewModel.price) } } @@ -136,9 +150,9 @@ final class OperationDetailsViewModelFactory { model.fee, precision: feeAssetInfo.assetPrecision ).map { amount in - let viewModelFactory = feeViewModelFactory ?? balanceViewModelFactory - return viewModelFactory.balanceFromPrice( - amount, + balanceViewModelFactoryFacade.balanceFromPrice( + targetAssetInfo: feeAssetInfo, + amount: amount, priceData: model.feePriceData ).value(for: locale) } @@ -211,6 +225,111 @@ final class OperationDetailsViewModelFactory { return OperationPoolRewardOrSlashViewModel(eventId: model.eventId, pool: poolViewModel) } + private func createSwapViewModel( + from model: OperationSwapModel, + chainAsset _: ChainAsset, + locale: Locale + ) -> OperationSwapViewModel { + let assetInViewModel = assetViewModel( + chain: model.chain, + asset: model.assetIn, + amount: model.amountIn, + priceData: model.priceIn, + locale: locale + ) + let assetOutViewModel = assetViewModel( + chain: model.chain, + asset: model.assetOut, + amount: model.amountOut, + priceData: model.priceOut, + locale: locale + ) + let rateViewModel = rateViewModel( + from: .init( + assetDisplayInfoIn: model.assetIn.displayInfo, + assetDisplayInfoOut: model.assetOut.displayInfo, + amountIn: model.amountIn, + amountOut: model.amountOut + ), + locale: locale + ) + let feeAmountDecimal = Decimal.fromSubstrateAmount( + model.fee, + precision: model.feeAsset.displayInfo.assetPrecision + ) ?? 0 + let feeBalanceViewModel = balanceViewModelFactoryFacade.balanceFromPrice( + targetAssetInfo: model.feeAsset.displayInfo, + amount: feeAmountDecimal, + priceData: model.feePrice + ).value(for: locale) + let walletViewModel = try? walletViewModelFactory.createViewModel(from: model.wallet) + + return OperationSwapViewModel( + isOutgoing: model.isOutgoing, + assetIn: assetInViewModel, + assetOut: assetOutViewModel, + rate: rateViewModel, + fee: feeBalanceViewModel, + wallet: walletViewModel ?? .init(walletName: nil, walletIcon: nil, address: "", addressIcon: nil), + transactionHash: model.txHash + ) + } + + private func assetViewModel( + chain: ChainModel, + asset: AssetModel, + amount: BigUInt, + priceData: PriceData?, + locale: Locale + ) -> SwapAssetAmountViewModel { + let networkViewModel = networkViewModelFactory.createViewModel(from: chain) + let assetIcon: ImageViewModelProtocol = asset.icon.map { RemoteImageViewModel(url: $0) } ?? + StaticImageViewModel(image: R.image.iconDefaultToken()!) + let amountDecimal = Decimal.fromSubstrateAmount( + amount, + precision: asset.displayInfo.assetPrecision + ) ?? 0 + let balanceViewModel = balanceViewModelFactoryFacade.balanceFromPrice( + targetAssetInfo: asset.displayInfo, + amount: amountDecimal, + priceData: priceData + ).value(for: locale) + + return .init( + imageViewModel: assetIcon, + hub: networkViewModel, + balance: balanceViewModel + ) + } + + func rateViewModel(from params: RateParams, locale: Locale) -> String { + guard + let amountOutDecimal = Decimal.fromSubstrateAmount( + params.amountOut, + precision: params.assetDisplayInfoOut.assetPrecision + ), + let amountInDecimal = Decimal.fromSubstrateAmount( + params.amountIn, + precision: params.assetDisplayInfoIn.assetPrecision + ), + amountInDecimal != 0 else { + return "" + } + + let difference = amountOutDecimal / amountInDecimal + + let amountIn = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: params.assetDisplayInfoIn, + value: 1 + ).value(for: locale) + let amountOut = balanceViewModelFactoryFacade.amountFromValue( + targetAssetInfo: params.assetDisplayInfoOut, + value: difference + ).value(for: locale) + + return "\(amountIn) = \(amountOut)" + } + private func createContentViewModel( from data: OperationDetailsModel.OperationData, chainAsset: ChainAsset, @@ -260,6 +379,9 @@ final class OperationDetailsViewModelFactory { locale: locale ) return .poolSlash(viewModel) + case let .swap(model): + let viewModel = createSwapViewModel(from: model, chainAsset: chainAsset, locale: locale) + return .swap(viewModel) } } } diff --git a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift index 5d61372815..e3ecef1019 100644 --- a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift +++ b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift @@ -27,7 +27,6 @@ final class TransactionHistoryViewModelFactory { let iconGenerator = PolkadotIconGenerator() let calendar = Calendar.current let dateFormatter: LocalizableResource - var chainFormat: ChainFormat { chainAsset.chain.chainFormat } init( @@ -98,6 +97,48 @@ final class TransactionHistoryViewModelFactory { ) } + private func createSwapItemFromData( + _ data: TransactionHistoryItem, + priceCalculator: TokenPriceCalculatorProtocol?, + locale: Locale, + txType: TransactionType + ) -> TransactionItemViewModel { + let assetIn = chainAsset.chain.asset(byHistoryAssetId: data.swap?.assetIdIn) ?? chainAsset.chain.utilityAsset() + let assetOut = chainAsset.chain.asset(byHistoryAssetId: data.swap?.assetIdOut) ?? chainAsset.chain.utilityAsset() + let isOutgoing = assetIn?.assetId == chainAsset.asset.assetId + let optAmountInPlank = isOutgoing ? data.swap?.amountIn : data.swap?.amountOut + let amountInPlank = optAmountInPlank.map { BigUInt($0) ?? 0 } ?? 0 + let precision = (isOutgoing ? assetIn?.precision : assetOut?.precision) ?? 0 + let amount = Decimal.fromSubstrateAmount( + amountInPlank, + precision: Int16(precision) + ) ?? .zero + let time = dateFormatter.value(for: locale) + .string(from: Date(timeIntervalSince1970: TimeInterval(data.timestamp))) + let balance = createBalance( + from: amount, + priceCalculator: priceCalculator, + timestamp: data.timestamp, + locale: locale + ) + let icon = R.image.iconActionSwap() + let imageViewModel = icon.map { StaticImageViewModel(image: $0) } + let amountDetails = amountDetails(price: balance.price, time: time, locale: locale) + let subtitle = [assetIn?.symbol, assetOut?.symbol].compactMap { $0 }.joined(separator: " → ") + + return .init( + identifier: data.identifier, + timestamp: data.timestamp, + title: R.string.localizable.commonSwap(preferredLanguages: locale.rLanguages), + subtitle: subtitle, + amount: balance.amount, + amountDetails: amountDetails, + type: txType, + status: data.status, + imageViewModel: imageViewModel + ) + } + private func createTransferItemTitleWithSubtitle( data: TransactionHistoryItem, address: AccountAddress, @@ -330,6 +371,13 @@ extension TransactionHistoryViewModelFactory: TransactionHistoryViewModelFactory locale: locale, txType: transactionType ) + case .swap: + return createSwapItemFromData( + data, + priceCalculator: priceCalculator, + locale: locale, + txType: transactionType + ) case .reward, .slash: return createRewardOrSlashItemFromData( data, @@ -366,6 +414,8 @@ extension TransactionHistoryItem { return .poolReward case .poolSlash: return .poolSlash + case .swap: + return .swap default: if callPath.isSubstrateOrEvmTransfer { return sender == address ? .outgoing : .incoming diff --git a/novawallet/Modules/TransactionHistory/Service/WalletRemoteHistoryProtocols.swift b/novawallet/Modules/TransactionHistory/Service/WalletRemoteHistoryProtocols.swift index 459bbd804b..45cb4dc799 100644 --- a/novawallet/Modules/TransactionHistory/Service/WalletRemoteHistoryProtocols.swift +++ b/novawallet/Modules/TransactionHistory/Service/WalletRemoteHistoryProtocols.swift @@ -7,6 +7,7 @@ enum WalletRemoteHistorySourceLabel: Int, CaseIterable { case rewards case extrinsics case poolRewards + case swaps } protocol WalletRemoteHistoryItemProtocol { diff --git a/novawallet/Modules/TransactionHistory/View/HistoryItemTableViewCell.swift b/novawallet/Modules/TransactionHistory/View/HistoryItemTableViewCell.swift index ce1190be0b..e2a736682f 100644 --- a/novawallet/Modules/TransactionHistory/View/HistoryItemTableViewCell.swift +++ b/novawallet/Modules/TransactionHistory/View/HistoryItemTableViewCell.swift @@ -169,7 +169,7 @@ extension HistoryItemTableViewCell { amountDetailsLabel.text = transactionModel.amountDetails switch transactionModel.type { - case .incoming, .reward, .poolReward: + case .incoming, .reward, .poolReward, .swap: amountLabel.text = "+ \(transactionModel.amount)" amountLabel.textColor = R.color.colorTextPositive()! case .outgoing, .slash, .poolSlash, .extrinsic: @@ -186,7 +186,7 @@ extension HistoryItemTableViewCell { .offset(-Constants.titleSpacingForTransfer) } - case .slash, .reward, .poolReward, .poolSlash: + case .slash, .reward, .poolReward, .poolSlash, .swap: subtitleLabel.lineBreakMode = .byTruncatingTail subtitleLabel.snp.updateConstraints { make in From ec0668ab905c11144b2cf8679742a0e4c8dae8db Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 9 Nov 2023 02:02:58 +0300 Subject: [PATCH 137/204] fix icons --- .../iconSwap.imageset/Contents.json | 12 ++++++++++++ .../iconSwap.imageset/flip-swap.pdf | Bin 0 -> 2338 bytes .../TransactionHistory/CalculatorFactory.swift | 4 ++-- .../Common/Substrate/Types/CallCodingPath.swift | 2 +- .../OperationDetailsContractProvider.swift | 4 ++-- .../OperationDetailsDirectStakingProvider.swift | 2 +- .../OperationDetailsExtrinsicProvider.swift | 2 +- .../OperationDetailsPoolStakingProvider.swift | 2 +- .../OperationDetailsSwapProvider.swift | 2 +- .../OperationDetailsTransferProvider.swift | 4 ++-- .../OperationDetailsPresenter.swift | 5 +++-- .../OperationDetailsViewModelFactory.swift | 5 ++--- .../ViewModel/WalletHistoryFilterViewModel.swift | 7 +++++++ .../TransactionHistoryViewModelFactory.swift | 2 +- 14 files changed, 36 insertions(+), 17 deletions(-) create mode 100644 novawallet/Assets.xcassets/iconSwap.imageset/Contents.json create mode 100644 novawallet/Assets.xcassets/iconSwap.imageset/flip-swap.pdf diff --git a/novawallet/Assets.xcassets/iconSwap.imageset/Contents.json b/novawallet/Assets.xcassets/iconSwap.imageset/Contents.json new file mode 100644 index 0000000000..e263582bda --- /dev/null +++ b/novawallet/Assets.xcassets/iconSwap.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "flip-swap.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconSwap.imageset/flip-swap.pdf b/novawallet/Assets.xcassets/iconSwap.imageset/flip-swap.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6bf538326dceb9c1ef3264df1a1d37ac8f1eadfe GIT binary patch literal 2338 zcmbVOO>Y}F5WVlO;Ke|4$ci)MkV7Ca(AZ8;v_)OHx1a}A-Z(03Z7XdGx4%AbNPRgj zkf3{zsJHX^W`?7)>&v&VsLx$T&b#sVKRV}Lzjm{$$LZb8w3vqWi|f~N;d~$1<8kNb zkbL&cuIO7N&(BSNSl^n%j-Pn`aai9?A6)=AH>vHk85R%S>~i>fwH)T}-nfgK(_hPN z_g80r@3tNF78|1T`@^fl>Es@7EsQVf?Ia6&oRPS#VBxg_rOAmCB=%xR5iVDDg{W80tmLe zPf5*IW<*}9Eo6d-fGKN?rP?ygt0V3`i9`>OdR8w%TIk>X>? zhImgo1L2Yqj6si)r6mB2qGn%hCAfes<2h>K0#H&E@y;M5KoL+;&LzPb2}d6s!A6eC z+{|$`2QWw~Io2fLqlh>HHA1Xh!Ki^j4!}f3C4&?T7~C5aS)`x}c%@XOBB^G96O=}M zNnGuL%@AM@Ie`?DG|xIm@v z8Uf`*2;-|GKo(~KOCUJJtl$zl+M{%eY6~iotmOd}Uj(cJLdRel37Pp2#Z)6nfYh=x zP=(OOW-oBCIRGeVqm%(6?WC}04OI%p1QkYWfha(TK%iRZCgNSQm%$?eM7waQZEtmt zl5Id(bL%aFZA$1vfSw9sw5~v@p$=q6vg00?Q#4Ot4B5sHM|Lgk=x3Kt%nDp$5Dr&SJ|mZXpar#L#o zMEjtivT-Wbf(9xkvf0DYMiU{!eemLN;RV9`BZ%3h&?`k;2~W|8DN3B`6v0qv^CUzi zR=Sc%F1TZmofzAVn~YMmAw#8nbB}op3B|0kc7RR9j8SfuI7{J%l^D=Uh^Tl)Jhnv4bC#%z-in!cS zIeQMa*kcU$(6m{s?uSih|6;zs1RXE7xDq_OS^Ya8`ubmE Rv6*T*SO%#(JG=V%?PpdK TokenPriceCalculatorProtocol? + func createPriceCalculator(for priceId: String?) -> TokenPriceCalculatorProtocol? } final class CalculatorFactory: CalculatorFactoryProtocol { var priceHistory: [AssetModel.PriceId: PriceHistory?] = [:] - func createPriceProvider(for priceId: String?) -> TokenPriceCalculatorProtocol? { + func createPriceCalculator(for priceId: String?) -> TokenPriceCalculatorProtocol? { guard let priceId = priceId, let priceHistory = priceHistory[priceId], let history = priceHistory else { diff --git a/novawallet/Common/Substrate/Types/CallCodingPath.swift b/novawallet/Common/Substrate/Types/CallCodingPath.swift index 068f93a8c7..c52b442505 100644 --- a/novawallet/Common/Substrate/Types/CallCodingPath.swift +++ b/novawallet/Common/Substrate/Types/CallCodingPath.swift @@ -128,7 +128,7 @@ extension CallCodingPath { } var isSwap: Bool { - [.swap].contains(self) + self == .swap } } diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsContractProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsContractProvider.swift index df18817637..6423269b8b 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsContractProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsContractProvider.swift @@ -9,8 +9,8 @@ extension OperationDetailsContractProvider: OperationDetailsDataProviderProtocol calculatorFactory: CalculatorFactoryProtocol, progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void ) { - let priceCalculator = calculatorFactory.createPriceProvider(for: chainAsset.asset.priceId) - let feePriceCalculator = calculatorFactory.createPriceProvider(for: chainAsset.chain.utilityAsset()?.priceId) + let priceCalculator = calculatorFactory.createPriceCalculator(for: chainAsset.asset.priceId) + let feePriceCalculator = calculatorFactory.createPriceCalculator(for: chainAsset.chain.utilityAsset()?.priceId) let fee: BigUInt = newFee ?? transaction.feeInPlankIntOrZero let feePriceData = feePriceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDirectStakingProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDirectStakingProvider.swift index b6ac61b08e..afabbc7b74 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDirectStakingProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDirectStakingProvider.swift @@ -59,7 +59,7 @@ extension OperationDetailsDirectStakingProvider: OperationDetailsDataProviderPro try JSONDecoder().decode(HistoryRewardContext.self, from: $0) } - let priceCalculator = calculatorFactory.createPriceProvider(for: chainAsset.asset.priceId) + let priceCalculator = calculatorFactory.createPriceCalculator(for: chainAsset.asset.priceId) let eventId = getEventId(from: context) ?? transaction.txHash let amount = transaction.amountInPlankIntOrZero diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsExtrinsicProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsExtrinsicProvider.swift index edf8f1cd9a..5d37ba0ade 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsExtrinsicProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsExtrinsicProvider.swift @@ -14,7 +14,7 @@ extension OperationDetailsExtrinsicProvider: OperationDetailsDataProviderProtoco return } - let feePriceCalculator = calculatorFactory.createPriceProvider(for: chain.utilityAsset()?.priceId) + let feePriceCalculator = calculatorFactory.createPriceCalculator(for: chain.utilityAsset()?.priceId) let fee = newFee ?? transaction.feeInPlankIntOrZero let feePriceData = feePriceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { PriceData.amount($0) diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsPoolStakingProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsPoolStakingProvider.swift index 3d63a36405..040471af22 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsPoolStakingProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsPoolStakingProvider.swift @@ -140,7 +140,7 @@ extension OperationDetailsPoolStakingProvider: OperationDetailsDataProviderProto try JSONDecoder().decode(HistoryPoolRewardContext.self, from: $0) } - let priceCalculator = calculatorFactory.createPriceProvider(for: chainAsset.asset.priceId) + let priceCalculator = calculatorFactory.createPriceCalculator(for: chainAsset.asset.priceId) let eventId = getEventId(from: optContext) ?? transaction.txHash let amount = transaction.amountInPlankIntOrZero let priceData = priceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift index 755b638606..7e7205b5d1 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift @@ -85,7 +85,7 @@ extension OperationDetailsSwapProvider: OperationDetailsDataProviderProtocol { guard let priceId = assetModel?.priceId else { return nil } - let provider = calculatorFactory.createPriceProvider(for: priceId) + let provider = calculatorFactory.createPriceCalculator(for: priceId) return provider?.calculatePrice(for: timestamp).map { PriceData.amount($0) } diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsTransferProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsTransferProvider.swift index 262d02640e..f71437e8ed 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsTransferProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsTransferProvider.swift @@ -34,8 +34,8 @@ extension OperationDetailsTransferProvider: OperationDetailsDataProviderProtocol progressClosure(nil) return } - let priceCalculator = calculatorFactory.createPriceProvider(for: chainAsset.asset.priceId) - let feePriceCalculator = calculatorFactory.createPriceProvider(for: chainAsset.chain.utilityAsset()?.priceId) + let priceCalculator = calculatorFactory.createPriceCalculator(for: chainAsset.asset.priceId) + let feePriceCalculator = calculatorFactory.createPriceCalculator(for: chainAsset.chain.utilityAsset()?.priceId) let peerAddress = (transaction.sender == accountAddress ? transaction.receiver : transaction.sender) ?? transaction.sender let accountId = try? peerAddress.toAccountId(using: chain.chainFormat) diff --git a/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift b/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift index 555bbca7d9..3220f36e80 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift @@ -137,8 +137,9 @@ extension OperationDetailsPresenter: OperationDetailsPresenterProtocol { presentTransactionHashOptions(contractModel.txHash) case let .poolReward(poolRewardOrSlashModel), let .poolSlash(poolRewardOrSlashModel): presentEventIdOptions(poolRewardOrSlashModel.eventId) - // TODO: repeat swap action - case .none, .swap: + case let .swap(swapModel): + presentTransactionHashOptions(swapModel.txHash) + case .none: break } } diff --git a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift index 81e553e413..7375db2d7f 100644 --- a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift +++ b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift @@ -56,7 +56,7 @@ final class OperationDetailsViewModelFactory { return nil } case .swap: - let image = R.image.iconActionSwap()! + let image = R.image.iconSwap()! return StaticImageViewModel(image: image) } } @@ -227,7 +227,6 @@ final class OperationDetailsViewModelFactory { private func createSwapViewModel( from model: OperationSwapModel, - chainAsset _: ChainAsset, locale: Locale ) -> OperationSwapViewModel { let assetInViewModel = assetViewModel( @@ -380,7 +379,7 @@ final class OperationDetailsViewModelFactory { ) return .poolSlash(viewModel) case let .swap(model): - let viewModel = createSwapViewModel(from: model, chainAsset: chainAsset, locale: locale) + let viewModel = createSwapViewModel(from: model, locale: locale) return .swap(viewModel) } } diff --git a/novawallet/Modules/TransactionHistory/HistoryFilter/ViewModel/WalletHistoryFilterViewModel.swift b/novawallet/Modules/TransactionHistory/HistoryFilter/ViewModel/WalletHistoryFilterViewModel.swift index 6df45bf7f7..d78214a508 100644 --- a/novawallet/Modules/TransactionHistory/HistoryFilter/ViewModel/WalletHistoryFilterViewModel.swift +++ b/novawallet/Modules/TransactionHistory/HistoryFilter/ViewModel/WalletHistoryFilterViewModel.swift @@ -3,6 +3,7 @@ import SoraFoundation enum WalletHistoryFilterRow: Int, CaseIterable { case transfers + case swaps case rewardsAndSlashes case extrinsics @@ -21,6 +22,10 @@ enum WalletHistoryFilterRow: Int, CaseIterable { return LocalizableResource { locale in R.string.localizable.walletFiltersExtrinsics(preferredLanguages: locale.rLanguages) } + case .swaps: + return LocalizableResource { locale in + R.string.localizable.commonSwap(preferredLanguages: locale.rLanguages) + } } } @@ -32,6 +37,8 @@ enum WalletHistoryFilterRow: Int, CaseIterable { return .rewardsAndSlashes case .extrinsics: return .extrinsics + case .swaps: + return .swaps } } } diff --git a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift index e3ecef1019..d488b60064 100644 --- a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift +++ b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift @@ -121,7 +121,7 @@ final class TransactionHistoryViewModelFactory { timestamp: data.timestamp, locale: locale ) - let icon = R.image.iconActionSwap() + let icon = R.image.iconSwap() let imageViewModel = icon.map { StaticImageViewModel(image: $0) } let amountDetails = amountDetails(price: balance.price, time: time, locale: locale) let subtitle = [assetIn?.symbol, assetOut?.symbol].compactMap { $0 }.joined(separator: " → ") From 7d848a72e3e16a325a1c91e594cdecebe52b6610 Mon Sep 17 00:00:00 2001 From: ERussel Date: Thu, 9 Nov 2023 06:42:40 +0100 Subject: [PATCH 138/204] add notification to confirm screen --- novawallet.xcodeproj/project.pbxproj | 4 + novawallet/Common/View/InlineAlertView.swift | 19 ++++ .../ScrollableContainerLayoutView.swift | 47 ++++++++-- .../Base/Model/SwapBaseViewModelFactory.swift | 60 ++++++++++++- .../Model/SwapPriceDifferenceConfig.swift | 15 ++++ .../Swaps/Base/SwapBasePresenter.swift | 2 +- .../Model/SwapConfirmViewModelFactory.swift | 88 +++++++------------ .../Swaps/Confirm/SwapConfirmPresenter.swift | 42 ++++++--- .../Swaps/Confirm/SwapConfirmProtocols.swift | 1 + .../Confirm/SwapConfirmViewController.swift | 4 + .../Confirm/SwapConfirmViewFactory.swift | 6 +- .../Confirm/View/SwapConfirmViewLayout.swift | 16 +++- .../Model/SwapsSetupViewModelFactory.swift | 17 ++-- .../Swaps/Setup/SwapSetupPresenter.swift | 3 +- .../Swaps/Setup/SwapSetupViewFactory.swift | 2 +- 15 files changed, 233 insertions(+), 93 deletions(-) create mode 100644 novawallet/Modules/Swaps/Base/Model/SwapPriceDifferenceConfig.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 8c2c0417ac..ea3e73eb41 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -216,6 +216,7 @@ 0C9951CA2AE2ABA400B65615 /* AssetListBannerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9951C92AE2ABA400B65615 /* AssetListBannerCell.swift */; }; 0C9951CF2AE2BAE500B65615 /* PromotionBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9951CE2AE2BAE500B65615 /* PromotionBannerView.swift */; }; 0C9951D32AE2DB0200B65615 /* PromotionViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9951D22AE2DB0200B65615 /* PromotionViewModelFactory.swift */; }; + 0C9A7F992AFC9A2B00938CD0 /* SwapPriceDifferenceConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9A7F982AFC9A2B00938CD0 /* SwapPriceDifferenceConfig.swift */; }; 0C9C642D2A8CE30A004DC078 /* SystemAccountValidating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C642C2A8CE30A004DC078 /* SystemAccountValidating.swift */; }; 0C9C64302A8D6779004DC078 /* StakingNPoolsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C642F2A8D6779004DC078 /* StakingNPoolsPresenter.swift */; }; 0C9C64322A8D67A0004DC078 /* StakingNPoolsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C64312A8D67A0004DC078 /* StakingNPoolsInteractor.swift */; }; @@ -4285,6 +4286,7 @@ 0C9951C92AE2ABA400B65615 /* AssetListBannerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListBannerCell.swift; sourceTree = ""; }; 0C9951CE2AE2BAE500B65615 /* PromotionBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionBannerView.swift; sourceTree = ""; }; 0C9951D22AE2DB0200B65615 /* PromotionViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionViewModelFactory.swift; sourceTree = ""; }; + 0C9A7F982AFC9A2B00938CD0 /* SwapPriceDifferenceConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPriceDifferenceConfig.swift; sourceTree = ""; }; 0C9C642C2A8CE30A004DC078 /* SystemAccountValidating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemAccountValidating.swift; sourceTree = ""; }; 0C9C642F2A8D6779004DC078 /* StakingNPoolsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingNPoolsPresenter.swift; sourceTree = ""; }; 0C9C64312A8D67A0004DC078 /* StakingNPoolsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingNPoolsInteractor.swift; sourceTree = ""; }; @@ -8480,6 +8482,7 @@ 0C13DFCC2AF8A5A300E5F355 /* SwapMaxModel.swift */, 0C13DFDA2AFA691400E5F355 /* SlippageConfig.swift */, 0C13DFE02AFBBAF600E5F355 /* SwapBaseViewModelFactory.swift */, + 0C9A7F982AFC9A2B00938CD0 /* SwapPriceDifferenceConfig.swift */, ); path = Model; sourceTree = ""; @@ -19741,6 +19744,7 @@ 84350ACE2845709D0031EF24 /* IdentityAccountView.swift in Sources */, 8489A6D027FD5B9E0040C066 /* StackActionView.swift in Sources */, 778D979F2A24D248002BA681 /* SearchViewProtocol.swift in Sources */, + 0C9A7F992AFC9A2B00938CD0 /* SwapPriceDifferenceConfig.swift in Sources */, 8460E713284ABE22002896E9 /* UnstakeCallWrapper.swift in Sources */, 843612C3278FF0C700DC739E /* FeeRetryable.swift in Sources */, 8849AD6029C3526700F4F7FF /* Caip2.swift in Sources */, diff --git a/novawallet/Common/View/InlineAlertView.swift b/novawallet/Common/View/InlineAlertView.swift index 334e0de41c..31397767c6 100644 --- a/novawallet/Common/View/InlineAlertView.swift +++ b/novawallet/Common/View/InlineAlertView.swift @@ -71,4 +71,23 @@ extension InlineAlertView { view.contentView.stackView.alignment = .top return view } + + static func inline(for style: InlineAlertView.Style) -> InlineAlertView { + switch style { + case .error: + return InlineAlertView.error() + case .warning: + return InlineAlertView.warning() + case .info: + return InlineAlertView.info() + } + } +} + +extension InlineAlertView { + enum Style { + case error + case warning + case info + } } diff --git a/novawallet/Common/View/ScrollableContainerView/ScrollableContainerLayoutView.swift b/novawallet/Common/View/ScrollableContainerView/ScrollableContainerLayoutView.swift index b77cb3a26b..48d135b540 100644 --- a/novawallet/Common/View/ScrollableContainerView/ScrollableContainerLayoutView.swift +++ b/novawallet/Common/View/ScrollableContainerView/ScrollableContainerLayoutView.swift @@ -58,15 +58,16 @@ class ScrollableContainerLayoutView: UIView { } } - func applyWarning( - on warningView: inout InlineAlertView?, + func applyInline( + on inlineView: inout InlineAlertView?, + style: InlineAlertView.Style, after view: UIView?, text: String?, spacing: CGFloat = 0 ) { if let text = text { - if warningView == nil { - let newView = InlineAlertView.warning() + if inlineView == nil { + let newView = InlineAlertView.inline(for: style) if let afterView = view { insertArrangedSubview(newView, after: afterView, spacingAfter: spacing) @@ -74,17 +75,47 @@ class ScrollableContainerLayoutView: UIView { addArrangedSubview(newView, spacingAfter: spacing) } - warningView = newView + inlineView = newView } - warningView?.contentView.detailsLabel.text = text + inlineView?.contentView.detailsLabel.text = text } else { - warningView?.removeFromSuperview() - warningView = nil + inlineView?.removeFromSuperview() + inlineView = nil } setNeedsLayout() } + + func applyWarning( + on warningView: inout InlineAlertView?, + after view: UIView?, + text: String?, + spacing: CGFloat = 0 + ) { + applyInline( + on: &warningView, + style: .warning, + after: view, + text: text, + spacing: spacing + ) + } + + func applyInfo( + on infoView: inout InlineAlertView?, + after view: UIView?, + text: String?, + spacing: CGFloat = 0 + ) { + applyInline( + on: &infoView, + style: .info, + after: view, + text: text, + spacing: spacing + ) + } } class SCGenericActionLayoutView: ScrollableContainerLayoutView { diff --git a/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift b/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift index 927c6b6ed9..0f3421b005 100644 --- a/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift @@ -1,5 +1,6 @@ import Foundation import BigInt +import SoraFoundation struct RateParams { let assetDisplayInfoIn: AssetBalanceDisplayInfo @@ -11,6 +12,13 @@ struct RateParams { protocol SwapBaseViewModelFactoryProtocol { func rateViewModel(from params: RateParams, locale: Locale) -> String + func priceDifferenceViewModel( + rateParams: RateParams, + priceIn: PriceData?, + priceOut: PriceData?, + locale: Locale + ) -> DifferenceViewModel? + func minimalBalanceSwapForFeeMessage( for networkFeeAddition: AssetConversion.AmountWithNative, feeChainAsset: ChainAsset, @@ -22,9 +30,17 @@ protocol SwapBaseViewModelFactoryProtocol { class SwapBaseViewModelFactory { let balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol + let percentForamatter: LocalizableResource + let priceDifferenceConfig: SwapPriceDifferenceConfig - init(balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol) { + init( + balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol, + percentForamatter: LocalizableResource, + priceDifferenceConfig: SwapPriceDifferenceConfig + ) { self.balanceViewModelFactoryFacade = balanceViewModelFactoryFacade + self.percentForamatter = percentForamatter + self.priceDifferenceConfig = priceDifferenceConfig } } @@ -92,4 +108,46 @@ extension SwapBaseViewModelFactory: SwapBaseViewModelFactoryProtocol { preferredLanguages: locale.rLanguages ) } + + func priceDifferenceViewModel( + rateParams params: RateParams, + priceIn: PriceData?, + priceOut: PriceData?, + locale: Locale + ) -> DifferenceViewModel? { + guard let priceIn = priceIn?.decimalRate, let priceOut = priceOut?.decimalRate else { + return nil + } + + guard + let amountOutDecimal = Decimal.fromSubstrateAmount( + params.amountOut, + precision: params.assetDisplayInfoOut.assetPrecision + ), + let amountInDecimal = Decimal.fromSubstrateAmount( + params.amountIn, + precision: params.assetDisplayInfoIn.assetPrecision + ) else { + return nil + } + + let amountPriceIn = amountInDecimal * priceIn + let amountPriceOut = amountOutDecimal * priceOut + + guard amountPriceIn != 0, amountPriceIn > amountPriceOut else { + return nil + } + + let diff = abs(amountPriceIn - amountPriceOut) / amountPriceIn + let diffString = percentForamatter.value(for: locale).stringFromDecimal(diff)?.inParenthesis() ?? "" + + switch diff { + case _ where diff > priceDifferenceConfig.warningMax: + return .init(details: diffString, attention: .high) + case priceDifferenceConfig.warningMin ... priceDifferenceConfig.warningMax: + return .init(details: diffString, attention: .medium) + default: + return .init(details: diffString, attention: .low) + } + } } diff --git a/novawallet/Modules/Swaps/Base/Model/SwapPriceDifferenceConfig.swift b/novawallet/Modules/Swaps/Base/Model/SwapPriceDifferenceConfig.swift new file mode 100644 index 0000000000..a0c2236663 --- /dev/null +++ b/novawallet/Modules/Swaps/Base/Model/SwapPriceDifferenceConfig.swift @@ -0,0 +1,15 @@ +import Foundation + +struct SwapPriceDifferenceConfig { + let warningMin: Decimal + let warningMax: Decimal +} + +extension SwapPriceDifferenceConfig { + static var defaultConfig: SwapPriceDifferenceConfig { + .init( + warningMin: 0.1, + warningMax: 0.2 + ) + } +} diff --git a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift index 5b6cff50f5..937910472e 100644 --- a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift +++ b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift @@ -263,7 +263,7 @@ class SwapBasePresenter { dataValidatingFactory.notViolatingMinBalancePaying( fee: swapModel.feeChainAsset.isUtilityAsset ? swapModel.feeModel?.totalFee.targetAmount : 0, total: swapModel.utilityAssetBalance?.freeInPlank, - minBalance: swapModel.utilityAssetExistense?.minBalance, + minBalance: swapModel.feeChainAsset.isUtilityAsset ? swapModel.utilityAssetExistense?.minBalance : 0, locale: locale ), dataValidatingFactory.canReceive(params: swapModel, locale: locale), diff --git a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift index 243f08e45e..2851f779a3 100644 --- a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift @@ -2,50 +2,43 @@ import Foundation import SoraFoundation import BigInt -protocol SwapConfirmViewModelFactoryProtocol: SwapPriceDifferenceViewModelFactoryProtocol { - var locale: Locale { get set } - +protocol SwapConfirmViewModelFactoryProtocol: SwapBaseViewModelFactoryProtocol { func assetViewModel( chainAsset: ChainAsset, amount: BigUInt, - priceData: PriceData? + priceData: PriceData?, + locale: Locale ) -> SwapAssetAmountViewModel - func rateViewModel(from params: RateParams) -> String - func priceDifferenceViewModel( - rateParams: RateParams, - priceIn: PriceData?, - priceOut: PriceData? - ) -> DifferenceViewModel? - func slippageViewModel(slippage: BigRational) -> String - func feeViewModel(fee: BigUInt, chainAsset: ChainAsset, priceData: PriceData?) -> SwapFeeViewModel + + func slippageViewModel(slippage: BigRational, locale: Locale) -> String + + func feeViewModel( + fee: BigUInt, + chainAsset: ChainAsset, + priceData: PriceData?, + locale: Locale + ) -> SwapFeeViewModel + func walletViewModel(walletAddress: WalletDisplayAddress) -> WalletAccountViewModel? } -final class SwapConfirmViewModelFactory { - let percentForamatter: LocalizableResource - let balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol +final class SwapConfirmViewModelFactory: SwapBaseViewModelFactory { let walletViewModelFactory = WalletAccountViewModelFactory() let networkViewModelFactory: NetworkViewModelFactoryProtocol - private(set) var localizedPercentForamatter: NumberFormatter - private(set) var priceDifferenceWarningRange: (start: Decimal, end: Decimal) = (start: 0.1, end: 0.2) - - var locale: Locale { - didSet { - localizedPercentForamatter = percentForamatter.value(for: locale) - } - } init( - locale: Locale, balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol, networkViewModelFactory: NetworkViewModelFactoryProtocol, - percentForamatter: LocalizableResource + percentForamatter: LocalizableResource, + priceDifferenceConfig: SwapPriceDifferenceConfig ) { - self.balanceViewModelFactoryFacade = balanceViewModelFactoryFacade self.networkViewModelFactory = networkViewModelFactory - self.percentForamatter = percentForamatter - self.locale = locale - localizedPercentForamatter = percentForamatter.value(for: locale) + + super.init( + balanceViewModelFactoryFacade: balanceViewModelFactoryFacade, + percentForamatter: percentForamatter, + priceDifferenceConfig: priceDifferenceConfig + ) } } @@ -53,7 +46,8 @@ extension SwapConfirmViewModelFactory: SwapConfirmViewModelFactoryProtocol { func assetViewModel( chainAsset: ChainAsset, amount: BigUInt, - priceData: PriceData? + priceData: PriceData?, + locale: Locale ) -> SwapAssetAmountViewModel { let networkViewModel = networkViewModelFactory.createViewModel(from: chainAsset.chain) let assetIcon: ImageViewModelProtocol = chainAsset.asset.icon.map { RemoteImageViewModel(url: $0) } ?? @@ -75,34 +69,16 @@ extension SwapConfirmViewModelFactory: SwapConfirmViewModelFactoryProtocol { ) } - func rateViewModel(from params: RateParams) -> String { - guard - let rate = Decimal.rateFromSubstrate( - amount1: params.amountIn, - amount2: params.amountOut, - precision1: params.assetDisplayInfoIn.assetPrecision, - precision2: params.assetDisplayInfoOut.assetPrecision - ) else { - return "" - } - - let amountIn = balanceViewModelFactoryFacade.amountFromValue( - targetAssetInfo: params.assetDisplayInfoIn, - value: 1 - ).value(for: locale) - let amountOut = balanceViewModelFactoryFacade.amountFromValue( - targetAssetInfo: params.assetDisplayInfoOut, - value: rate - ).value(for: locale) - - return amountIn.estimatedEqual(to: amountOut) - } - - func slippageViewModel(slippage: BigRational) -> String { - slippage.decimalValue.map { localizedPercentForamatter.stringFromDecimal($0) ?? "" } ?? "" + func slippageViewModel(slippage: BigRational, locale: Locale) -> String { + slippage.decimalValue.map { percentForamatter.value(for: locale).stringFromDecimal($0) ?? "" } ?? "" } - func feeViewModel(fee: BigUInt, chainAsset: ChainAsset, priceData: PriceData?) -> SwapFeeViewModel { + func feeViewModel( + fee: BigUInt, + chainAsset: ChainAsset, + priceData: PriceData?, + locale: Locale + ) -> SwapFeeViewModel { let amountDecimal = Decimal.fromSubstrateAmount( fee, precision: chainAsset.assetDisplayInfo.assetPrecision diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index bf0a9b5bf3..1e0432a2fe 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -150,6 +150,7 @@ final class SwapConfirmPresenter: SwapBasePresenter { feeChainAssetId _: ChainAssetId? ) { provideFeeViewModel() + provideNotificationViewModel() } override func handleNewPrice(_: PriceData?, chainAssetId: ChainAssetId) { @@ -178,7 +179,8 @@ extension SwapConfirmPresenter { let viewModel = viewModelFactory.assetViewModel( chainAsset: initState.chainAssetIn, amount: quote.amountIn, - priceData: payAssetPriceData + priceData: payAssetPriceData, + locale: selectedLocale ) view?.didReceiveAssetIn(viewModel: viewModel) @@ -191,7 +193,8 @@ extension SwapConfirmPresenter { let viewModel = viewModelFactory.assetViewModel( chainAsset: initState.chainAssetOut, amount: quote.amountOut, - priceData: receiveAssetPriceData + priceData: receiveAssetPriceData, + locale: selectedLocale ) view?.didReceiveAssetOut(viewModel: viewModel) } @@ -208,7 +211,7 @@ extension SwapConfirmPresenter { amountIn: quote.amountIn, amountOut: quote.amountOut ) - let viewModel = viewModelFactory.rateViewModel(from: params) + let viewModel = viewModelFactory.rateViewModel(from: params, locale: selectedLocale) view?.didReceiveRate(viewModel: .loaded(value: viewModel)) } @@ -229,7 +232,8 @@ extension SwapConfirmPresenter { if let viewModel = viewModelFactory.priceDifferenceViewModel( rateParams: params, priceIn: payAssetPriceData, - priceOut: receiveAssetPriceData + priceOut: receiveAssetPriceData, + locale: selectedLocale ) { view?.didReceivePriceDifference(viewModel: .loaded(value: viewModel)) } else { @@ -238,13 +242,30 @@ extension SwapConfirmPresenter { } private func provideSlippageViewModel() { - let viewModel = viewModelFactory.slippageViewModel(slippage: initState.slippage) + let viewModel = viewModelFactory.slippageViewModel(slippage: initState.slippage, locale: selectedLocale) view?.didReceiveSlippage(viewModel: viewModel) - let warning = slippageBounds.warning( - for: initState.slippage.decimalValue, + let warning = slippageBounds.warning(for: initState.slippage.decimalValue, locale: selectedLocale) + view?.didReceiveWarning(viewModel: warning) + } + + private func provideNotificationViewModel() { + guard + let networkFeeAddition = fee?.networkFeeAddition, + !initState.feeChainAsset.isUtilityAsset, + let utilityChainAsset = initState.feeChainAsset.chain.utilityChainAsset() else { + view?.didReceiveNotification(viewModel: nil) + return + } + + let message = viewModelFactory.minimalBalanceSwapForFeeMessage( + for: networkFeeAddition, + feeChainAsset: initState.feeChainAsset, + utilityChainAsset: utilityChainAsset, + utilityPriceData: prices[utilityChainAsset.chainAssetId], locale: selectedLocale ) - view?.didReceiveWarning(viewModel: warning) + + view?.didReceiveNotification(viewModel: message) } private func provideFeeViewModel() { @@ -256,7 +277,8 @@ extension SwapConfirmPresenter { let viewModel = viewModelFactory.feeViewModel( fee: fee.networkFee.targetAmount, chainAsset: initState.feeChainAsset, - priceData: feeAssetPriceData + priceData: feeAssetPriceData, + locale: selectedLocale ) view?.didReceiveNetworkFee(viewModel: .loaded(value: viewModel)) @@ -284,6 +306,7 @@ extension SwapConfirmPresenter { provideRateViewModel() providePriceDifferenceViewModel() provideSlippageViewModel() + provideNotificationViewModel() provideFeeViewModel() provideWalletViewModel() } @@ -410,7 +433,6 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { extension SwapConfirmPresenter: Localizable { func applyLocalization() { if view?.isSetup == true { - viewModelFactory.locale = selectedLocale updateViews() } } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift index f2030c5960..7f45947be3 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift @@ -9,6 +9,7 @@ protocol SwapConfirmViewProtocol: ControllerBackedProtocol { func didReceiveNetworkFee(viewModel: LoadableViewModelState) func didReceiveWallet(viewModel: WalletAccountViewModel?) func didReceiveWarning(viewModel: String?) + func didReceiveNotification(viewModel: String?) func didReceiveStartLoading() func didReceiveStopLoading() } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift index 150478de24..5e8658d657 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift @@ -125,6 +125,10 @@ extension SwapConfirmViewController: SwapConfirmViewProtocol { rootView.set(warning: viewModel) } + func didReceiveNotification(viewModel: String?) { + rootView.set(notification: viewModel) + } + func didReceiveStartLoading() { rootView.loadableActionView.startLoading() } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index ccfbe26522..5d26a1c670 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -7,8 +7,6 @@ struct SwapConfirmViewFactory { initState: SwapConfirmInitState, generalSubscriptonFactory: GeneralStorageSubscriptionFactoryProtocol ) -> SwapConfirmViewProtocol? { - let accountRequest = initState.chainAssetIn.chain.accountRequest() - guard let currencyManager = CurrencyManager.shared, let wallet = SelectedWalletSettings.shared.value else { return nil } @@ -28,10 +26,10 @@ struct SwapConfirmViewFactory { ) let viewModelFactory = SwapConfirmViewModelFactory( - locale: LocalizationManager.shared.selectedLocale, balanceViewModelFactoryFacade: balanceViewModelFactoryFacade, networkViewModelFactory: NetworkViewModelFactory(), - percentForamatter: NumberFormatter.percentSingle.localizableResource() + percentForamatter: NumberFormatter.percentSingle.localizableResource(), + priceDifferenceConfig: .defaultConfig ) let dataValidatingFactory = SwapDataValidatorFactory( diff --git a/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift b/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift index 55d737520c..924b07ab6a 100644 --- a/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift +++ b/novawallet/Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift @@ -45,6 +45,8 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { private var warningView: InlineAlertView? + private var notificationView: InlineAlertView? + let loadableActionView = LoadableActionView() override func setupStyle() { @@ -100,8 +102,18 @@ final class SwapConfirmViewLayout: ScrollableContainerLayoutView { func set(warning: String?) { applyWarning( on: &warningView, - after: nil, - text: warning + after: walletTableView, + text: warning, + spacing: 8 + ) + } + + func set(notification: String?) { + applyInfo( + on: ¬ificationView, + after: warningView ?? walletTableView, + text: notification, + spacing: 8 ) } } diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift index 16db23d908..f2dd4476ed 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift @@ -1,9 +1,7 @@ import SoraFoundation import BigInt -protocol SwapsSetupViewModelFactoryProtocol: SwapBaseViewModelFactoryProtocol, - SwapPriceDifferenceViewModelFactoryProtocol, - SwapIssueViewModelFactoryProtocol { +protocol SwapsSetupViewModelFactoryProtocol: SwapBaseViewModelFactoryProtocol, SwapIssueViewModelFactoryProtocol { func buttonState(for issueParams: SwapIssueCheckParams, locale: Locale) -> ButtonState func payTitleViewModel( @@ -44,21 +42,22 @@ protocol SwapsSetupViewModelFactoryProtocol: SwapBaseViewModelFactoryProtocol, final class SwapsSetupViewModelFactory: SwapBaseViewModelFactory { let issuesViewModelFactory: SwapIssueViewModelFactoryProtocol let networkViewModelFactory: NetworkViewModelFactoryProtocol - let percentForamatter: LocalizableResource - - private(set) var priceDifferenceWarningRange: (start: Decimal, end: Decimal) = (start: 0.1, end: 0.2) init( balanceViewModelFactoryFacade: BalanceViewModelFactoryFacadeProtocol, issuesViewModelFactory: SwapIssueViewModelFactoryProtocol, networkViewModelFactory: NetworkViewModelFactoryProtocol, - percentForamatter: LocalizableResource + percentForamatter: LocalizableResource, + priceDifferenceConfig: SwapPriceDifferenceConfig ) { self.issuesViewModelFactory = issuesViewModelFactory self.networkViewModelFactory = networkViewModelFactory - self.percentForamatter = percentForamatter - super.init(balanceViewModelFactoryFacade: balanceViewModelFactoryFacade) + super.init( + balanceViewModelFactoryFacade: balanceViewModelFactoryFacade, + percentForamatter: percentForamatter, + priceDifferenceConfig: priceDifferenceConfig + ) } private static func buttonTitle( diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 151ef1acd8..b52b3bddc7 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -380,7 +380,8 @@ extension SwapSetupPresenter { differenceViewModel = viewModelFactory.priceDifferenceViewModel( rateParams: params, priceIn: payAssetPriceData, - priceOut: receiveAssetPriceData + priceOut: receiveAssetPriceData, + locale: selectedLocale ) } else { differenceViewModel = nil diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index bf1bc24a5a..ab65cd6e7e 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -41,7 +41,7 @@ struct SwapSetupViewFactory { issuesViewModelFactory: issuesViewModelFactory, networkViewModelFactory: NetworkViewModelFactory(), percentForamatter: NumberFormatter.percentSingle.localizableResource(), - locale: LocalizationManager.shared.selectedLocale + priceDifferenceConfig: .defaultConfig ) let dataValidatingFactory = SwapDataValidatorFactory( From 2bf20a7019b5215a774d490295e6c8657ab887d6 Mon Sep 17 00:00:00 2001 From: ERussel Date: Thu, 9 Nov 2023 07:24:51 +0100 Subject: [PATCH 139/204] fix tests --- .../Modules/Swaps/SwapsValidationTests.swift | 73 +++++++++---------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/novawalletTests/Modules/Swaps/SwapsValidationTests.swift b/novawalletTests/Modules/Swaps/SwapsValidationTests.swift index d5089201ed..4bae078c13 100644 --- a/novawalletTests/Modules/Swaps/SwapsValidationTests.swift +++ b/novawalletTests/Modules/Swaps/SwapsValidationTests.swift @@ -23,37 +23,38 @@ final class SwapsValidationTests: XCTestCase { reservedInPlank: 0, frozenInPlank: 0, blocked: false) - let utilityAssetBalance = AssetBalance(chainAssetId: utilityChainAsset.chainAssetId, - accountId: accountId, - freeInPlank: 0, - reservedInPlank: 0, - frozenInPlank: 0, - blocked: false) let existentialDeposit = amountInPlank(1, utilityChainAsset) let fee = amountInPlank(0.1, payChainAsset) let existentialDepositInFeeToken = amountInPlank(0.01, payChainAsset) - let params = SwapFeeParams( - fee: fee, - feeChainAsset: payChainAsset, - feeAssetBalance: payAssetBalance, - edAmount: existentialDeposit, - edAmountInFeeToken: existentialDepositInFeeToken, - edChainAsset: utilityChainAsset, - edChainAssetBalance: utilityAssetBalance, + let swapMax = SwapMaxModel( payChainAsset: payChainAsset, - amountUpdateClosure: { _ in }) + feeChainAsset: feeChainAsset, + balance: payAssetBalance, + feeModel: .init( + totalFee: .init( + targetAmount: fee + existentialDepositInFeeToken, + nativeAmount: (fee + existentialDepositInFeeToken) / 100 + ), + networkFeeAddition: .init( + targetAmount: existentialDepositInFeeToken, + nativeAmount: existentialDeposit + ) + ), + payAssetExistense: nil, + receiveAssetExistense: nil, + accountInfo: nil + ) - let result = params.prepare(swapAmount: 50) + let result = swapMax.calculate() - XCTAssertEqual(result.availableToPay, 49.89) + XCTAssertEqual(result, 49.89) } func testCalculatedFeeWithED() throws { let chain = ChainModelGenerator.generateChain(generatingAssets: 3, addressPrefix: 42) let utilityChainAsset = ChainAsset(chain: chain, asset: chain.assets.first(where: { $0.assetId == 0 })!) - let ksmChainAsset = ChainAsset(chain: chain, asset: chain.assets.first(where: { $0.assetId == 1 })!) let payChainAsset = ChainAsset(chain: chain, asset: chain.assets.first(where: { $0.assetId == 2 })!) let feeChainAsset = utilityChainAsset let accountId = try WestendStub.address.toAccountId() @@ -65,30 +66,28 @@ final class SwapsValidationTests: XCTestCase { reservedInPlank: 0, frozenInPlank: 0, blocked: false) - let utilityAssetBalance = AssetBalance(chainAssetId: utilityChainAsset.chainAssetId, - accountId: accountId, - freeInPlank: 10, - reservedInPlank: 0, - frozenInPlank: 0, - blocked: false) - let existentialDeposit = amountInPlank(1, utilityChainAsset) + let fee = amountInPlank(0.1, payChainAsset) - let existentialDepositInFeeToken = amountInPlank(0.01, payChainAsset) - let params = SwapFeeParams( - fee: fee, - feeChainAsset: payChainAsset, - feeAssetBalance: payAssetBalance, - edAmount: existentialDeposit, - edAmountInFeeToken: existentialDepositInFeeToken, - edChainAsset: utilityChainAsset, - edChainAssetBalance: utilityAssetBalance, + let params = SwapMaxModel( payChainAsset: payChainAsset, - amountUpdateClosure: { _ in }) + feeChainAsset: feeChainAsset, + balance: payAssetBalance, + feeModel: .init( + totalFee: .init( + targetAmount: fee, + nativeAmount: fee + ), + networkFeeAddition: nil + ), + payAssetExistense: nil, + receiveAssetExistense: nil, + accountInfo: nil + ) - let result = params.prepare(swapAmount: 50) + let result = params.calculate() - XCTAssertEqual(result.availableToPay, 49.9) + XCTAssertEqual(result, 50) } } From 89a0dadad4bd254f2d117e4424851202cfaca5cc Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 9 Nov 2023 11:13:53 +0300 Subject: [PATCH 140/204] fix transaction history amount --- .../TransactionHistoryViewModelFactory.swift | 8 +++--- .../Model/TransactionSectionModel.swift | 27 ++++++++++++++++--- .../View/HistoryItemTableViewCell.swift | 7 +++-- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift index d488b60064..f048a431e5 100644 --- a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift +++ b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift @@ -91,7 +91,7 @@ final class TransactionHistoryViewModelFactory { subtitle: itemTitleWithSubtitle.subtitle, amount: balance.amount, amountDetails: amountDetails, - type: txType, + typeViewModel: .init(txType), status: data.status, imageViewModel: imageViewModel ) @@ -133,7 +133,7 @@ final class TransactionHistoryViewModelFactory { subtitle: subtitle, amount: balance.amount, amountDetails: amountDetails, - type: txType, + typeViewModel: .init(txType, isIncome: !isOutgoing), status: data.status, imageViewModel: imageViewModel ) @@ -263,7 +263,7 @@ final class TransactionHistoryViewModelFactory { subtitle: subtitle, amount: balance.amount, amountDetails: amountDetails, - type: txType, + typeViewModel: .init(txType), status: data.status, imageViewModel: imageViewModel ) @@ -306,7 +306,7 @@ final class TransactionHistoryViewModelFactory { subtitle: extrinsicTitleWithSubtitle.subtitle, amount: balance.amount, amountDetails: amountDetails, - type: txType, + typeViewModel: .init(txType), status: data.status, imageViewModel: imageViewModel ) diff --git a/novawallet/Modules/TransactionHistory/Model/TransactionSectionModel.swift b/novawallet/Modules/TransactionHistory/Model/TransactionSectionModel.swift index 02b672a946..b79e58f41e 100644 --- a/novawallet/Modules/TransactionHistory/Model/TransactionSectionModel.swift +++ b/novawallet/Modules/TransactionHistory/Model/TransactionSectionModel.swift @@ -14,13 +14,13 @@ struct TransactionItemViewModel: Hashable { lhs.subtitle == rhs.subtitle && lhs.amountDetails == rhs.amountDetails && lhs.amount == rhs.amount && - lhs.type == rhs.type && + lhs.typeViewModel.type == rhs.typeViewModel.type && lhs.status == rhs.status } func hash(into hasher: inout Hasher) { hasher.combine(identifier) - hasher.combine(type) + hasher.combine(typeViewModel.type) } let identifier: String @@ -29,7 +29,28 @@ struct TransactionItemViewModel: Hashable { let subtitle: String let amount: String let amountDetails: String - let type: TransactionType + let typeViewModel: TransactionTypeViewModel let status: TransactionHistoryItem.Status let imageViewModel: ImageViewModelProtocol? } + +struct TransactionTypeViewModel { + let type: TransactionType + let isIncome: Bool + + init(_ type: TransactionType, isIncome: Bool? = nil) { + self.type = type + self.isIncome = isIncome ?? TransactionTypeViewModel.incomeDefault(for: type) + } + + static func incomeDefault(for type: TransactionType) -> Bool { + switch type { + case .incoming, .reward, .poolReward: + return true + case .outgoing, .slash, .poolSlash, .extrinsic: + return false + case .swap: + return false + } + } +} diff --git a/novawallet/Modules/TransactionHistory/View/HistoryItemTableViewCell.swift b/novawallet/Modules/TransactionHistory/View/HistoryItemTableViewCell.swift index e2a736682f..a572e2e12b 100644 --- a/novawallet/Modules/TransactionHistory/View/HistoryItemTableViewCell.swift +++ b/novawallet/Modules/TransactionHistory/View/HistoryItemTableViewCell.swift @@ -168,16 +168,15 @@ extension HistoryItemTableViewCell { subtitleLabel.text = transactionModel.subtitle amountDetailsLabel.text = transactionModel.amountDetails - switch transactionModel.type { - case .incoming, .reward, .poolReward, .swap: + if transactionModel.typeViewModel.isIncome { amountLabel.text = "+ \(transactionModel.amount)" amountLabel.textColor = R.color.colorTextPositive()! - case .outgoing, .slash, .poolSlash, .extrinsic: + } else { amountLabel.text = "- \(transactionModel.amount)" amountLabel.textColor = R.color.colorTextPrimary()! } - switch transactionModel.type { + switch transactionModel.typeViewModel.type { case .incoming, .outgoing, .extrinsic: subtitleLabel.lineBreakMode = .byTruncatingMiddle From d9fe10f0a1e230fee6f91f2572c340a1d8f4628e Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 9 Nov 2023 12:16:00 +0300 Subject: [PATCH 141/204] add hasSwaps check to filter --- novawallet.xcodeproj/project.pbxproj | 17 +- .../Migration/SubstrateStorageVersion.swift | 3 + .../SubqueryHistoryOperationFactory.swift | 9 +- .../.xccurrentversion | 2 +- .../SubstrateDataModel21.xcdatamodel/contents | 202 ++++++++++++++++++ .../Storage/SubstrateDataStorageFacade.swift | 2 +- .../OperationDetailsViewModelFactory.swift | 2 +- .../Service/AssetHistoryFactoryFacade.swift | 3 +- 8 files changed, 223 insertions(+), 17 deletions(-) create mode 100644 novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel21.xcdatamodel/contents diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 2fcd3d27d0..952a2bb4e9 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -807,13 +807,11 @@ 77AB55592AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */; }; 77AB555B2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */; }; 77AB555D2AA24BA90058814E /* OperationDetailsPoolRewardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */; }; - 77C9761E2AF220130049272C /* SwapSelectedChainAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9761D2AF220130049272C /* SwapSelectedChainAsset.swift */; }; - 77C976262AF421AE0049272C /* OperationDetailsSwapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C976252AF421AE0049272C /* OperationDetailsSwapView.swift */; }; - 77C976282AF426100049272C /* OperationSwapViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C976272AF426100049272C /* OperationSwapViewModel.swift */; }; 77C976202AF36A170049272C /* SwapModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9761F2AF36A170049272C /* SwapModels.swift */; }; 77C976222AF39F180049272C /* TokenOperationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C976212AF39F180049272C /* TokenOperationTableViewCell.swift */; }; 77C976242AF3A5280049272C /* SwapViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C976232AF3A5280049272C /* SwapViewModels.swift */; }; - 77C9BCBC2ACD1AF500022EA2 /* ViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */; }; + 77C976262AF421AE0049272C /* OperationDetailsSwapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C976252AF421AE0049272C /* OperationDetailsSwapView.swift */; }; + 77C976282AF426100049272C /* OperationSwapViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C976272AF426100049272C /* OperationSwapViewModel.swift */; }; 77C9BCBE2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */; }; 77C9BCC42ACD570100022EA2 /* SwapAssetsOperationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */; }; 77C9BCC62ACD571400022EA2 /* SwapAssetOperationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77C9BCC52ACD571400022EA2 /* SwapAssetOperationPresenter.swift */; }; @@ -4868,23 +4866,21 @@ 77A6F5CE2A31C4D4004AFD1A /* Web3TransferRecipientRepositoryFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Web3TransferRecipientRepositoryFactory.swift; sourceTree = ""; }; 77A6F5D12A31DB8C004AFD1A /* JsonCanonicalizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonCanonicalizer.swift; sourceTree = ""; }; 77A6F5D42A31E046004AFD1A /* JsonCanonicalizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonCanonicalizerTests.swift; sourceTree = ""; }; - 77AAE21C2AF56C88006872CC /* SubstrateDataModel20.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel20.xcdatamodel; sourceTree = ""; }; 77AAE21F2AFB00CB006872CC /* OperationSwapModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationSwapModel.swift; sourceTree = ""; }; 77AAE2212AFB026E006872CC /* OperationDetailsSwapProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsSwapProvider.swift; sourceTree = ""; }; 77AAE2232AFB67BE006872CC /* OperationSwapDetailsInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationSwapDetailsInteractor.swift; sourceTree = ""; }; 77AAE2252AFC10EE006872CC /* CalculatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorFactory.swift; sourceTree = ""; }; 77AAE2272AFC1167006872CC /* ChainModel+historyId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChainModel+historyId.swift"; sourceTree = ""; }; 77AAE2292AFC36EE006872CC /* OperationDetailsBaseInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsBaseInteractor.swift; sourceTree = ""; }; + 77AAE22B2AFCD7AA006872CC /* SubstrateDataModel21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel21.xcdatamodel; sourceTree = ""; }; 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashModel.swift; sourceTree = ""; }; 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashViewModel.swift; sourceTree = ""; }; 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsPoolRewardView.swift; sourceTree = ""; }; - 77C9761D2AF220130049272C /* SwapSelectedChainAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapSelectedChainAsset.swift; sourceTree = ""; }; - 77C976252AF421AE0049272C /* OperationDetailsSwapView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsSwapView.swift; sourceTree = ""; }; - 77C976272AF426100049272C /* OperationSwapViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationSwapViewModel.swift; sourceTree = ""; }; 77C9761F2AF36A170049272C /* SwapModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapModels.swift; sourceTree = ""; }; 77C976212AF39F180049272C /* TokenOperationTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenOperationTableViewCell.swift; sourceTree = ""; }; 77C976232AF3A5280049272C /* SwapViewModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapViewModels.swift; sourceTree = ""; }; - 77C9BCBB2ACD1AF500022EA2 /* ViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModels.swift; sourceTree = ""; }; + 77C976252AF421AE0049272C /* OperationDetailsSwapView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsSwapView.swift; sourceTree = ""; }; + 77C976272AF426100049272C /* OperationSwapViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationSwapViewModel.swift; sourceTree = ""; }; 77C9BCBD2ACD286100022EA2 /* SwapsSetupViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapsSetupViewModelFactory.swift; sourceTree = ""; }; 77C9BCC32ACD570100022EA2 /* SwapAssetsOperationInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetsOperationInteractor.swift; sourceTree = ""; }; 77C9BCC52ACD571400022EA2 /* SwapAssetOperationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapAssetOperationPresenter.swift; sourceTree = ""; }; @@ -24185,6 +24181,7 @@ 843910CA253F7E6500E3C217 /* SubstrateDataModel.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 77AAE22B2AFCD7AA006872CC /* SubstrateDataModel21.xcdatamodel */, 0CEB4ECF2AF148500048FD84 /* SubstrateDataModel20.xcdatamodel */, 0C59E8C72AA5C3F4001E11F3 /* SubstrateDataModel19.xcdatamodel */, 0CC2E55C2A6AAFFD004092E7 /* SubstrateDataModel18.xcdatamodel */, @@ -24206,7 +24203,7 @@ 88787F0328DB3A7B00B115AB /* SubstrateDataModel2.xcdatamodel */, 843910CB253F7E6500E3C217 /* SubstrateDataModel.xcdatamodel */, ); - currentVersion = 0CEB4ECF2AF148500048FD84 /* SubstrateDataModel20.xcdatamodel */; + currentVersion = 77AAE22B2AFCD7AA006872CC /* SubstrateDataModel21.xcdatamodel */; path = SubstrateDataModel.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/novawallet/Common/Migration/SubstrateStorageVersion.swift b/novawallet/Common/Migration/SubstrateStorageVersion.swift index 498998e6dc..a136c16f3b 100644 --- a/novawallet/Common/Migration/SubstrateStorageVersion.swift +++ b/novawallet/Common/Migration/SubstrateStorageVersion.swift @@ -19,6 +19,7 @@ enum SubstrateStorageVersion: String, CaseIterable { case version18 = "SubstrateDataModel18" case version19 = "SubstrateDataModel19" case version20 = "SubstrateDataModel20" + case version21 = "SubstrateDataModel21" static var current: SubstrateStorageVersion { allCases.last! @@ -65,6 +66,8 @@ enum SubstrateStorageVersion: String, CaseIterable { case .version19: return .version20 case .version20: + return .version21 + case .version21: return nil } } diff --git a/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift b/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift index eccf3d46c4..df39127ffa 100644 --- a/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift +++ b/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift @@ -15,12 +15,14 @@ final class SubqueryHistoryOperationFactory { let filter: WalletHistoryFilter let assetId: String? let hasPoolStaking: Bool + let hasSwaps: Bool - init(url: URL, filter: WalletHistoryFilter, assetId: String?, hasPoolStaking: Bool) { + init(url: URL, filter: WalletHistoryFilter, assetId: String?, hasPoolStaking: Bool, hasSwaps: Bool) { self.url = url self.filter = filter self.assetId = assetId self.hasPoolStaking = hasPoolStaking + self.hasSwaps = hasSwaps } private func prepareExtrinsicInclusionFilter() -> String { @@ -87,7 +89,7 @@ final class SubqueryHistoryOperationFactory { } } - if filter.contains(.swaps) { + if filter.contains(.swaps), hasSwaps { if let assetId = assetId { filterStrings.append(prepareAssetIdFilter(assetId)) } else { @@ -107,6 +109,7 @@ final class SubqueryHistoryOperationFactory { let transferField = assetId != nil ? "assetTransfer" : "transfer" let filterString = prepareFilter() let poolRewardField = hasPoolStaking ? "poolReward" : "" + let swapField = hasSwaps ? "swap" : "" return """ { historyElements( @@ -135,7 +138,7 @@ final class SubqueryHistoryOperationFactory { extrinsic \(transferField) \(poolRewardField) - swap + \(swapField) } } } diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion index 716e4bb7fc..9ead670ac5 100644 --- a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - SubstrateDataModel20.xcdatamodel + SubstrateDataModel21.xcdatamodel diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel21.xcdatamodel/contents b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel21.xcdatamodel/contents new file mode 100644 index 0000000000..c53f8a08b5 --- /dev/null +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel21.xcdatamodel/contents @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/novawallet/Common/Storage/SubstrateDataStorageFacade.swift b/novawallet/Common/Storage/SubstrateDataStorageFacade.swift index a8ccd5b55f..0c16dde937 100644 --- a/novawallet/Common/Storage/SubstrateDataStorageFacade.swift +++ b/novawallet/Common/Storage/SubstrateDataStorageFacade.swift @@ -4,7 +4,7 @@ import CoreData enum SubstrateStorageParams { static let databaseName = "SubstrateDataModel.sqlite" static let modelDirectory: String = "SubstrateDataModel.momd" - static let modelVersion: SubstrateStorageVersion = .version20 + static let modelVersion: SubstrateStorageVersion = .version21 static let storageDirectoryURL: URL = { let baseURL = FileManager.default.urls( diff --git a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift index 7375db2d7f..c1e947a62c 100644 --- a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift +++ b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift @@ -326,7 +326,7 @@ final class OperationDetailsViewModelFactory { value: difference ).value(for: locale) - return "\(amountIn) = \(amountOut)" + return "\(amountIn) ≈ \(amountOut)" } private func createContentViewModel( diff --git a/novawallet/Modules/TransactionHistory/Service/AssetHistoryFactoryFacade.swift b/novawallet/Modules/TransactionHistory/Service/AssetHistoryFactoryFacade.swift index 933e1afb11..f500cc2aef 100644 --- a/novawallet/Modules/TransactionHistory/Service/AssetHistoryFactoryFacade.swift +++ b/novawallet/Modules/TransactionHistory/Service/AssetHistoryFactoryFacade.swift @@ -37,7 +37,8 @@ final class AssetHistoryFacade { url: url, filter: mappedFilter, assetId: historyAssetId, - hasPoolStaking: asset.hasPoolStaking + hasPoolStaking: asset.hasPoolStaking, + hasSwaps: chainAsset.chain.hasSwaps ) } catch { return nil From 42fceab5a030b23cd5df379d95f624b1e86d2d20 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 9 Nov 2023 14:13:01 +0300 Subject: [PATCH 142/204] fix tests --- .../Helper/AssetTransactionGenerator.swift | 4 +- novawalletTests/Mocks/ModuleMocks.swift | 96 +++++++++++++++++++ .../ExtrinsicDetailsTests.swift | 6 +- 3 files changed, 101 insertions(+), 5 deletions(-) diff --git a/novawalletTests/Helper/AssetTransactionGenerator.swift b/novawalletTests/Helper/AssetTransactionGenerator.swift index 405a430603..f96d451bb0 100644 --- a/novawalletTests/Helper/AssetTransactionGenerator.swift +++ b/novawalletTests/Helper/AssetTransactionGenerator.swift @@ -22,9 +22,11 @@ enum AssetTransactionGenerator { txHash: hash, timestamp: Int64(Date().timeIntervalSince1970), fee: nil, + feeAssetId: nil, blockNumber: 100, txIndex: 0, callPath: .transfer, - call: nil + call: nil, + swap: nil ) } } diff --git a/novawalletTests/Mocks/ModuleMocks.swift b/novawalletTests/Mocks/ModuleMocks.swift index 5883da179f..494ddeb062 100644 --- a/novawalletTests/Mocks/ModuleMocks.swift +++ b/novawalletTests/Mocks/ModuleMocks.swift @@ -17485,6 +17485,51 @@ import Cuckoo } + + + func showRateInfo() { + + return cuckoo_manager.call("showRateInfo()", + parameters: (), + escapingParameters: (), + superclassCall: + + Cuckoo.MockManager.crashOnProtocolSuperclassCall() + , + defaultCall: __defaultImplStub!.showRateInfo()) + + } + + + + func showNetworkFeeInfo() { + + return cuckoo_manager.call("showNetworkFeeInfo()", + parameters: (), + escapingParameters: (), + superclassCall: + + Cuckoo.MockManager.crashOnProtocolSuperclassCall() + , + defaultCall: __defaultImplStub!.showNetworkFeeInfo()) + + } + + + + func repeatOperation() { + + return cuckoo_manager.call("repeatOperation()", + parameters: (), + escapingParameters: (), + superclassCall: + + Cuckoo.MockManager.crashOnProtocolSuperclassCall() + , + defaultCall: __defaultImplStub!.repeatOperation()) + + } + struct __StubbingProxy_OperationDetailsPresenterProtocol: Cuckoo.StubbingProxy { private let cuckoo_manager: Cuckoo.MockManager @@ -17519,6 +17564,21 @@ import Cuckoo return .init(stub: cuckoo_manager.createStub(for: MockOperationDetailsPresenterProtocol.self, method: "send()", parameterMatchers: matchers)) } + func showRateInfo() -> Cuckoo.ProtocolStubNoReturnFunction<()> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return .init(stub: cuckoo_manager.createStub(for: MockOperationDetailsPresenterProtocol.self, method: "showRateInfo()", parameterMatchers: matchers)) + } + + func showNetworkFeeInfo() -> Cuckoo.ProtocolStubNoReturnFunction<()> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return .init(stub: cuckoo_manager.createStub(for: MockOperationDetailsPresenterProtocol.self, method: "showNetworkFeeInfo()", parameterMatchers: matchers)) + } + + func repeatOperation() -> Cuckoo.ProtocolStubNoReturnFunction<()> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return .init(stub: cuckoo_manager.createStub(for: MockOperationDetailsPresenterProtocol.self, method: "repeatOperation()", parameterMatchers: matchers)) + } + } struct __VerificationProxy_OperationDetailsPresenterProtocol: Cuckoo.VerificationProxy { @@ -17565,6 +17625,24 @@ import Cuckoo return cuckoo_manager.verify("send()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } + @discardableResult + func showRateInfo() -> Cuckoo.__DoNotUse<(), Void> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return cuckoo_manager.verify("showRateInfo()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + } + + @discardableResult + func showNetworkFeeInfo() -> Cuckoo.__DoNotUse<(), Void> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return cuckoo_manager.verify("showNetworkFeeInfo()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + } + + @discardableResult + func repeatOperation() -> Cuckoo.__DoNotUse<(), Void> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return cuckoo_manager.verify("repeatOperation()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + } + } } @@ -17604,6 +17682,24 @@ import Cuckoo return DefaultValueRegistry.defaultValue(for: (Void).self) } + + + func showRateInfo() { + return DefaultValueRegistry.defaultValue(for: (Void).self) + } + + + + func showNetworkFeeInfo() { + return DefaultValueRegistry.defaultValue(for: (Void).self) + } + + + + func repeatOperation() { + return DefaultValueRegistry.defaultValue(for: (Void).self) + } + } diff --git a/novawalletTests/Modules/WalletOperationDetails/ExtrinsicDetailsTests.swift b/novawalletTests/Modules/WalletOperationDetails/ExtrinsicDetailsTests.swift index 7ea343c2a5..2e3d5e07d4 100644 --- a/novawalletTests/Modules/WalletOperationDetails/ExtrinsicDetailsTests.swift +++ b/novawalletTests/Modules/WalletOperationDetails/ExtrinsicDetailsTests.swift @@ -62,14 +62,12 @@ class OperationDetailsTests: XCTestCase { operationDataProvider: operationDataProvider ) - let balanceViewModelFactory = BalanceViewModelFactory( - targetAssetInfo: chainAsset.assetDisplayInfo, + let balanceViewModelFactoryFacade = BalanceViewModelFactoryFacade( priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: CurrencyManagerStub()) ) let viewModelFactory = OperationDetailsViewModelFactory( - balanceViewModelFactory: balanceViewModelFactory, - feeViewModelFactory: nil + balanceViewModelFactoryFacade: balanceViewModelFactoryFacade ) let presenter = OperationDetailsPresenter( From f4d671140816ffa2c7aa0b8a415adf1a931cad26 Mon Sep 17 00:00:00 2001 From: ERussel Date: Thu, 9 Nov 2023 16:32:38 +0100 Subject: [PATCH 143/204] fix fee in custom token --- .../Substrate/ExtrinsicServiceFactory.swift | 12 ++ .../Service/AssetConversionFeeService.swift | 21 +-- .../Service/AssetHub/AssetHubFeeService.swift | 121 +++++++++++------- .../Base/Model/AssetHubFeeModelBuilder.swift | 4 +- .../Swaps/Base/Model/SwapMaxModel.swift | 7 +- .../Swaps/Confirm/SwapConfirmPresenter.swift | 2 +- .../Swaps/Setup/SwapSetupPresenter.swift | 2 +- .../Modules/Swaps/Validation/SwapModel.swift | 4 +- 8 files changed, 109 insertions(+), 64 deletions(-) diff --git a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceFactory.swift b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceFactory.swift index 08bb8a2d46..33ced3b4ea 100644 --- a/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceFactory.swift +++ b/novawallet/Common/Services/ExtrinsicService/Substrate/ExtrinsicServiceFactory.swift @@ -50,6 +50,18 @@ extension ExtrinsicServiceFactoryProtocol { extensions: DefaultExtrinsicExtension.extensions() ) } + + func createOperationFactory( + account: ChainAccountResponse, + chain: ChainModel, + feeAssetConversionId: AssetConversionPallet.AssetId + ) -> ExtrinsicOperationFactoryProtocol { + createOperationFactory( + account: account, + chain: chain, + extensions: DefaultExtrinsicExtension.extensions(payingFeeIn: feeAssetConversionId) + ) + } } final class ExtrinsicServiceFactory { diff --git a/novawallet/Modules/AssetConversion/Service/AssetConversionFeeService.swift b/novawallet/Modules/AssetConversion/Service/AssetConversionFeeService.swift index 8bf5c4dbef..f827230900 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetConversionFeeService.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetConversionFeeService.swift @@ -9,20 +9,20 @@ extension AssetConversion { struct FeeModel: Equatable { let totalFee: AmountWithNative - let networkFeeAddition: AmountWithNative? + let networkFee: AmountWithNative - var networkFee: AmountWithNative { - guard let addition = networkFeeAddition else { - return totalFee - } + var networkNativeFeeAddition: AmountWithNative? { + let targetAmount = totalFee.targetAmount > networkFee.targetAmount ? + totalFee.targetAmount - networkFee.targetAmount : 0 - let feeInTargetToken = totalFee.targetAmount >= addition.targetAmount ? - totalFee.targetAmount - addition.targetAmount : totalFee.targetAmount + guard targetAmount > 0 else { + return nil + } - let feeInNativeToken = totalFee.nativeAmount >= addition.nativeAmount ? - totalFee.nativeAmount - addition.nativeAmount : totalFee.nativeAmount + let nativeAmount = totalFee.nativeAmount > networkFee.nativeAmount ? + totalFee.nativeAmount - networkFee.nativeAmount : 0 - return .init(targetAmount: feeInTargetToken, nativeAmount: feeInNativeToken) + return .init(targetAmount: targetAmount, nativeAmount: nativeAmount) } } @@ -45,6 +45,7 @@ enum AssetConversionFeeServiceError: Error { case chainRuntimeMissing case chainConnectionMissing case utilityAssetMissing + case feeAssetConversionFailed case setupFailed(String) case calculationFailed(String) } diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubFeeService.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubFeeService.swift index 466f50ca47..b6d37af77b 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubFeeService.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubFeeService.swift @@ -4,7 +4,7 @@ import BigInt final class AssetHubFeeService: AnyCancellableCleaning { struct ChainOperationFactory { - let extrinsicOperationFactory: ExtrinsicOperationFactoryProtocol + let extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol let conversionOperationFactory: AssetConversionOperationFactoryProtocol let conversionExtrinsicService: AssetConversionExtrinsicServiceProtocol } @@ -28,20 +28,14 @@ final class AssetHubFeeService: AnyCancellableCleaning { self.operationQueue = operationQueue } - private func updateFactories(for asset: ChainAsset) throws -> ChainOperationFactory { - if asset.chainAssetId.chainId == chainId, let factories = factories { + private func updateFactories(for chain: ChainModel) throws -> ChainOperationFactory { + if chain.chainId == chainId, let factories = factories { return factories } factories = nil chainId = nil - let chain = asset.chain - - guard let account = wallet.fetch(for: chain.accountRequest()) else { - throw AssetConversionFeeServiceError.accountMissing - } - guard let connection = chainRegistry.getConnection(for: chain.chainId) else { throw AssetConversionFeeServiceError.chainConnectionMissing } @@ -50,13 +44,10 @@ final class AssetHubFeeService: AnyCancellableCleaning { throw AssetConversionFeeServiceError.chainRuntimeMissing } - let extrinsicOperationFactory = ExtrinsicServiceFactory( + let extrinsicServiceFactory = ExtrinsicServiceFactory( runtimeRegistry: runtimeProvider, engine: connection, operationManager: OperationManager(operationQueue: operationQueue) - ).createOperationFactory( - account: account, - chain: chain ) let conversionOperationFactory = AssetHubSwapOperationFactory( @@ -69,13 +60,13 @@ final class AssetHubFeeService: AnyCancellableCleaning { let conversionExtrinsicService = AssetHubExtrinsicService(chain: chain) let factories = ChainOperationFactory( - extrinsicOperationFactory: extrinsicOperationFactory, + extrinsicServiceFactory: extrinsicServiceFactory, conversionOperationFactory: conversionOperationFactory, conversionExtrinsicService: conversionExtrinsicService ) self.factories = factories - chainId = asset.chainAssetId.chainId + chainId = chain.chainId return factories } @@ -96,13 +87,15 @@ final class AssetHubFeeService: AnyCancellableCleaning { let utilityChainAsset = ChainAsset(chain: asset.chain, asset: utilityAsset) - let factories = try updateFactories(for: asset) + let factories = try updateFactories(for: asset.chain) let nativeFeeWrapper = createNativeFeeWrapper( for: callArgs, runtimeProvider: runtimeProvider, - extrinsicOperationFactory: factories.extrinsicOperationFactory, - conversionExtrinsicService: factories.conversionExtrinsicService + extrinsicServiceFactory: factories.extrinsicServiceFactory, + conversionExtrinsicService: factories.conversionExtrinsicService, + wallet: wallet, + asset: asset ) let universalFeeWrapper: CompoundOperationWrapper @@ -141,27 +134,66 @@ final class AssetHubFeeService: AnyCancellableCleaning { operationQueue.addOperations(universalFeeWrapper.allOperations, waitUntilFinished: false) } + // swiftlint:disable:next function_parameter_count private func createNativeFeeWrapper( for callArgs: AssetConversion.CallArgs, runtimeProvider: RuntimeProviderProtocol, - extrinsicOperationFactory: ExtrinsicOperationFactoryProtocol, - conversionExtrinsicService: AssetConversionExtrinsicServiceProtocol + extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, + conversionExtrinsicService: AssetConversionExtrinsicServiceProtocol, + wallet: MetaAccountModel, + asset: ChainAsset ) -> CompoundOperationWrapper { let coderFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() - let feeWrapper = extrinsicOperationFactory.estimateFeeOperation { builder in - let codingFactory = try coderFactoryOperation.extractNoCancellableResultData() + let mainFeeOperation = OperationCombiningService( + operationManager: OperationManager(operationQueue: operationQueue) + ) { + guard let account = wallet.fetch(for: asset.chain.accountRequest()) else { + throw AssetConversionFeeServiceError.accountMissing + } - let builderSetupClosure = conversionExtrinsicService.fetchExtrinsicBuilderClosure( - for: callArgs, - codingFactory: codingFactory - ) + let coderFactory = try coderFactoryOperation.extractNoCancellableResultData() - return try builderSetupClosure(builder) - } + let extrinsicOperationFactory: ExtrinsicOperationFactoryProtocol + + if asset.isUtilityAsset { + extrinsicOperationFactory = extrinsicServiceFactory.createOperationFactory( + account: account, + chain: asset.chain + ) + } else { + guard let assetId = AssetHubTokensConverter.convertToMultilocation( + chainAsset: asset, + codingFactory: coderFactory + ) else { + throw AssetConversionFeeServiceError.feeAssetConversionFailed + } + + extrinsicOperationFactory = extrinsicServiceFactory.createOperationFactory( + account: account, + chain: asset.chain, + feeAssetConversionId: assetId + ) + } + + let feeWrapper = extrinsicOperationFactory.estimateFeeOperation { builder in + let codingFactory = try coderFactoryOperation.extractNoCancellableResultData() + + let builderSetupClosure = conversionExtrinsicService.fetchExtrinsicBuilderClosure( + for: callArgs, + codingFactory: codingFactory + ) + + return try builderSetupClosure(builder) + } + + return [feeWrapper] + }.longrunOperation() let mappingOperation = ClosureOperation { - let feeModel = try feeWrapper.targetOperation.extractNoCancellableResultData() + guard let feeModel = try mainFeeOperation.extractNoCancellableResultData().first else { + throw CommonError.dataCorruption + } guard let fee = BigUInt(feeModel.fee) else { throw CommonError.dataCorruption @@ -170,12 +202,13 @@ final class AssetHubFeeService: AnyCancellableCleaning { return fee } - feeWrapper.addDependency(operations: [coderFactoryOperation]) - mappingOperation.addDependency(feeWrapper.targetOperation) + mainFeeOperation.addDependency(coderFactoryOperation) + mappingOperation.addDependency(mainFeeOperation) - return feeWrapper - .insertingHead(operations: [coderFactoryOperation]) - .insertingTail(operation: mappingOperation) + return .init( + targetOperation: mappingOperation, + dependencies: [coderFactoryOperation, mainFeeOperation] + ) } private func createNativeTokenFeeCalculationWrapper( @@ -184,13 +217,9 @@ final class AssetHubFeeService: AnyCancellableCleaning { let resultOperation = ClosureOperation { let feeAmount = try nativeFeeWrapper.targetOperation.extractNoCancellableResultData() - return .init( - totalFee: .init( - targetAmount: feeAmount, - nativeAmount: feeAmount - ), - networkFeeAddition: nil - ) + let model = AssetConversion.AmountWithNative(targetAmount: feeAmount, nativeAmount: feeAmount) + + return .init(totalFee: model, networkFee: model) } resultOperation.addDependency(nativeFeeWrapper.targetOperation) @@ -244,9 +273,9 @@ final class AssetHubFeeService: AnyCancellableCleaning { targetAmount: quotes[0].amountIn, nativeAmount: feeAmount + edAmount ), - networkFeeAddition: .init( + networkFee: .init( targetAmount: quotes[1].amountIn, - nativeAmount: edAmount + nativeAmount: feeAmount ) ) } @@ -280,16 +309,16 @@ final class AssetHubFeeService: AnyCancellableCleaning { ) ) - let edQuoteWrapper = conversionOperationFactory.quote( + let feeQuoteWrapper = conversionOperationFactory.quote( for: .init( assetIn: feeAsset.chainAssetId, assetOut: utilityAsset.chainAssetId, - amount: edAmount, + amount: fee, direction: .buy ) ) - return [feeWithAdditionsQuoteWrapper, edQuoteWrapper] + return [feeWithAdditionsQuoteWrapper, feeQuoteWrapper] }.longrunOperation() } diff --git a/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift b/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift index 712cc119a0..1650f7512b 100644 --- a/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift +++ b/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift @@ -30,10 +30,10 @@ final class AssetHubFeeModelBuilder { let resultModel: AssetConversion.FeeModel - if balance.totalInPlank >= feeModel.totalFee.nativeAmount { + if balance.freeInPlank > 0 { // we have enough tokens for ed - need to exchange only network fee let networkFee = feeModel.networkFee - resultModel = .init(totalFee: networkFee, networkFeeAddition: nil) + resultModel = .init(totalFee: networkFee, networkFee: networkFee) } else { resultModel = feeModel } diff --git a/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift b/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift index 3cc0c11bfc..57be6c2f90 100644 --- a/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift +++ b/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift @@ -20,10 +20,13 @@ struct SwapMaxModel { return false } - let receiveSelfSufficient = receiveAssetExistense?.isSelfSufficient ?? false + guard let receiveAssetExistense = receiveAssetExistense else { + return false + } + let hasConsumers = (accountInfo?.hasConsumers ?? false) - return (!receiveSelfSufficient || hasConsumers) + return (!receiveAssetExistense.isSelfSufficient || hasConsumers) } private func calculateForNativeAsset(_ payChainAsset: ChainAsset, balance: AssetBalance) -> Decimal { diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 1e0432a2fe..6c4033af11 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -250,7 +250,7 @@ extension SwapConfirmPresenter { private func provideNotificationViewModel() { guard - let networkFeeAddition = fee?.networkFeeAddition, + let networkFeeAddition = fee?.networkNativeFeeAddition, !initState.feeChainAsset.isUtilityAsset, let utilityChainAsset = initState.feeChainAsset.chain.utilityChainAsset() else { view?.didReceiveNotification(viewModel: nil) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index b52b3bddc7..f44c0ef43a 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -475,7 +475,7 @@ extension SwapSetupPresenter { private func provideNotification() { guard - let networkFeeAddition = fee?.networkFeeAddition, + let networkFeeAddition = fee?.networkNativeFeeAddition, let feeChainAsset = feeChainAsset, !feeChainAsset.isUtilityAsset, let utilityChainAsset = feeChainAsset.chain.utilityChainAsset() else { diff --git a/novawallet/Modules/Swaps/Validation/SwapModel.swift b/novawallet/Modules/Swaps/Validation/SwapModel.swift index 56f4b7c10a..8c8bb3573f 100644 --- a/novawallet/Modules/Swaps/Validation/SwapModel.swift +++ b/novawallet/Modules/Swaps/Validation/SwapModel.swift @@ -152,7 +152,7 @@ struct SwapModel { if isFeeInPayToken, - let addition = feeModel?.networkFeeAddition, + let addition = feeModel?.networkNativeFeeAddition, let utilityAsset = feeChainAsset.chain.utilityAsset() { return .feeInPayAsset( .init( @@ -235,7 +235,7 @@ struct SwapModel { if isFeeInPayToken, !payChainAsset.isUtilityAsset, let networkFee = feeModel?.networkFee, - let feeAdditions = feeModel?.networkFeeAddition, + let feeAdditions = feeModel?.networkNativeFeeAddition, let utilityAsset = feeChainAsset.chain.utilityAsset() { return .swapAndFee( .init( From 7bab3f15fd467a73d1bf3237a579c09f43346fdc Mon Sep 17 00:00:00 2001 From: ERussel Date: Thu, 9 Nov 2023 17:40:21 +0100 Subject: [PATCH 144/204] fix fee --- .../Modules/Swaps/SwapsValidationTests.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/novawalletTests/Modules/Swaps/SwapsValidationTests.swift b/novawalletTests/Modules/Swaps/SwapsValidationTests.swift index 4bae078c13..e09e28e3db 100644 --- a/novawalletTests/Modules/Swaps/SwapsValidationTests.swift +++ b/novawalletTests/Modules/Swaps/SwapsValidationTests.swift @@ -34,11 +34,11 @@ final class SwapsValidationTests: XCTestCase { feeModel: .init( totalFee: .init( targetAmount: fee + existentialDepositInFeeToken, - nativeAmount: (fee + existentialDepositInFeeToken) / 100 + nativeAmount: (fee + existentialDeposit) / 100 ), - networkFeeAddition: .init( - targetAmount: existentialDepositInFeeToken, - nativeAmount: existentialDeposit + networkFee: .init( + targetAmount: fee, + nativeAmount: fee / 100 ) ), payAssetExistense: nil, @@ -78,7 +78,10 @@ final class SwapsValidationTests: XCTestCase { targetAmount: fee, nativeAmount: fee ), - networkFeeAddition: nil + networkFee: .init( + targetAmount: fee, + nativeAmount: fee + ) ), payAssetExistense: nil, receiveAssetExistense: nil, From bff4ef78bb03809b1565964e467e8bbd03c39c9b Mon Sep 17 00:00:00 2001 From: ERussel Date: Thu, 9 Nov 2023 18:14:35 +0100 Subject: [PATCH 145/204] fix fee threshold --- .../Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift b/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift index 1650f7512b..92b3e72a7d 100644 --- a/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift +++ b/novawallet/Modules/Swaps/Base/Model/AssetHubFeeModelBuilder.swift @@ -30,7 +30,7 @@ final class AssetHubFeeModelBuilder { let resultModel: AssetConversion.FeeModel - if balance.freeInPlank > 0 { + if balance.freeInPlank >= feeModel.totalFee.nativeAmount { // we have enough tokens for ed - need to exchange only network fee let networkFee = feeModel.networkFee resultModel = .init(totalFee: networkFee, networkFee: networkFee) From 9e1c3cfdca611e70dc7bec08788dbf6157707c0a Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 10 Nov 2023 00:45:30 +0300 Subject: [PATCH 146/204] fixes --- .../Swaps/Base/ShortTextInfoPresentableExtensions.swift | 2 +- novawallet/Modules/Swaps/Base/View/SwapNetworkFeeView.swift | 1 + .../Modules/Swaps/Confirm/View/SwapConfirmViewLayout.swift | 2 +- .../Modules/Swaps/Setup/View/SwapAmountInputView.swift | 5 +++-- novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift | 1 + 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/novawallet/Modules/Swaps/Base/ShortTextInfoPresentableExtensions.swift b/novawallet/Modules/Swaps/Base/ShortTextInfoPresentableExtensions.swift index 872e23342a..e00895e039 100644 --- a/novawallet/Modules/Swaps/Base/ShortTextInfoPresentableExtensions.swift +++ b/novawallet/Modules/Swaps/Base/ShortTextInfoPresentableExtensions.swift @@ -3,7 +3,7 @@ import SoraFoundation extension ShortTextInfoPresentable { func showFeeInfo(from view: ControllerBackedProtocol?) { let title = LocalizableResource { - R.string.localizable.commonNetwork( + R.string.localizable.commonNetworkFee( preferredLanguages: $0.rLanguages ) } diff --git a/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeView.swift b/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeView.swift index ca9bafde49..425d5a89c9 100644 --- a/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeView.swift +++ b/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeView.swift @@ -56,6 +56,7 @@ final class SwapNetworkFeeView: GenericTitleValueView CGRect { - let estimatedInputViewWidth = bounds.maxX - contentInsets.right - assetControlFrame.maxX - horizontalSpacing + let estimatedInputViewWidth = bounds.width - contentInsets.left - assetControlFrame.width - contentInsets.right - horizontalSpacing + let inputWidth = max(estimatedInputViewWidth, 0) let inputSize = textInputView.intrinsicContentSize return CGRect( - x: bounds.maxX - contentInsets.right - inputWidth, + x: assetControlFrame.maxX + horizontalSpacing, y: bounds.midY - inputSize.height / 2.0, width: inputWidth, height: inputSize.height diff --git a/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift b/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift index b01e9704be..9743db8053 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift @@ -10,6 +10,7 @@ final class SwapDetailsView: CollapsableContainerView { let networkFeeCell: SwapNetworkFeeViewCell = .create { $0.contentInsets = .init(top: 8, left: 16, bottom: 8, right: 16) + $0.borderView.borderType = .none } override var rows: [UIView] { From ea19fcd073a70e65b8a5df7a7dd3323e9f7fe9f4 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 10 Nov 2023 01:15:35 +0300 Subject: [PATCH 147/204] confirm fixes --- .../Common/Extension/Foundation/String+Helpers.swift | 4 ++++ .../Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift | 7 +++++-- novawallet/Modules/Swaps/Setup/Model/SwapViewModels.swift | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/novawallet/Common/Extension/Foundation/String+Helpers.swift b/novawallet/Common/Extension/Foundation/String+Helpers.swift index 70211bed9e..d5b78d1a0a 100644 --- a/novawallet/Common/Extension/Foundation/String+Helpers.swift +++ b/novawallet/Common/Extension/Foundation/String+Helpers.swift @@ -49,6 +49,10 @@ extension String { func estimatedEqual(to other: String) -> String { "\(self) ≈ \(other)" } + + func approximately() -> String { + "~\(self)" + } } extension Optional where Wrapped == String { diff --git a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift index 2851f779a3..495dd685e9 100644 --- a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift @@ -61,11 +61,14 @@ extension SwapConfirmViewModelFactory: SwapConfirmViewModelFactoryProtocol { amount: amountDecimal, priceData: priceData ).value(for: locale) - + let price = balanceViewModel.price.map { $0.approximately() } return .init( imageViewModel: assetIcon, hub: networkViewModel, - balance: balanceViewModel + balance: BalanceViewModel( + amount: balanceViewModel.amount, + price: price + ) ) } diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapViewModels.swift b/novawallet/Modules/Swaps/Setup/Model/SwapViewModels.swift index 1a362818dd..ed061e0a90 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapViewModels.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapViewModels.swift @@ -33,8 +33,8 @@ struct SwapPriceDifferenceViewModel { } enum FeeSelectionViewModel: Int, CaseIterable { - case payAsset case utilityAsset + case payAsset } extension FeeSelectionViewModel { From b8a316ea92a3ea940f0c130e72469a5268981316 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 10 Nov 2023 12:40:38 +0300 Subject: [PATCH 148/204] fix toolbar, price difference --- novawallet/Common/View/UIFactory.swift | 61 ++++++++++++++----- .../Base/Model/SwapBaseViewModelFactory.swift | 6 +- .../Model/SwapsSetupViewModelFactory.swift | 4 ++ .../Swaps/Setup/View/SwapAssetControl.swift | 8 ++- 4 files changed, 61 insertions(+), 18 deletions(-) diff --git a/novawallet/Common/View/UIFactory.swift b/novawallet/Common/View/UIFactory.swift index 588b41de3d..eef8382329 100644 --- a/novawallet/Common/View/UIFactory.swift +++ b/novawallet/Common/View/UIFactory.swift @@ -332,24 +332,24 @@ final class UIFactory: UIFactoryProtocol { private func createActionsAccessoryView( for toolBar: UIToolbar, + style: ToolBarStyle = .defaultSyle, actions: [ViewSelectorAction], doneAction: ViewSelectorAction, target: Any?, spacing: CGFloat ) -> UIToolbar { - toolBar.isTranslucent = false + toolBar.isTranslucent = style.isTranslucent + + if let backgroundColor = style.backgroundColor { + let background = UIImage.background(from: backgroundColor) + toolBar.setBackgroundImage( + background, + forToolbarPosition: .any, + barMetrics: .default + ) + } - let background = UIImage.background(from: .clear) - toolBar.setBackgroundImage( - background, - forToolbarPosition: .any, - barMetrics: .default - ) - - let actionAttributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: R.color.colorTextPrimary()!, - .font: UIFont.p1Paragraph - ] + let actionAttributes = style.actionAttributes let barItems = actions.reduce([UIBarButtonItem]()) { result, action in let barItem = UIBarButtonItem( @@ -388,10 +388,7 @@ final class UIFactory: UIFactoryProtocol { action: doneAction.selector ) - let doneAttributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: R.color.colorTextPrimary()!, - .font: UIFont.h5Title - ] + let doneAttributes = style.doneAttributes doneItem.setTitleTextAttributes(doneAttributes, for: .normal) doneItem.setTitleTextAttributes(doneAttributes, for: .highlighted) @@ -569,9 +566,19 @@ extension UIFactory { title: doneTitle, selector: selector ) + let toolbarStyle = ToolBarStyle( + backgroundColor: nil, + isTranslucent: true, + doneAttributes: [ + .foregroundColor: R.color.colorButtonTextAccent()!, + .font: UIFont.p0Paragraph + ], + actionAttributes: [:] + ) return createActionsAccessoryView( for: toolBar, + style: toolbarStyle, actions: [], doneAction: doneAction, target: target, @@ -579,3 +586,25 @@ extension UIFactory { ) } } + +extension UIFactory { + struct ToolBarStyle { + let backgroundColor: UIColor? + let isTranslucent: Bool + let doneAttributes: [NSAttributedString.Key: Any] + let actionAttributes: [NSAttributedString.Key: Any] + + static var defaultSyle = ToolBarStyle( + backgroundColor: .clear, + isTranslucent: false, + doneAttributes: [ + .foregroundColor: R.color.colorTextPrimary()!, + .font: UIFont.h5Title + ], + actionAttributes: [ + .foregroundColor: R.color.colorTextPrimary()!, + .font: UIFont.p1Paragraph + ] + ) + } +} diff --git a/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift b/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift index 0f3421b005..13b4b60d03 100644 --- a/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift @@ -42,6 +42,10 @@ class SwapBaseViewModelFactory { self.percentForamatter = percentForamatter self.priceDifferenceConfig = priceDifferenceConfig } + + func formatPriceDifference(amount: Decimal, locale: Locale) -> String { + percentForamatter.value(for: locale).stringFromDecimal(amount) ?? "" + } } extension SwapBaseViewModelFactory: SwapBaseViewModelFactoryProtocol { @@ -139,7 +143,7 @@ extension SwapBaseViewModelFactory: SwapBaseViewModelFactoryProtocol { } let diff = abs(amountPriceIn - amountPriceOut) / amountPriceIn - let diffString = percentForamatter.value(for: locale).stringFromDecimal(diff)?.inParenthesis() ?? "" + let diffString = formatPriceDifference(amount: diff, locale: locale) switch diff { case _ where diff > priceDifferenceConfig.warningMax: diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift index f2dd4476ed..a388cff814 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapsSetupViewModelFactory.swift @@ -106,6 +106,10 @@ final class SwapsSetupViewModelFactory: SwapBaseViewModelFactory { subtitle: R.string.localizable.swapsSetupAssetSelectSubtitle(preferredLanguages: locale.rLanguages) ) } + + override func formatPriceDifference(amount: Decimal, locale: Locale) -> String { + percentForamatter.value(for: locale).stringFromDecimal(amount)?.inParenthesis() ?? "" + } } extension SwapsSetupViewModelFactory: SwapsSetupViewModelFactoryProtocol { diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift b/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift index b643d027c5..8a4e70aaa0 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAssetControl.swift @@ -44,7 +44,13 @@ final class SwapAssetControl: BackgroundedContentControl { assetView.intrinsicContentSize.height ) - let iconWidth = lazyIconView.map { $0.intrinsicContentSize.width + horizontalSpacing } ?? 0 + let iconWidth: CGFloat + if lazyIconView == nil { + iconWidth = 0 + } else { + iconWidth = 2 * iconRadius + horizontalSpacing + } + let contentWidth = iconWidth + assetView.intrinsicContentSize.width let height = contentInsets.top + contentHeight + contentInsets.bottom From c2092f4427ab7b08df80f07bf28ff0d9c277bab4 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 10 Nov 2023 12:48:34 +0300 Subject: [PATCH 149/204] remove toolbar style --- novawallet/Common/View/UIFactory.swift | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/novawallet/Common/View/UIFactory.swift b/novawallet/Common/View/UIFactory.swift index eef8382329..e437447b6d 100644 --- a/novawallet/Common/View/UIFactory.swift +++ b/novawallet/Common/View/UIFactory.swift @@ -566,19 +566,8 @@ extension UIFactory { title: doneTitle, selector: selector ) - let toolbarStyle = ToolBarStyle( - backgroundColor: nil, - isTranslucent: true, - doneAttributes: [ - .foregroundColor: R.color.colorButtonTextAccent()!, - .font: UIFont.p0Paragraph - ], - actionAttributes: [:] - ) - return createActionsAccessoryView( for: toolBar, - style: toolbarStyle, actions: [], doneAction: doneAction, target: target, From 27d8dc36805c8767497b1a6c598997f3db149c74 Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 10 Nov 2023 11:18:28 +0100 Subject: [PATCH 150/204] refactor --- .../TransferSetup/TransferSetupViewFactory.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift index a82502e478..40cf09e92f 100644 --- a/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift @@ -9,7 +9,11 @@ struct TransferSetupViewFactory { recepient: DisplayAddress?, transferCompletion: TransferCompletionClosure? = nil ) -> TransferSetupViewProtocol? { - createView(from: chainAsset, recepient: recepient, transferCompletion: transferCompletion) { factory, state, view in + createView( + from: chainAsset, + recepient: recepient, + transferCompletion: transferCompletion + ) { factory, state, view in factory.createOnChainPresenter(for: chainAsset, initialState: state, view: view) } } @@ -21,7 +25,11 @@ struct TransferSetupViewFactory { recepient: DisplayAddress?, transferCompletion: TransferCompletionClosure? = nil ) -> TransferSetupViewProtocol? { - createView(from: chainAsset, recepient: recepient, transferCompletion: transferCompletion) { factory, state, view in + createView( + from: chainAsset, + recepient: recepient, + transferCompletion: transferCompletion + ) { factory, state, view in factory.createCrossChainPresenter( for: chainAsset, destinationChainAsset: destinationChainAsset, From 3a62b804b60f3128d0cf1e0bb3ea9083779b470c Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 10 Nov 2023 13:53:25 +0300 Subject: [PATCH 151/204] fix fee cell tap area --- novawallet/Modules/Swaps/Base/View/SwapNetworkFeeViewCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeViewCell.swift b/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeViewCell.swift index d60a5c5816..6b7ef4cc54 100644 --- a/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeViewCell.swift +++ b/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeViewCell.swift @@ -11,7 +11,7 @@ final class SwapNetworkFeeViewCell: RowView, StackTableViewC override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let pointInContentViewSpace = convert(point, to: rowContentView) - if rowContentView.valueView.frame.contains(pointInContentViewSpace) { + if valueTopButton.isUserInteractionEnabled, rowContentView.valueView.frame.contains(pointInContentViewSpace) { return valueTopButton } else { return super.hitTest(point, with: event) From 592b2de7f852055d8d40dcdd4704adffa04d7827 Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 10 Nov 2023 13:10:07 +0100 Subject: [PATCH 152/204] fix price difference --- .../Base/Model/SwapBaseViewModelFactory.swift | 8 +++++--- .../Base/Model/SwapPriceDifferenceConfig.swift | 10 ++++++---- .../Swaps/Setup/SwapSetupPresenter.swift | 17 +++++++++++++---- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift b/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift index 13b4b60d03..a84880748b 100644 --- a/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift @@ -146,12 +146,14 @@ extension SwapBaseViewModelFactory: SwapBaseViewModelFactoryProtocol { let diffString = formatPriceDifference(amount: diff, locale: locale) switch diff { - case _ where diff > priceDifferenceConfig.warningMax: + case _ where diff >= priceDifferenceConfig.high: return .init(details: diffString, attention: .high) - case priceDifferenceConfig.warningMin ... priceDifferenceConfig.warningMax: + case priceDifferenceConfig.medium ... priceDifferenceConfig.high: return .init(details: diffString, attention: .medium) - default: + case priceDifferenceConfig.low ... priceDifferenceConfig.medium: return .init(details: diffString, attention: .low) + default: + return nil } } } diff --git a/novawallet/Modules/Swaps/Base/Model/SwapPriceDifferenceConfig.swift b/novawallet/Modules/Swaps/Base/Model/SwapPriceDifferenceConfig.swift index a0c2236663..a401b8fc36 100644 --- a/novawallet/Modules/Swaps/Base/Model/SwapPriceDifferenceConfig.swift +++ b/novawallet/Modules/Swaps/Base/Model/SwapPriceDifferenceConfig.swift @@ -1,15 +1,17 @@ import Foundation struct SwapPriceDifferenceConfig { - let warningMin: Decimal - let warningMax: Decimal + let high: Decimal + let medium: Decimal + let low: Decimal } extension SwapPriceDifferenceConfig { static var defaultConfig: SwapPriceDifferenceConfig { .init( - warningMin: 0.1, - warningMax: 0.2 + high: 0.15, + medium: 0.05, + low: 0.01 ) } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index f44c0ef43a..bea55ae077 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -167,6 +167,8 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { } payAmountInput = payAmount.map { .absolute($0) } providePayAmountInputViewModel() + providePayInputPriceViewModel() + provideReceiveInputPriceViewModel() case .sell: receiveAmountInput = receiveChainAsset.map { Decimal.fromSubstrateAmount( @@ -176,6 +178,7 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { } provideReceiveAmountInputViewModel() provideReceiveInputPriceViewModel() + providePayInputPriceViewModel() } provideRateViewModel() @@ -192,8 +195,8 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { provideFeeViewModel() if case .rate = payAmountInput { - providePayInputPriceViewModel() providePayAmountInputViewModel() + providePayInputPriceViewModel() // as fee changes the max amount we might also refresh the quote refreshQuote(direction: quoteArgs?.direction ?? .sell, forceUpdate: false) @@ -664,6 +667,8 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { func updatePayAmount(_ amount: Decimal?) { payAmountInput = amount.map { .absolute($0) } refreshQuote(direction: .sell) + providePayInputPriceViewModel() + provideReceiveInputPriceViewModel() provideButtonState() provideIssues() provideNotification() @@ -672,6 +677,8 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { func updateReceiveAmount(_ amount: Decimal?) { receiveAmountInput = amount refreshQuote(direction: .buy) + provideReceiveInputPriceViewModel() + providePayInputPriceViewModel() provideButtonState() provideIssues() provideNotification() @@ -697,15 +704,17 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { newFocus = nil } - switch quoteArgs?.direction { + let previousDirection = quoteArgs?.direction + + switch previousDirection { case .sell: receiveAmountInput = payAmount payAmountInput = nil - refreshQuote(direction: .buy, forceUpdate: false) + refreshQuote(direction: .buy, forceUpdate: true) case .buy: payAmountInput = receiveAmount receiveAmountInput = nil - refreshQuote(direction: .sell, forceUpdate: false) + refreshQuote(direction: .sell, forceUpdate: true) case .none: payAmountInput = nil receiveAmountInput = nil From da4f6ceaa55b0db1f2ccf14db980d28f506f4a0e Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 10 Nov 2023 15:07:25 +0100 Subject: [PATCH 153/204] add modules --- novawallet.xcodeproj/project.pbxproj | 28 +++++++++++++++++++ .../GetTokenOptionsInteractor.swift | 1 + .../GetTokenOptionsPresenter.swift | 9 ++++++ .../GetTokenOptionsProtocols.swift | 5 ++++ .../GetTokenOptionsViewController.swift | 8 ++++++ .../GetTokenOptionsViewFactory.swift | 1 + 6 files changed, 52 insertions(+) create mode 100644 novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsInteractor.swift create mode 100644 novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsPresenter.swift create mode 100644 novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsProtocols.swift create mode 100644 novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewController.swift create mode 100644 novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewFactory.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 7b085b254e..43b5d9d783 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -227,6 +227,11 @@ 0C9D87AE2AC708070095FE8C /* AssetHubTokensConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9D87AD2AC708070095FE8C /* AssetHubTokensConverter.swift */; }; 0C9ECB5A2A4A9AB400BDCA73 /* AssetListAccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9ECB592A4A9AB400BDCA73 /* AssetListAccountCell.swift */; }; 0CA307BC2F570941CD22C9AA /* ExportMnemonicConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4688AF0658F8BB7A90C2BE /* ExportMnemonicConfirmViewFactory.swift */; }; + 0CA50CAF2AFE6094005668CD /* GetTokenOptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA50CAE2AFE6094005668CD /* GetTokenOptionsViewController.swift */; }; + 0CA50CB12AFE6F15005668CD /* GetTokenOptionsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA50CB02AFE6F15005668CD /* GetTokenOptionsProtocols.swift */; }; + 0CA50CB32AFE6F23005668CD /* GetTokenOptionsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA50CB22AFE6F23005668CD /* GetTokenOptionsPresenter.swift */; }; + 0CA50CB52AFE6F31005668CD /* GetTokenOptionsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA50CB42AFE6F31005668CD /* GetTokenOptionsInteractor.swift */; }; + 0CA50CB72AFE6F40005668CD /* GetTokenOptionsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA50CB62AFE6F40005668CD /* GetTokenOptionsViewFactory.swift */; }; 0CAC01552A52E0CC0069413E /* AssetListModelHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAC01542A52E0CC0069413E /* AssetListModelHelpers.swift */; }; 0CAC01572A52E1960069413E /* AssetListPresenterHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAC01562A52E1960069413E /* AssetListPresenterHelpers.swift */; }; 0CAC44AA2A7A79C2001EDE61 /* StartStakingInfoParachainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAC44A92A7A79C2001EDE61 /* StartStakingInfoParachainWireframe.swift */; }; @@ -4295,6 +4300,11 @@ 0C9C64392A8DF97E004DC078 /* StakingNPoolsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingNPoolsError.swift; sourceTree = ""; }; 0C9D87AD2AC708070095FE8C /* AssetHubTokensConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHubTokensConverter.swift; sourceTree = ""; }; 0C9ECB592A4A9AB400BDCA73 /* AssetListAccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListAccountCell.swift; sourceTree = ""; }; + 0CA50CAE2AFE6094005668CD /* GetTokenOptionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsViewController.swift; sourceTree = ""; }; + 0CA50CB02AFE6F15005668CD /* GetTokenOptionsProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsProtocols.swift; sourceTree = ""; }; + 0CA50CB22AFE6F23005668CD /* GetTokenOptionsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsPresenter.swift; sourceTree = ""; }; + 0CA50CB42AFE6F31005668CD /* GetTokenOptionsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsInteractor.swift; sourceTree = ""; }; + 0CA50CB62AFE6F40005668CD /* GetTokenOptionsViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsViewFactory.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 = ""; }; 0CAC44A92A7A79C2001EDE61 /* StartStakingInfoParachainWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoParachainWireframe.swift; sourceTree = ""; }; @@ -8732,6 +8742,18 @@ path = ViewModel; sourceTree = ""; }; + 0CA50CAD2AFE602F005668CD /* GetTokenOptions */ = { + isa = PBXGroup; + children = ( + 0CA50CAE2AFE6094005668CD /* GetTokenOptionsViewController.swift */, + 0CA50CB02AFE6F15005668CD /* GetTokenOptionsProtocols.swift */, + 0CA50CB22AFE6F23005668CD /* GetTokenOptionsPresenter.swift */, + 0CA50CB42AFE6F31005668CD /* GetTokenOptionsInteractor.swift */, + 0CA50CB62AFE6F40005668CD /* GetTokenOptionsViewFactory.swift */, + ); + path = GetTokenOptions; + sourceTree = ""; + }; 0CB261D52A97A4D300287305 /* NominationPools */ = { isa = PBXGroup; children = ( @@ -9108,6 +9130,7 @@ 29BD7DA0076BA8BC3411221A /* Setup */ = { isa = PBXGroup; children = ( + 0CA50CAD2AFE602F005668CD /* GetTokenOptions */, 77E304B32AEFC2E2006FD6F0 /* NetworkFeeBottomSheet */, 77C9BCBA2ACD1AE800022EA2 /* Model */, 774091FA2ACC052400172516 /* View */, @@ -19592,6 +19615,7 @@ 88D02FE52942EA7800E26390 /* AssetDetailsStyles.swift in Sources */, 84CAC1CC298A715900F78169 /* GovernanceOffchainVotesLocal.swift in Sources */, 84F2CEFC29A36CE9003EBE36 /* WalletNoAccountHandling.swift in Sources */, + 0CA50CB32AFE6F23005668CD /* GetTokenOptionsPresenter.swift in Sources */, 774A481129F8BFB70094635B /* OperationAuthPresentable.swift in Sources */, 849014C324AA87E4008F705E /* LocalAuthInteractor.swift in Sources */, 84BEE059255F4D5700D05EB3 /* AccountImportPreferredInfo.swift in Sources */, @@ -21148,6 +21172,7 @@ 8846F74029D757FE00B8B776 /* Data+base64.swift in Sources */, 84EC2D16276B92CB009B0BE1 /* DAppScriptResponse.swift in Sources */, F43A596A26D520E0005E973D /* EmptyStateViewCell.swift in Sources */, + 0CA50CAF2AFE6094005668CD /* GetTokenOptionsViewController.swift in Sources */, 841E556D282EC50700C8438F /* ParaStkStateMachine.swift in Sources */, 8846F73329D6BEEA00B8B776 /* Data+base64url.swift in Sources */, 8487584927F1830D00495306 /* QRImageUploadDelegate.swift in Sources */, @@ -21452,6 +21477,7 @@ 845B89262959627A00EE25B0 /* SecurityLayerWireframe.swift in Sources */, 777AE2AB2ABCB4A5004989C0 /* HtmlParsingOperationFactory.swift in Sources */, 8489A6D227FD5FB80040C066 /* StackActionCell.swift in Sources */, + 0CA50CB52AFE6F31005668CD /* GetTokenOptionsInteractor.swift in Sources */, F429324F26280F6B00752C2C /* StakingRewardDetailsViewModel.swift in Sources */, F4A11B5A272FEB0B0030E85B /* CrowdloanYourContributionsCell.swift in Sources */, 0C79C8992A7BE46A00B171E3 /* AssetModel+Staking.swift in Sources */, @@ -22401,6 +22427,7 @@ 84E25BE827E751B400290BF1 /* Charset+Encoding.swift in Sources */, 847999B12888A4FF00D1BAD2 /* SwitchAccount+CreateWatchOnlyWireframe.swift in Sources */, 0C3205D42A895EDA002EB914 /* EvmConstantGasLimitProvider.swift in Sources */, + 0CA50CB72AFE6F40005668CD /* GetTokenOptionsViewFactory.swift in Sources */, 0C3205DC2A89677B002EB914 /* EvmGasLimitProviderFactory.swift in Sources */, 8487580727EDEB9600495306 /* BorderedIconLabelView.swift in Sources */, 0C6F0C9E2A69723B007170C6 /* StartStakingStateProtocol.swift in Sources */, @@ -23202,6 +23229,7 @@ 2EDE38E0F2E3494D16717A74 /* WalletConnectInteractor.swift in Sources */, EB11BF594D7E16A8885D47DD /* WalletConnectServiceFactory.swift in Sources */, C8171AF2893A4723F4F63E23 /* WalletConnectSessionsProtocols.swift in Sources */, + 0CA50CB12AFE6F15005668CD /* GetTokenOptionsProtocols.swift in Sources */, 0C59E8D52AA5FC96001E11F3 /* ExternalAssetBalanceMapper.swift in Sources */, 842E9EA42A2DC71000759972 /* StakingDashboardModel.swift in Sources */, AB5EA0348C8E8C40FCA9DC86 /* WalletConnectSessionsWireframe.swift in Sources */, diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsInteractor.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsInteractor.swift new file mode 100644 index 0000000000..fecc4ab449 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsInteractor.swift @@ -0,0 +1 @@ +import Foundation diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsPresenter.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsPresenter.swift new file mode 100644 index 0000000000..0b2c529c4e --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsPresenter.swift @@ -0,0 +1,9 @@ +// +// GetTokenOptionsPresenter.swift +// novawallet +// +// Created by Ruslan Rezin on 10.11.2023. +// Copyright © 2023 Nova Foundation. All rights reserved. +// + +import Foundation diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsProtocols.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsProtocols.swift new file mode 100644 index 0000000000..e87f56fc47 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsProtocols.swift @@ -0,0 +1,5 @@ +import Foundation + +protocol GetTokenOptionsPresenterProtocol: AnyObject { + +} diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewController.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewController.swift new file mode 100644 index 0000000000..92ff7d9d8b --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewController.swift @@ -0,0 +1,8 @@ +import UIKit + +final class GetTokenOptionsViewController: ModalPickerViewController< + TokenOperationTableViewCell, + TokenOperationTableViewCell.Model +> { + +} diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewFactory.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewFactory.swift new file mode 100644 index 0000000000..fecc4ab449 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewFactory.swift @@ -0,0 +1 @@ +import Foundation From 820f8a9b2cdc939c3a054fd543f626daa7287855 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 10 Nov 2023 20:51:18 +0300 Subject: [PATCH 154/204] PR fixes --- .../iconSwap.imageset/flip-swap.pdf | Bin 2338 -> 2381 bytes .../Subquery/Filter/SubqueryFilter.swift | 43 ++++++++++++++++ .../SubqueryHistoryOperationFactory.swift | 48 ++++++++++++++++-- .../Model/OperationSwapModel.swift | 2 +- .../OperationDetailsSwapProvider.swift | 4 +- .../View/OperationDetailsSwapView.swift | 5 +- .../OperationDetailsViewModelFactory.swift | 17 +++---- .../ViewModel/OperationSwapViewModel.swift | 2 +- .../Service/AssetHistoryFactoryFacade.swift | 4 +- 9 files changed, 103 insertions(+), 22 deletions(-) diff --git a/novawallet/Assets.xcassets/iconSwap.imageset/flip-swap.pdf b/novawallet/Assets.xcassets/iconSwap.imageset/flip-swap.pdf index 6bf538326dceb9c1ef3264df1a1d37ac8f1eadfe..912575ba45737dbb7fdcb2d2c6195abd01ca0d03 100644 GIT binary patch literal 2381 zcmbW3Pj4GH5XJBNDR?oE972)9|3F}%v7Ml3i@I`eK@Y0Daa`C^D=CHBPoLj#Wm$%M z2)YM*^~4#@ym^n*$>sU`H{O`Yuh2k`1YZ;LP&=De|!F@UmU52Ctud49$3(qa;a& zm;>l+EmaENIvb@$$y6XQHxbipZZ0OR$)^}nHp`*eSgVgoF&5Wk?uKfMv?irvEl4oc zdY3}0AjQ{~Dv0wT6i*c~h3p!+z?{!fv7Fek^<0YiY$RG2JP)AQl)Wp?_-a$7%Fsb4 zF={kYPnfL_SuV53Hh41?8v8lHx^XmoAuIs@CMA& zhIFh^B&?t#7pp5q$Q@fQY04OzW&|vRl-hfN(MAUXLrzkg$Z9#q7K}72EQ}GJsk%mj zfok%+F%7=Ufo0X!P)cRi11Ll^MhFUFwPm)^CZLCMb#upzqoOnsi>EbE?`y4O?O3UB z#v2LfqDM=d7)mv`qGJZ7;7&$oxB6s!fnHX2+R$N$L}v($sdmcYSKA>v+ZbF|RVO%y zUgsn_GU;r6tfgq32Pq!(c{h9>J`Jnkmu?UFU-rn*7{0+CK}$vDP~xC8m8AXvZ6y`# zNO=-O-4KeczV+dNVHyYzh{|CS(P-rk?JoyVVi{V`S#xqakkC|LEkLAtr$rYGnCgpx zH5<^JtD2HBJdRXbF*YSsGbSfl08@@kTNA1pD2U^a{Svt+&;>!CfEb|qE;Kwyw945z z#D}}s#d#@_Wq1g0vI}4gxm7hXvuCa(L_IOO%3QT8eAb7CWK*o-s2hpl2UQeMAfvWN zry;8HAh)VB59G3qgIq{~@`+r}O0VR$0A&-o90nqgTTrP+E4iqMXbyuSh_mO!&)vL~ zjv|$pWF?2fp+)HvsHuw0QaJ~nqZN}$%H3on%|XLfBqcJ>~e84 z?t!1i>)XY<^@I7Svg*&%OFTfxx$#ixW2X7IU4Pju$F0#zdcIG0wqEVV6+7zv`3PPv zHeiM$yF*8}tU^!s`wzF*lxnnt`C*^4^%r9Oi~6@9N13?XjxuS3+rx(ZBG@;6w%9H1 z);G^XA2#Es!I?5l?dxBYQ^L-``v>{;H?w+#Jd%12d6fJDN{mULLm&dD*5{DdUk7_= zw^`iYjhjJ#@O*y`9j~_Q`|-7Tb$k7KB4svTuXjcp?gY=SZvP#rzW*#)Y<9gJLEgj3 J$;Hp_zXAXp?Hd39 literal 2338 zcmbVOO>Y}F5WVlO;Ke|4$ci)MkV7Ca(AZ8;v_)OHx1a}A-Z(03Z7XdGx4%AbNPRgj zkf3{zsJHX^W`?7)>&v&VsLx$T&b#sVKRV}Lzjm{$$LZb8w3vqWi|f~N;d~$1<8kNb zkbL&cuIO7N&(BSNSl^n%j-Pn`aai9?A6)=AH>vHk85R%S>~i>fwH)T}-nfgK(_hPN z_g80r@3tNF78|1T`@^fl>Es@7EsQVf?Ia6&oRPS#VBxg_rOAmCB=%xR5iVDDg{W80tmLe zPf5*IW<*}9Eo6d-fGKN?rP?ygt0V3`i9`>OdR8w%TIk>X>? zhImgo1L2Yqj6si)r6mB2qGn%hCAfes<2h>K0#H&E@y;M5KoL+;&LzPb2}d6s!A6eC z+{|$`2QWw~Io2fLqlh>HHA1Xh!Ki^j4!}f3C4&?T7~C5aS)`x}c%@XOBB^G96O=}M zNnGuL%@AM@Ie`?DG|xIm@v z8Uf`*2;-|GKo(~KOCUJJtl$zl+M{%eY6~iotmOd}Uj(cJLdRel37Pp2#Z)6nfYh=x zP=(OOW-oBCIRGeVqm%(6?WC}04OI%p1QkYWfha(TK%iRZCgNSQm%$?eM7waQZEtmt zl5Id(bL%aFZA$1vfSw9sw5~v@p$=q6vg00?Q#4Ot4B5sHM|Lgk=x3Kt%nDp$5Dr&SJ|mZXpar#L#o zMEjtivT-Wbf(9xkvf0DYMiU{!eemLN;RV9`BZ%3h&?`k;2~W|8DN3B`6v0qv^CUzi zR=Sc%F1TZmofzAVn~YMmAw#8nbB}op3B|0kc7RR9j8SfuI7{J%l^D=Uh^Tl)Jhnv4bC#%z-in!cS zIeQMa*kcU$(6m{s?uSih|6;zs1RXE7xDq_OS^Ya8`ubmE Rv6*T*SO%#(JG=V%?PpdK: SubqueryFilter { } } +struct SubqueryValueFilter: SubqueryFilter { + let fieldName: String + let value: T + + func rawSubqueryFilter() -> String { + "\(fieldName): \(value.rawSubqueryFilter())" + } +} + struct SubqueryIsNotNullFilter: SubqueryFilter { let fieldName: String @@ -43,6 +52,40 @@ struct SubqueryIsNotNullFilter: SubqueryFilter { } } +struct SubqueryNotFilter: SubqueryFilter { + let fieldName: String + let inner: SubqueryFilter + + func rawSubqueryFilter() -> String { + "not: { \(fieldName): { \(inner.rawSubqueryFilter()) }}" + } +} + +struct SubqueryContainsFilter: SubqueryFilter { + let fieldName: String + let inner: SubqueryFilter + + func rawSubqueryFilter() -> String { + "\(fieldName): { contains: { \(inner.rawSubqueryFilter()) } }" + } +} + +struct SubqueryContainsKeyFilter: SubqueryFilter { + let fieldName: String + + func rawSubqueryFilter() -> String { + "containsKey: \(fieldName.rawSubqueryFilter())" + } +} + +struct SubqueryIsNullFilter: SubqueryFilter { + let fieldName: String + + func rawSubqueryFilter() -> String { + "\(fieldName): { isNull: true }" + } +} + extension String: SubqueryFilterValue { func rawSubqueryFilter() -> String { "\"\(self)\"" diff --git a/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift b/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift index df39127ffa..a9107f4a15 100644 --- a/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift +++ b/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift @@ -16,13 +16,22 @@ final class SubqueryHistoryOperationFactory { let assetId: String? let hasPoolStaking: Bool let hasSwaps: Bool - - init(url: URL, filter: WalletHistoryFilter, assetId: String?, hasPoolStaking: Bool, hasSwaps: Bool) { + let isUtilityAsset: Bool + + init( + url: URL, + filter: WalletHistoryFilter, + assetId: String?, + isUtilityAsset: Bool, + hasPoolStaking: Bool, + hasSwaps: Bool + ) { self.url = url self.filter = filter self.assetId = assetId self.hasPoolStaking = hasPoolStaking self.hasSwaps = hasSwaps + self.isUtilityAsset = isUtilityAsset } private func prepareExtrinsicInclusionFilter() -> String { @@ -61,6 +70,37 @@ final class SubqueryHistoryOperationFactory { """ } + private func prepareSwapAssetIdFilter(_ assetId: String?) -> String { + let filters: [SubqueryFilter] + if let assetId = assetId { + filters = [ + SubqueryContainsFilter( + fieldName: "swap", + inner: SubqueryValueFilter(fieldName: "assetIdIn", value: assetId) + ), + SubqueryContainsFilter( + fieldName: "swap", + inner: SubqueryValueFilter(fieldName: "assetIdOut", value: assetId) + ) + ] + } else { + filters = [ + SubqueryContainsFilter( + fieldName: "swap", + inner: SubqueryIsNullFilter(fieldName: "assetIdIn") + ), + SubqueryContainsFilter( + fieldName: "swap", + inner: SubqueryIsNullFilter(fieldName: "assetIdOut") + ), + SubqueryNotFilter(fieldName: "swap", inner: SubqueryContainsKeyFilter(fieldName: "assetIdIn")), + SubqueryNotFilter(fieldName: "swap", inner: SubqueryContainsKeyFilter(fieldName: "assetIdOut")) + ] + } + + return SubqueryInnerFilter(inner: SubqueryCompoundFilter.or(filters)).rawSubqueryFilter() + } + private func prepareFilter() -> String { var filterStrings: [String] = [] @@ -91,7 +131,9 @@ final class SubqueryHistoryOperationFactory { if filter.contains(.swaps), hasSwaps { if let assetId = assetId { - filterStrings.append(prepareAssetIdFilter(assetId)) + filterStrings.append(prepareSwapAssetIdFilter(assetId)) + } else if isUtilityAsset { + filterStrings.append(prepareSwapAssetIdFilter(nil)) } else { filterStrings.append("{ swap: { isNull: false } }") } diff --git a/novawallet/Modules/OperationDetails/Model/OperationSwapModel.swift b/novawallet/Modules/OperationDetails/Model/OperationSwapModel.swift index fd257617e3..d0c87dafa5 100644 --- a/novawallet/Modules/OperationDetails/Model/OperationSwapModel.swift +++ b/novawallet/Modules/OperationDetails/Model/OperationSwapModel.swift @@ -14,5 +14,5 @@ struct OperationSwapModel { let feePrice: PriceData? let feeAsset: AssetModel let wallet: WalletDisplayAddress - let isOutgoing: Bool + let direction: AssetConversion.Direction } diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift index 7e7205b5d1..1835a6f0df 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift @@ -35,7 +35,7 @@ extension OperationDetailsSwapProvider: OperationDetailsDataProviderProtocol { return } - let isOutgoing = assetIn.assetId == chainAsset.asset.assetId + let direction: AssetConversion.Direction = assetIn.assetId == chainAsset.asset.assetId ? .sell : .buy let timestamp = UInt64(bitPattern: transaction.timestamp) let priceIn = calculatePrice( @@ -72,7 +72,7 @@ extension OperationDetailsSwapProvider: OperationDetailsDataProviderProtocol { feePrice: feePriceData, feeAsset: feeAsset, wallet: wallet, - isOutgoing: isOutgoing + direction: direction ) progressClosure(.swap(model)) } diff --git a/novawallet/Modules/OperationDetails/View/OperationDetailsSwapView.swift b/novawallet/Modules/OperationDetails/View/OperationDetailsSwapView.swift index 014f014050..aa268cd9e1 100644 --- a/novawallet/Modules/OperationDetails/View/OperationDetailsSwapView.swift +++ b/novawallet/Modules/OperationDetails/View/OperationDetailsSwapView.swift @@ -69,10 +69,11 @@ final class OperationDetailsSwapView: LocalizableView { )) transactionHashView.bind(details: viewModel.transactionHash) - if viewModel.isOutgoing { + switch viewModel.direction { + case .sell: pairsView.leftAssetView.valueLabel.textColor = R.color.colorTextPositive() pairsView.rigthAssetView.valueLabel.textColor = R.color.colorTextPrimary() - } else { + case .buy: pairsView.leftAssetView.valueLabel.textColor = R.color.colorTextPrimary() pairsView.rigthAssetView.valueLabel.textColor = R.color.colorTextPositive() } diff --git a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift index c1e947a62c..219b009686 100644 --- a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift +++ b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift @@ -101,12 +101,13 @@ final class OperationDetailsViewModelFactory { priceData = model.priceData prefix = "-" case let .swap(model): - if model.isOutgoing { + switch model.direction { + case .sell: amount = model.amountIn priceData = model.priceIn prefix = "-" precision = model.assetIn.displayInfo.assetPrecision - } else { + case .buy: amount = model.amountOut priceData = model.priceOut prefix = "+" @@ -264,7 +265,7 @@ final class OperationDetailsViewModelFactory { let walletViewModel = try? walletViewModelFactory.createViewModel(from: model.wallet) return OperationSwapViewModel( - isOutgoing: model.isOutgoing, + direction: model.direction, assetIn: assetInViewModel, assetOut: assetOutViewModel, rate: rateViewModel, @@ -314,19 +315,13 @@ final class OperationDetailsViewModelFactory { amountInDecimal != 0 else { return "" } - let difference = amountOutDecimal / amountInDecimal - let amountIn = balanceViewModelFactoryFacade.amountFromValue( - targetAssetInfo: params.assetDisplayInfoIn, - value: 1 - ).value(for: locale) - let amountOut = balanceViewModelFactoryFacade.amountFromValue( + return balanceViewModelFactoryFacade.rateFromValue( + mainSymbol: params.assetDisplayInfoIn.symbol, targetAssetInfo: params.assetDisplayInfoOut, value: difference ).value(for: locale) - - return "\(amountIn) ≈ \(amountOut)" } private func createContentViewModel( diff --git a/novawallet/Modules/OperationDetails/ViewModel/OperationSwapViewModel.swift b/novawallet/Modules/OperationDetails/ViewModel/OperationSwapViewModel.swift index cb762fd75e..cf5ab7b9d4 100644 --- a/novawallet/Modules/OperationDetails/ViewModel/OperationSwapViewModel.swift +++ b/novawallet/Modules/OperationDetails/ViewModel/OperationSwapViewModel.swift @@ -1,7 +1,7 @@ import Foundation struct OperationSwapViewModel { - let isOutgoing: Bool + let direction: AssetConversion.Direction let assetIn: SwapAssetAmountViewModel let assetOut: SwapAssetAmountViewModel let rate: String diff --git a/novawallet/Modules/TransactionHistory/Service/AssetHistoryFactoryFacade.swift b/novawallet/Modules/TransactionHistory/Service/AssetHistoryFactoryFacade.swift index f500cc2aef..3882bb9745 100644 --- a/novawallet/Modules/TransactionHistory/Service/AssetHistoryFactoryFacade.swift +++ b/novawallet/Modules/TransactionHistory/Service/AssetHistoryFactoryFacade.swift @@ -31,12 +31,12 @@ final class AssetHistoryFacade { // we support only transfers for non utility assets - let mappedFilter = asset.isUtility ? filter : .transfers - + let mappedFilter = asset.isUtility ? filter : [.transfers, .swaps] return SubqueryHistoryOperationFactory( url: url, filter: mappedFilter, assetId: historyAssetId, + isUtilityAsset: asset.isUtility, hasPoolStaking: asset.hasPoolStaking, hasSwaps: chainAsset.chain.hasSwaps ) From b0290b779b069c65d64b0239c58e8cf7638ea706 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 10 Nov 2023 21:50:19 +0300 Subject: [PATCH 155/204] fix call coding path --- ...sactionHistoryItem+CoreDataDecodable.swift | 15 +++++++----- .../Subquery/SubqueryHistory+Wallet.swift | 12 ++++++---- .../Substrate/Types/CallCodingPath.swift | 24 ++++++++++++------- .../TransactionHistoryViewModelFactory.swift | 6 ++--- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/novawallet/Common/Extension/Storage/CDTransactionHistoryItem+CoreDataDecodable.swift b/novawallet/Common/Extension/Storage/CDTransactionHistoryItem+CoreDataDecodable.swift index cd17f001c7..ad8c783184 100644 --- a/novawallet/Common/Extension/Storage/CDTransactionHistoryItem+CoreDataDecodable.swift +++ b/novawallet/Common/Extension/Storage/CDTransactionHistoryItem+CoreDataDecodable.swift @@ -51,12 +51,15 @@ extension CDTransactionItem: CoreDataCodable { txIndex = nil } if let swapContainer = try? container.nestedContainer(keyedBy: SwapHistoryData.CodingKeys.self, forKey: .swap) { - let newSwap = CDTransactionSwapItem(context: context) - newSwap.amountIn = try swapContainer.decode(String.self, forKey: .amountIn) - newSwap.amountOut = try swapContainer.decode(String.self, forKey: .amountOut) - newSwap.assetIdIn = try swapContainer.decodeIfPresent(String.self, forKey: .assetIdIn) - newSwap.assetIdOut = try swapContainer.decodeIfPresent(String.self, forKey: .assetIdOut) - swap = newSwap + if swap == nil { + let newSwap = CDTransactionSwapItem(context: context) + newSwap.transaction = self + swap = newSwap + } + swap?.amountIn = try swapContainer.decode(String.self, forKey: .amountIn) + swap?.amountOut = try swapContainer.decode(String.self, forKey: .amountOut) + swap?.assetIdIn = try swapContainer.decodeIfPresent(String.self, forKey: .assetIdIn) + swap?.assetIdOut = try swapContainer.decodeIfPresent(String.self, forKey: .assetIdOut) } } diff --git a/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift b/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift index 6d6ab14cfc..5039eb1f4e 100644 --- a/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift +++ b/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift @@ -50,7 +50,7 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { } else if let swap = swap { return createTransactionFromSwap( swap, - chainAssetId: chainAsset.chainAssetId, + chainAsset: chainAsset, chainFormat: chainAsset.chain.chainFormat ) } else if let reward = reward { @@ -112,16 +112,18 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { private func createTransactionFromSwap( _ swap: SubquerySwap, - chainAssetId: ChainAssetId, + chainAsset: ChainAsset, chainFormat: ChainFormat ) -> TransactionHistoryItem { let source = TransactionHistoryItemSource.substrate let remoteIdentifier = TransactionHistoryItem.createIdentifier(from: identifier, source: source) + let assetIdIn = chainAsset.chain.asset(byHistoryAssetId: swap.assetIdIn) ?? chainAsset.chain.utilityAsset() + let direction: AssetConversion.Direction = assetIdIn?.assetId == chainAsset.asset.assetId ? .sell : .buy return .init( identifier: remoteIdentifier, source: source, - chainId: chainAssetId.chainId, - assetId: chainAssetId.assetId, + chainId: chainAsset.chain.chainId, + assetId: chainAsset.asset.assetId, sender: swap.sender.normalize(for: chainFormat) ?? swap.sender, receiver: swap.receiver.normalize(for: chainFormat) ?? swap.receiver, amountInPlank: nil, @@ -134,7 +136,7 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { feeAssetId: nil, blockNumber: blockNumber, txIndex: UInt16(swap.eventIdx), - callPath: CallCodingPath.swap, + callPath: CallCodingPath.swap(direction: direction), call: nil, swap: .init( amountIn: swap.amountIn, diff --git a/novawallet/Common/Substrate/Types/CallCodingPath.swift b/novawallet/Common/Substrate/Types/CallCodingPath.swift index c52b442505..5a5e484069 100644 --- a/novawallet/Common/Substrate/Types/CallCodingPath.swift +++ b/novawallet/Common/Substrate/Types/CallCodingPath.swift @@ -26,6 +26,13 @@ extension CallCodingPath { PalletAssets.possibleTransferCallPaths().contains(self) } + var isSwap: Bool { + [ + Self.swap(direction: .buy), + Self.swap(direction: .sell) + ].contains(self) + } + var isTokensTransfer: Bool { [ .tokensTransfer, @@ -98,6 +105,15 @@ extension CallCodingPath { static var ethereumTransact: CallCodingPath { CallCodingPath(moduleName: "Ethereum", callName: "transact") } + + static func swap(direction: AssetConversion.Direction) -> CallCodingPath { + switch direction { + case .sell: + return CallCodingPath(moduleName: AssetConversionPallet.name, callName: "swap_exact_tokens_for_tokens") + case .buy: + return CallCodingPath(moduleName: AssetConversionPallet.name, callName: "swap_tokens_for_exact_tokens") + } + } } // MARK: Syntetic keys @@ -119,17 +135,9 @@ extension CallCodingPath { CallCodingPath(moduleName: "Substrate", callName: "poolSlash") } - static var swap: CallCodingPath { - CallCodingPath(moduleName: "Substrate", callName: "swap") - } - var isAnyStakingRewardOrSlash: Bool { [.slash, .reward, .poolReward, .poolSlash].contains(self) } - - var isSwap: Bool { - self == .swap - } } // MARK: Filter diff --git a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift index f048a431e5..9860825911 100644 --- a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift +++ b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift @@ -414,10 +414,10 @@ extension TransactionHistoryItem { return .poolReward case .poolSlash: return .poolSlash - case .swap: - return .swap default: - if callPath.isSubstrateOrEvmTransfer { + if callPath.isSwap { + return .swap + } else if callPath.isSubstrateOrEvmTransfer { return sender == address ? .outgoing : .incoming } else { return TransactionType.extrinsic From c0b922b909e4fd1eb9bfeecfc3812d0c3c8d2e93 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 10 Nov 2023 23:50:27 +0300 Subject: [PATCH 156/204] repeat operation --- .../AssetDetailsContainerViewFactory.swift | 3 ++- .../AssetList/AssetListWireframe.swift | 3 ++- .../OperationDetailsPresenter.swift | 19 +++++++++++++++- .../OperationDetailsProtocols.swift | 5 +++++ .../OperationDetailsViewFactory.swift | 5 +++-- .../OperationDetailsWireframe.swift | 20 +++++++++++++++++ .../Swaps/Setup/Model/SwapModels.swift | 22 +++++++++++++++++++ .../Swaps/Setup/SwapSetupPresenter.swift | 21 +++++++++++++++--- .../Swaps/Setup/SwapSetupViewFactory.swift | 12 +++++++++- .../TransactionHistoryViewFactory.swift | 10 +++++++-- .../TransactionHistoryWireframe.swift | 10 +++++++-- 11 files changed, 117 insertions(+), 13 deletions(-) diff --git a/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift b/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift index f41f84fc4c..6d20e2231a 100644 --- a/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift +++ b/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift @@ -13,7 +13,8 @@ final class AssetDetailsContainerViewFactory: AssetDetailsContainerViewFactoryPr asset: asset ), let historyView = TransactionHistoryViewFactory.createView( - chainAsset: .init(chain: chain, asset: asset) + chainAsset: .init(chain: chain, asset: asset), + assetListObservable: assetListObservable ) else { return nil } diff --git a/novawallet/Modules/AssetList/AssetListWireframe.swift b/novawallet/Modules/AssetList/AssetListWireframe.swift index 65777a0fef..f7bfab1f7b 100644 --- a/novawallet/Modules/AssetList/AssetListWireframe.swift +++ b/novawallet/Modules/AssetList/AssetListWireframe.swift @@ -31,7 +31,8 @@ final class AssetListWireframe: AssetListWireframeProtocol { func showHistory(from view: AssetListViewProtocol?, chain: ChainModel, asset: AssetModel) { guard let history = TransactionHistoryViewFactory.createView( - chainAsset: .init(chain: chain, asset: asset) + chainAsset: .init(chain: chain, asset: asset), + assetListObservable: assetListModelObservable ) else { return } diff --git a/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift b/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift index 3220f36e80..85a274c847 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift @@ -165,7 +165,24 @@ extension OperationDetailsPresenter: OperationDetailsPresenterProtocol { } func repeatOperation() { - // TODO: Show swap + guard case let .swap(swapModel) = model?.operation else { + return + } + let payChainAsset = ChainAsset(chain: swapModel.chain, asset: swapModel.assetIn) + let receiveChainAsset = ChainAsset(chain: swapModel.chain, asset: swapModel.assetOut) + let feeChainAsset = ChainAsset(chain: swapModel.chain, asset: swapModel.feeAsset) + let amount = swapModel.direction == .sell ? + swapModel.amountIn.decimal(precision: payChainAsset.asset.precision) : + swapModel.amountOut.decimal(precision: receiveChainAsset.asset.precision) + let swapSetupInitState = SwapSetupInitState( + payChainAsset: payChainAsset, + receiveChainAsset: receiveChainAsset, + feeChainAsset: feeChainAsset, + amount: amount, + direction: swapModel.direction + ) + + wireframe.showSwapSetup(from: view, state: swapSetupInitState) } } diff --git a/novawallet/Modules/OperationDetails/OperationDetailsProtocols.swift b/novawallet/Modules/OperationDetails/OperationDetailsProtocols.swift index 4b557e26bf..a4586b6a2d 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsProtocols.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsProtocols.swift @@ -29,4 +29,9 @@ protocol OperationDetailsWireframeProtocol: AlertPresentable, ErrorPresentable, displayAddress: DisplayAddress, chainAsset: ChainAsset ) + + func showSwapSetup( + from: OperationDetailsViewProtocol?, + state: SwapSetupInitState + ) } diff --git a/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift b/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift index d91bf5b2cb..55237adddc 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift @@ -6,7 +6,8 @@ import RobinHood struct OperationDetailsViewFactory { static func createView( for transaction: TransactionHistoryItem, - chainAsset: ChainAsset + chainAsset: ChainAsset, + assetListObservable: AssetListModelObservable ) -> OperationDetailsViewProtocol? { guard let currencyManager = CurrencyManager.shared, @@ -59,7 +60,7 @@ struct OperationDetailsViewFactory { ) } - let wireframe = OperationDetailsWireframe() + let wireframe = OperationDetailsWireframe(assetListObservable: assetListObservable) let localizationManager = LocalizationManager.shared let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) diff --git a/novawallet/Modules/OperationDetails/OperationDetailsWireframe.swift b/novawallet/Modules/OperationDetails/OperationDetailsWireframe.swift index a63a06c05d..d9d87b7616 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsWireframe.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsWireframe.swift @@ -1,6 +1,12 @@ import Foundation final class OperationDetailsWireframe: OperationDetailsWireframeProtocol { + let assetListObservable: AssetListModelObservable + + init(assetListObservable: AssetListModelObservable) { + self.assetListObservable = assetListObservable + } + func showSend( from view: OperationDetailsViewProtocol?, displayAddress: DisplayAddress, @@ -15,4 +21,18 @@ final class OperationDetailsWireframe: OperationDetailsWireframeProtocol { view?.controller.navigationController?.pushViewController(transferView.controller, animated: true) } + + func showSwapSetup( + from view: OperationDetailsViewProtocol?, + state: SwapSetupInitState + ) { + guard let swapView = SwapSetupViewFactory.createView( + assetListObservable: assetListObservable, + initState: state + ) else { + return + } + + view?.controller.navigationController?.pushViewController(swapView.controller, animated: true) + } } diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapModels.swift b/novawallet/Modules/Swaps/Setup/Model/SwapModels.swift index 6caa5d8908..ae33aa71ef 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapModels.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapModels.swift @@ -1,5 +1,27 @@ import SoraFoundation +struct SwapSetupInitState { + let payChainAsset: ChainAsset? + let receiveChainAsset: ChainAsset? + let feeChainAsset: ChainAsset? + let amount: Decimal? + let direction: AssetConversion.Direction? + + init( + payChainAsset: ChainAsset?, + receiveChainAsset: ChainAsset? = nil, + feeChainAsset: ChainAsset? = nil, + amount: Decimal? = nil, + direction: AssetConversion.Direction? = nil + ) { + self.payChainAsset = payChainAsset + self.receiveChainAsset = receiveChainAsset + self.feeChainAsset = feeChainAsset + self.amount = amount + self.direction = direction + } +} + struct SwapSetupFeeIdentifier: Equatable { let transactionId: String let feeChainAssetId: ChainAssetId? diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index b52b3bddc7..c055921c36 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -7,6 +7,7 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { let wireframe: SwapSetupWireframeProtocol let interactor: SwapSetupInteractorInputProtocol let purchaseProvider: PurchaseProviderProtocol + let initState: SwapSetupInitState private(set) var viewModelFactory: SwapsSetupViewModelFactoryProtocol @@ -32,7 +33,7 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { private var xcmTransfers: XcmTransfers? init( - payChainAsset: ChainAsset?, + initState: SwapSetupInitState, interactor: SwapSetupInteractorInputProtocol, wireframe: SwapSetupWireframeProtocol, viewModelFactory: SwapsSetupViewModelFactoryProtocol, @@ -43,8 +44,11 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { purchaseProvider: PurchaseProviderProtocol, logger: LoggerProtocol ) { - self.payChainAsset = payChainAsset - feeChainAsset = payChainAsset?.chain.utilityChainAsset() + self.initState = initState + payChainAsset = initState.payChainAsset + feeChainAsset = initState.feeChainAsset ?? payChainAsset?.chain.utilityChainAsset() + receiveChainAsset = initState.receiveChainAsset + self.interactor = interactor self.wireframe = wireframe self.viewModelFactory = viewModelFactory @@ -610,6 +614,17 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { interactor.setup() interactor.update(payChainAsset: payChainAsset) interactor.update(feeChainAsset: feeChainAsset) + + if let amount = initState.amount, let direction = initState.direction { + switch direction { + case .sell: + updatePayAmount(amount) + providePayAssetViews() + case .buy: + updateReceiveAmount(amount) + provideReceiveAssetViews() + } + } } func selectPayToken() { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index ab65cd6e7e..6a1ca9b022 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -6,6 +6,16 @@ struct SwapSetupViewFactory { static func createView( assetListObservable: AssetListModelObservable, payChainAsset: ChainAsset + ) -> SwapSetupViewProtocol? { + createView( + assetListObservable: assetListObservable, + initState: .init(payChainAsset: payChainAsset) + ) + } + + static func createView( + assetListObservable: AssetListModelObservable, + initState: SwapSetupInitState ) -> SwapSetupViewProtocol? { guard let currencyManager = CurrencyManager.shared, @@ -50,7 +60,7 @@ struct SwapSetupViewFactory { ) let presenter = SwapSetupPresenter( - payChainAsset: payChainAsset, + initState: initState, interactor: interactor, wireframe: wireframe, viewModelFactory: viewModelFactory, diff --git a/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift b/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift index 278c2d4bd3..54aa5b0f0e 100644 --- a/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift +++ b/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift @@ -4,7 +4,10 @@ import SoraFoundation import RobinHood struct TransactionHistoryViewFactory { - static func createView(chainAsset: ChainAsset) -> TransactionHistoryViewProtocol? { + static func createView( + chainAsset: ChainAsset, + assetListObservable: AssetListModelObservable + ) -> TransactionHistoryViewProtocol? { guard let selectedMetaAccount = SelectedWalletSettings.shared.value, let accountId = selectedMetaAccount.fetch(for: chainAsset.chain.accountRequest())?.accountId, @@ -19,7 +22,10 @@ struct TransactionHistoryViewFactory { currencyManager: currencyManager ) - let wireframe = TransactionHistoryWireframe(chainAsset: chainAsset) + let wireframe = TransactionHistoryWireframe( + chainAsset: chainAsset, + assetListObservable: assetListObservable + ) let balanceViewModelFactory = BalanceViewModelFactory( targetAssetInfo: chainAsset.assetDisplayInfo, diff --git a/novawallet/Modules/TransactionHistory/TransactionHistoryWireframe.swift b/novawallet/Modules/TransactionHistory/TransactionHistoryWireframe.swift index f578bd7579..a9d0003629 100644 --- a/novawallet/Modules/TransactionHistory/TransactionHistoryWireframe.swift +++ b/novawallet/Modules/TransactionHistory/TransactionHistoryWireframe.swift @@ -2,9 +2,14 @@ import UIKit final class TransactionHistoryWireframe: TransactionHistoryWireframeProtocol { let chainAsset: ChainAsset + let assetListObservable: AssetListModelObservable - init(chainAsset: ChainAsset) { + init( + chainAsset: ChainAsset, + assetListObservable: AssetListModelObservable + ) { self.chainAsset = chainAsset + self.assetListObservable = assetListObservable } func showFilter( @@ -28,7 +33,8 @@ final class TransactionHistoryWireframe: TransactionHistoryWireframeProtocol { ) { guard let operationDetailsView = OperationDetailsViewFactory.createView( for: operation, - chainAsset: chainAsset + chainAsset: chainAsset, + assetListObservable: assetListObservable ) else { return } From f0bcfb283f7a9a1e54217aa6dfae4fe559973040 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sun, 12 Nov 2023 16:05:20 +0100 Subject: [PATCH 157/204] refactor get token flow --- novawallet.xcodeproj/project.pbxproj | 26 +++- .../Protocols/PurchaseFlowManaging.swift | 2 +- .../Protocols/PurchasePresentable.swift | 2 +- .../ModalPickerViewController.swift | 4 + .../GetTokenOptionsInteractor.swift | 116 ++++++++++++++++++ .../GetTokenOptionsPresenter.swift | 97 +++++++++++++-- .../GetTokenOptionsProtocols.swift | 20 ++- .../GetTokenOptionsViewController.swift | 35 +++++- .../GetTokenOptionsViewFactory.swift | 79 +++++++++++- .../GetTokenOptionsWireframe.swift | 25 ++++ .../Model/GetTokenOperation.swift | 58 +++++++++ .../Model/GetTokenOptionsModel.swift | 17 +++ .../Model/GetTokenOptionsResult.swift | 9 ++ .../Swaps/Setup/SwapSetupInteractor.swift | 58 --------- .../Swaps/Setup/SwapSetupPresenter.swift | 99 +++------------ .../Swaps/Setup/SwapSetupProtocols.swift | 24 +--- .../Swaps/Setup/SwapSetupViewFactory.swift | 7 -- .../Swaps/Setup/SwapSetupWireframe.swift | 73 +++++++---- .../TransferSetupViewFactory.swift | 25 +--- 19 files changed, 553 insertions(+), 223 deletions(-) create mode 100644 novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsWireframe.swift create mode 100644 novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOperation.swift create mode 100644 novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOptionsModel.swift create mode 100644 novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOptionsResult.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 43b5d9d783..c7b5a6f517 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -254,6 +254,10 @@ 0CB261F52A9E188300287305 /* NominationPoolsBondExtraCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261F42A9E188300287305 /* NominationPoolsBondExtraCall.swift */; }; 0CB261F72A9E2D8400287305 /* StackSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261F62A9E2D8400287305 /* StackSwitchCell.swift */; }; 0CB261F92A9F1F2200287305 /* NPoolsRedeemError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261F82A9F1F2200287305 /* NPoolsRedeemError.swift */; }; + 0CB64E5A2AFE9947008F268F /* GetTokenOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E592AFE9947008F268F /* GetTokenOperation.swift */; }; + 0CB64E5C2B009DA9008F268F /* GetTokenOptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E5B2B009DA9008F268F /* GetTokenOptionsModel.swift */; }; + 0CB64E5E2B00AA8F008F268F /* GetTokenOptionsResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E5D2B00AA8F008F268F /* GetTokenOptionsResult.swift */; }; + 0CB64E602B00AD83008F268F /* GetTokenOptionsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E5F2B00AD83008F268F /* GetTokenOptionsWireframe.swift */; }; 0CBC29C62A421B5000F7B1F7 /* StakingMainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC29C52A421B5000F7B1F7 /* StakingMainWireframe.swift */; }; 0CBC29C82A42AE2F00F7B1F7 /* StakingDashboardBuilderResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC29C72A42AE2F00F7B1F7 /* StakingDashboardBuilderResult.swift */; }; 0CBF5DE72AB1A60500087EBF /* SharedOperationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBF5DE62AB1A60500087EBF /* SharedOperationStatus.swift */; }; @@ -4327,6 +4331,10 @@ 0CB261F42A9E188300287305 /* NominationPoolsBondExtraCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsBondExtraCall.swift; sourceTree = ""; }; 0CB261F62A9E2D8400287305 /* StackSwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackSwitchCell.swift; sourceTree = ""; }; 0CB261F82A9F1F2200287305 /* NPoolsRedeemError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsRedeemError.swift; sourceTree = ""; }; + 0CB64E592AFE9947008F268F /* GetTokenOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOperation.swift; sourceTree = ""; }; + 0CB64E5B2B009DA9008F268F /* GetTokenOptionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsModel.swift; sourceTree = ""; }; + 0CB64E5D2B00AA8F008F268F /* GetTokenOptionsResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsResult.swift; sourceTree = ""; }; + 0CB64E5F2B00AD83008F268F /* GetTokenOptionsWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsWireframe.swift; sourceTree = ""; }; 0CBC29C52A421B5000F7B1F7 /* StakingMainWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMainWireframe.swift; sourceTree = ""; }; 0CBC29C72A42AE2F00F7B1F7 /* StakingDashboardBuilderResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingDashboardBuilderResult.swift; sourceTree = ""; }; 0CBF5DE62AB1A60500087EBF /* SharedOperationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedOperationStatus.swift; sourceTree = ""; }; @@ -8745,11 +8753,13 @@ 0CA50CAD2AFE602F005668CD /* GetTokenOptions */ = { isa = PBXGroup; children = ( + 0CB64E582AFE9939008F268F /* Model */, 0CA50CAE2AFE6094005668CD /* GetTokenOptionsViewController.swift */, 0CA50CB02AFE6F15005668CD /* GetTokenOptionsProtocols.swift */, 0CA50CB22AFE6F23005668CD /* GetTokenOptionsPresenter.swift */, 0CA50CB42AFE6F31005668CD /* GetTokenOptionsInteractor.swift */, 0CA50CB62AFE6F40005668CD /* GetTokenOptionsViewFactory.swift */, + 0CB64E5F2B00AD83008F268F /* GetTokenOptionsWireframe.swift */, ); path = GetTokenOptions; sourceTree = ""; @@ -8824,6 +8834,16 @@ path = Model; sourceTree = ""; }; + 0CB64E582AFE9939008F268F /* Model */ = { + isa = PBXGroup; + children = ( + 0CB64E592AFE9947008F268F /* GetTokenOperation.swift */, + 0CB64E5B2B009DA9008F268F /* GetTokenOptionsModel.swift */, + 0CB64E5D2B00AA8F008F268F /* GetTokenOptionsResult.swift */, + ); + path = Model; + sourceTree = ""; + }; 0CCA24592AC6914100AEF23D /* V3 */ = { isa = PBXGroup; children = ( @@ -13259,9 +13279,9 @@ 8490140124A92F6D008F705E /* OnbordingMain */, 849014A724AA87E3008F705E /* Pincode */, 8428764224ADDE0200D91AD8 /* Settings */, - F1A9198B13888515D787A6C1 /* Purchase */, 8490146E24A94A37008F705E /* Root */, 84F43B8725DE9F8500AEDA56 /* Staking */, + F1A9198B13888515D787A6C1 /* Purchase */, 84E1CCF3260DC973001E81B5 /* SwitchAccount */, C7AEDB8341B78EC46F6F98DC /* UsernameSetup */, A800B16846A754FEDAF801EC /* AssetSelection */, @@ -20665,6 +20685,7 @@ 84FB298426393D0900BE0FCD /* YourValidatorListViewModelFactory.swift in Sources */, 842E9E942A2A277D00759972 /* StakingDashboardLocalStorageSubscriber.swift in Sources */, 8489A6D827FDA51C0040C066 /* AccountLocalSubscriptionHandler.swift in Sources */, + 0CB64E602B00AD83008F268F /* GetTokenOptionsWireframe.swift in Sources */, 8406B5AB26FBD9EB00635B61 /* AccountInfoUpdatingService.swift in Sources */, 844B2E7A27C425E3000CC079 /* NftLocalStorageSubscriber.swift in Sources */, AE6F7FE62685F2C3002BBC3E /* ValidatorListFilterViewModel.swift in Sources */, @@ -21049,6 +21070,7 @@ 8401AEC82642A71D000B03E3 /* StakingRebondConfirmationProtocols.swift in Sources */, 849014BA24AA87E4008F705E /* PinSetupProtocol.swift in Sources */, F418E887264D308700699085 /* StakingAlert.swift in Sources */, + 0CB64E5A2AFE9947008F268F /* GetTokenOperation.swift in Sources */, 84DD5F7C263DFEC600425ACF /* StakingDataValidatorFactory.swift in Sources */, 779107682AB8CA71000A4B17 /* AssetListSearchEmptyCell.swift in Sources */, 84E1CCF5260DCB91001E81B5 /* SwitchAccount+WalletManagementWireframe.swift in Sources */, @@ -22624,6 +22646,7 @@ BFC8C5A2C95D6EDF97D73732 /* ParaStkCollatorsSearchPresenter.swift in Sources */, 25993E2E536DE682E1DFC9AD /* ParaStkCollatorsSearchInteractor.swift in Sources */, 842643BF28785CFC0031B5B5 /* TuringStaking.swift in Sources */, + 0CB64E5E2B00AA8F008F268F /* GetTokenOptionsResult.swift in Sources */, 688F73AFD5FF20F77242B57E /* ParaStkCollatorsSearchViewController.swift in Sources */, 3BFD635E852E4D395025BEE8 /* ParaStkCollatorsSearchViewFactory.swift in Sources */, 88FB7DCB2950712E00784E08 /* AssetDetailsContainerViewController.swift in Sources */, @@ -22859,6 +22882,7 @@ 84CEF28E29050A3300BA25BB /* DataValidationRunner+GovVote.swift in Sources */, 95EBC71EAE906B0DFA758AB8 /* LedgerTxConfirmProtocols.swift in Sources */, 992678FD5D3F9D39FFC2BB53 /* LedgerTxConfirmWireframe.swift in Sources */, + 0CB64E5C2B009DA9008F268F /* GetTokenOptionsModel.swift in Sources */, 84C1DBC529C27D9800F295A5 /* RewardCalculatorParamsServiceFactory.swift in Sources */, ED529AA4ED05E8847C4C067F /* LedgerTxConfirmPresenter.swift in Sources */, E488F3E052650FF525D41D63 /* LedgerTxConfirmInteractor.swift in Sources */, diff --git a/novawallet/Common/Protocols/PurchaseFlowManaging.swift b/novawallet/Common/Protocols/PurchaseFlowManaging.swift index 35348b3a48..b317ce4ce0 100644 --- a/novawallet/Common/Protocols/PurchaseFlowManaging.swift +++ b/novawallet/Common/Protocols/PurchaseFlowManaging.swift @@ -1,6 +1,6 @@ import SoraFoundation -protocol PurchaseFlowManaging { +protocol PurchaseFlowManaging: AnyObject { func startPuchaseFlow( from view: ControllerBackedProtocol?, purchaseActions: [PurchaseAction], diff --git a/novawallet/Common/Protocols/PurchasePresentable.swift b/novawallet/Common/Protocols/PurchasePresentable.swift index 67cc3affbd..60aaa5c677 100644 --- a/novawallet/Common/Protocols/PurchasePresentable.swift +++ b/novawallet/Common/Protocols/PurchasePresentable.swift @@ -43,7 +43,7 @@ extension PurchasePresentable { guard let pickerView = ModalPickerFactory.createPickerForList( actions, delegate: delegate, - context: nil + context: actions as NSArray ) else { return } diff --git a/novawallet/Common/ViewController/ModalPicker/ModalPickerViewController.swift b/novawallet/Common/ViewController/ModalPicker/ModalPickerViewController.swift index 734af0bee5..815f23d255 100644 --- a/novawallet/Common/ViewController/ModalPicker/ModalPickerViewController.swift +++ b/novawallet/Common/ViewController/ModalPicker/ModalPickerViewController.swift @@ -106,6 +106,10 @@ class ModalPickerViewController sectionFooters[lastSectionIndex] = footer } + func reload() { + tableView.reloadData() + } + private func configure() { if let cellNib = cellNib { tableView.register(cellNib, forCellReuseIdentifier: cellIdentifier) diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsInteractor.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsInteractor.swift index fecc4ab449..4f2a12d221 100644 --- a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsInteractor.swift @@ -1 +1,117 @@ import Foundation + +final class GetTokenOptionsInteractor { + weak var presenter: GetTokenOptionsInteractorOutputProtocol? + + let selectedWallet: MetaAccountModel + let assetModelObservable: AssetListModelObservable + let destinationChainAsset: ChainAsset + let xcmTransfersSyncService: XcmTransfersSyncServiceProtocol + let purchaseProvider: PurchaseProviderProtocol + let logger: LoggerProtocol + + private var xcmTransfers: XcmTransfers? + + init( + selectedWallet: MetaAccountModel, + destinationChainAsset: ChainAsset, + assetModelObservable: AssetListModelObservable, + xcmTransfersSyncService: XcmTransfersSyncServiceProtocol, + purchaseProvider: PurchaseProviderProtocol, + logger: LoggerProtocol + ) { + self.selectedWallet = selectedWallet + self.destinationChainAsset = destinationChainAsset + self.assetModelObservable = assetModelObservable + self.xcmTransfersSyncService = xcmTransfersSyncService + self.purchaseProvider = purchaseProvider + self.logger = logger + } + + deinit { + xcmTransfersSyncService.throttle() + } + + private func provideModel() { + let accountRequest = destinationChainAsset.chain.accountRequest() + + guard let selectedAccount = selectedWallet.fetchMetaChainAccount(for: accountRequest) else { + presenter?.didReceive(model: .empty) + return + } + + let availableXcmOrigins = determineAvailableXcmOrigins() + let purchaseActions = purchaseProvider.buildPurchaseActions( + for: destinationChainAsset, + accountId: selectedAccount.chainAccount.accountId + ) + + let receiveAvailable = TokenOperation.checkReceiveOperationAvailable( + walletType: selectedWallet.type, + chainAsset: destinationChainAsset + ).available + + let buyAvailable = TokenOperation.checkBuyOperationAvailable( + purchaseActions: purchaseActions, + walletType: selectedWallet.type, + chainAsset: destinationChainAsset + ).available + + let model = GetTokenOptionsModel( + availableXcmOrigins: availableXcmOrigins, + receiveAccount: receiveAvailable ? selectedAccount : nil, + buyOptions: buyAvailable ? purchaseActions : [] + ) + + presenter?.didReceive(model: model) + } + + private func determineAvailableXcmOrigins() -> Set { + guard let xcmTransfers = xcmTransfers else { + return [] + } + + let balances = assetModelObservable.state.value.balances + + let availableOrigins = xcmTransfers + .transferChainAssets(to: destinationChainAsset.chainAssetId) + .filter { chainAssetId in + if case let .success(balance) = balances[chainAssetId], balance.transferable > 0 { + return true + } else { + return false + } + } + + return Set(availableOrigins) + } + + private func setupBalances() { + assetModelObservable.addObserver(with: self, queue: .main) { [weak self] _, _ in + self?.provideModel() + } + } + + private func setupXcms() { + xcmTransfersSyncService.notificationCallback = { [weak self] result in + switch result { + case let .success(xcmTransfers): + self?.xcmTransfers = xcmTransfers + self?.provideModel() + case let .failure(error): + self?.logger.error("Xcm sync failed: \(error)") + } + } + + xcmTransfersSyncService.setup() + } +} + +extension GetTokenOptionsInteractor: GetTokenOptionsInteractorInputProtocol { + func setup() { + setupBalances() + setupXcms() + + provideModel() + } +} diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsPresenter.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsPresenter.swift index 0b2c529c4e..d9df290ef9 100644 --- a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsPresenter.swift @@ -1,9 +1,90 @@ -// -// GetTokenOptionsPresenter.swift -// novawallet -// -// Created by Ruslan Rezin on 10.11.2023. -// Copyright © 2023 Nova Foundation. All rights reserved. -// - import Foundation +import SoraFoundation + +final class GetTokenOptionsPresenter { + weak var view: GetTokenOptionsViewProtocol? + let interactor: GetTokenOptionsInteractorInputProtocol + let wireframe: GetTokenOptionsWireframeProtocol + let destinationChainAsset: ChainAsset + + let allOperations: [GetTokenOperation] = [.crosschain, .receive, .buy] + + private var model: GetTokenOptionsModel? + + init( + interactor: GetTokenOptionsInteractorInputProtocol, + wireframe: GetTokenOptionsWireframeProtocol, + destinationChainAsset: ChainAsset + ) { + self.interactor = interactor + self.wireframe = wireframe + self.destinationChainAsset = destinationChainAsset + } + + private func isOperationAvailable(_ operation: GetTokenOperation) -> Bool { + guard let model = model else { + return false + } + + switch operation { + case .crosschain: + return !model.availableXcmOrigins.isEmpty + case .receive: + return model.receiveAccount != nil + case .buy: + return !model.buyOptions.isEmpty + } + } + + private func provideViewModel() { + let viewModels = allOperations.map { operation in + let isActive = isOperationAvailable(operation) + let token = destinationChainAsset.asset.symbol + + return LocalizableResource { locale in + TokenOperationTableViewCell.Model( + content: .init( + title: operation.titleForLocale(locale), + subtitle: operation.subtitleForLocale(locale, token: token), + icon: operation.icon + ), + isActive: isActive + ) + } + } + + view?.didReceive(viewModels: viewModels) + } +} + +extension GetTokenOptionsPresenter: GetTokenOptionsPresenterProtocol { + func setup() { + provideViewModel() + + interactor.setup() + } + + func selectOption(at index: Int) { + guard let model = model else { + return + } + + switch allOperations[index] { + case .crosschain: + wireframe.complete(on: view, result: .crosschains(model.availableXcmOrigins)) + case .receive: + if let account = model.receiveAccount { + wireframe.complete(on: view, result: .receive(account)) + } + case .buy: + wireframe.complete(on: view, result: .buy(model.buyOptions)) + } + } +} + +extension GetTokenOptionsPresenter: GetTokenOptionsInteractorOutputProtocol { + func didReceive(model: GetTokenOptionsModel) { + self.model = model + provideViewModel() + } +} diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsProtocols.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsProtocols.swift index e87f56fc47..cee619f526 100644 --- a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsProtocols.swift @@ -1,5 +1,23 @@ import Foundation +import SoraFoundation + +protocol GetTokenOptionsViewProtocol: ControllerBackedProtocol { + func didReceive(viewModels: [LocalizableResource]) +} protocol GetTokenOptionsPresenterProtocol: AnyObject { - + func setup() + func selectOption(at index: Int) +} + +protocol GetTokenOptionsWireframeProtocol: AnyObject { + func complete(on view: GetTokenOptionsViewProtocol?, result: GetTokenOptionsResult) +} + +protocol GetTokenOptionsInteractorInputProtocol: AnyObject { + func setup() +} + +protocol GetTokenOptionsInteractorOutputProtocol: AnyObject { + func didReceive(model: GetTokenOptionsModel) } diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewController.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewController.swift index 92ff7d9d8b..71f02f224c 100644 --- a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewController.swift +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewController.swift @@ -1,8 +1,41 @@ import UIKit +import SoraFoundation final class GetTokenOptionsViewController: ModalPickerViewController< TokenOperationTableViewCell, TokenOperationTableViewCell.Model > { - + let operationPresenter: GetTokenOptionsPresenterProtocol + + init(operationPresenter: GetTokenOptionsPresenterProtocol) { + self.operationPresenter = operationPresenter + + let nib = R.nib.modalPickerViewController + super.init(nibName: nib.name, bundle: nib.bundle) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + operationPresenter.setup() + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + operationPresenter.selectOption(at: indexPath.row) + } +} + +extension GetTokenOptionsViewController: GetTokenOptionsViewProtocol { + func didReceive(viewModels: [LocalizableResource]) { + self.viewModels = viewModels + + reload() + } } diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewFactory.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewFactory.swift index fecc4ab449..9ef0e259e6 100644 --- a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsViewFactory.swift @@ -1 +1,78 @@ -import Foundation +import UIKit +import SoraUI +import SoraFoundation + +enum GetTokenOptionsViewFactory { + static func createView( + from destinationChainAsset: ChainAsset, + assetModelObservable: AssetListModelObservable, + completion: @escaping GetTokenOptionsCompletion + ) -> GetTokenOptionsViewProtocol? { + guard let interactor = createInteractor( + from: destinationChainAsset, + assetModelObservable: assetModelObservable + ) else { + return nil + } + + let wireframe = GetTokenOptionsWireframe(completion: completion) + + let presenter = GetTokenOptionsPresenter( + interactor: interactor, + wireframe: wireframe, + destinationChainAsset: destinationChainAsset + ) + + let view = GetTokenOptionsViewController(operationPresenter: presenter) + + view.localizedTitle = .init { + R.string.localizable.swapsSetupDepositTitle( + destinationChainAsset.asset.symbol, + preferredLanguages: $0.rLanguages + ) + } + + view.selectedIndex = NSNotFound + view.modalPresentationStyle = .custom + view.headerBorderType = .none + view.separatorStyle = .none + view.separatorColor = R.color.colorDivider() + view.cellHeight = 48 + + let factory = ModalSheetPresentationFactory(configuration: ModalSheetPresentationConfiguration.nova) + view.modalTransitioningFactory = factory + + let height = view.headerHeight + CGFloat(presenter.allOperations.count) * view.cellHeight + view.footerHeight + view.preferredContentSize = CGSize(width: 0.0, height: height) + + view.localizationManager = LocalizationManager.shared + + presenter.view = view + interactor.presenter = presenter + + return view + } + + private static func createInteractor( + from destinationChainAsset: ChainAsset, + assetModelObservable: AssetListModelObservable + ) -> GetTokenOptionsInteractor? { + guard let selectedWallet = SelectedWalletSettings.shared.value else { + return nil + } + + let xcmTransfersSyncService = XcmTransfersSyncService( + remoteUrl: ApplicationConfig.shared.xcmTransfersURL, + operationQueue: OperationManagerFacade.sharedDefaultQueue + ) + + return GetTokenOptionsInteractor( + selectedWallet: selectedWallet, + destinationChainAsset: destinationChainAsset, + assetModelObservable: assetModelObservable, + xcmTransfersSyncService: xcmTransfersSyncService, + purchaseProvider: PurchaseAggregator.defaultAggregator(), + logger: Logger.shared + ) + } +} diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsWireframe.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsWireframe.swift new file mode 100644 index 0000000000..a7b5c6c962 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsWireframe.swift @@ -0,0 +1,25 @@ +import Foundation + +final class GetTokenOptionsWireframe { + let completion: GetTokenOptionsCompletion? + + init(completion: GetTokenOptionsCompletion?) { + self.completion = completion + } + + func complete( + on view: GetTokenOptionsViewProtocol?, + completion: GetTokenOptionsCompletion?, + result: GetTokenOptionsResult + ) { + view?.controller.dismiss(animated: true) { + completion?(result) + } + } +} + +extension GetTokenOptionsWireframe: GetTokenOptionsWireframeProtocol { + func complete(on view: GetTokenOptionsViewProtocol?, result: GetTokenOptionsResult) { + complete(on: view, completion: completion, result: result) + } +} diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOperation.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOperation.swift new file mode 100644 index 0000000000..6c2f5ebe2e --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOperation.swift @@ -0,0 +1,58 @@ +import Foundation +import UIKit + +enum GetTokenOperation { + case crosschain + case receive + case buy +} + +extension GetTokenOperation { + func titleForLocale(_ locale: Locale) -> String { + switch self { + case .crosschain: + return R.string.localizable.swapsSetupDepositByCrossChainTransferTitle( + preferredLanguages: locale.rLanguages + ) + case .receive: + return R.string.localizable.walletAssetReceive( + preferredLanguages: locale.rLanguages + ) + case .buy: + return R.string.localizable.walletAssetBuy( + preferredLanguages: locale.rLanguages + ) + } + } + + func subtitleForLocale(_ locale: Locale, token: String) -> String { + switch self { + case .crosschain: + return R.string.localizable.swapsSetupDepositByCrossChainTransferSubtitle( + token, + preferredLanguages: locale.rLanguages + ) + case .receive: + return R.string.localizable.swapsSetupDepositByReceiveSubtitle( + token, + preferredLanguages: locale.rLanguages + ) + case .buy: + return R.string.localizable.swapsSetupDepositByBuySubtitle( + token, + preferredLanguages: locale.rLanguages + ) + } + } + + var icon: UIImage? { + switch self { + case .crosschain: + return R.image.iconCrossChainTransfer() + case .receive: + return R.image.iconReceive() + case .buy: + return R.image.iconBuy() + } + } +} diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOptionsModel.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOptionsModel.swift new file mode 100644 index 0000000000..7b2dc0f0cd --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOptionsModel.swift @@ -0,0 +1,17 @@ +import Foundation + +struct GetTokenOptionsModel { + let availableXcmOrigins: Set + let receiveAccount: MetaChainAccountResponse? + let buyOptions: [PurchaseAction] +} + +extension GetTokenOptionsModel { + static var empty: GetTokenOptionsModel { + .init( + availableXcmOrigins: [], + receiveAccount: nil, + buyOptions: [] + ) + } +} diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOptionsResult.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOptionsResult.swift new file mode 100644 index 0000000000..834ec152c0 --- /dev/null +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOptionsResult.swift @@ -0,0 +1,9 @@ +import Foundation + +enum GetTokenOptionsResult { + case crosschains(Set) + case receive(MetaChainAccountResponse) + case buy([PurchaseAction]) +} + +typealias GetTokenOptionsCompletion = (GetTokenOptionsResult) -> Void diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift index 82d6217f6c..53f57fe2b5 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupInteractor.swift @@ -4,17 +4,14 @@ import BigInt import SubstrateSdk final class SwapSetupInteractor: SwapBaseInteractor { - let xcmTransfersSyncService: XcmTransfersSyncServiceProtocol let storageRepository: AnyDataProviderRepository - private var xcmTransfers: XcmTransfers? private var canPayFeeInAssetCall = CancellableCallStore() private var remoteSubscription: CallbackBatchStorageSubscription? private var blockNumberSubscription: AnyDataProvider? init( - xcmTransfersSyncService: XcmTransfersSyncServiceProtocol, assetConversionAggregatorFactory: AssetConversionAggregationFactoryProtocol, assetConversionFeeService: AssetConversionFeeServiceProtocol, chainRegistry: ChainRegistryProtocol, @@ -27,7 +24,6 @@ final class SwapSetupInteractor: SwapBaseInteractor { selectedWallet: MetaAccountModel, operationQueue: OperationQueue ) { - self.xcmTransfersSyncService = xcmTransfersSyncService self.storageRepository = storageRepository super.init( @@ -78,53 +74,10 @@ final class SwapSetupInteractor: SwapBaseInteractor { } deinit { - xcmTransfersSyncService.throttle() canPayFeeInAssetCall.cancel() clearRemoteSubscription() } - private func setupXcmTransfersSyncService() { - xcmTransfersSyncService.notificationCallback = { [weak self] result in - switch result { - case let .success(xcmTransfers): - self?.xcmTransfers = xcmTransfers - self?.provideAvailableTransfers() - case let .failure(error): - self?.presenter?.didReceive(setupError: .xcm(error)) - } - } - - xcmTransfersSyncService.setup() - } - - private func provideAvailableTransfers() { - guard let xcmTransfers = xcmTransfers, let payChainAsset = payChainAsset else { - presenter?.didReceiveAvailableXcm(origins: [], xcmTransfers: nil) - return - } - - let chainAssets = xcmTransfers.transferChainAssets(to: payChainAsset.chainAssetId) - - guard !chainAssets.isEmpty else { - presenter?.didReceiveAvailableXcm(origins: [], xcmTransfers: xcmTransfers) - return - } - - let origins: [ChainAsset] = chainAssets.compactMap { chainAsset in - guard - chainAsset != payChainAsset.chainAssetId, - let chain = chainRegistry.getChain(for: chainAsset.chainId), - let asset = chain.asset(for: chainAsset.assetId) - else { - return nil - } - - return ChainAsset(chain: chain, asset: asset) - } - - presenter?.didReceiveAvailableXcm(origins: origins, xcmTransfers: xcmTransfers) - } - private func provideCanPayFee(for asset: ChainAsset) { canPayFeeInAssetCall.cancel() @@ -228,11 +181,6 @@ final class SwapSetupInteractor: SwapBaseInteractor { } } - override func setup() { - super.setup() - setupXcmTransfersSyncService() - } - override func handleBlockNumber( result: Result, chainId: ChainModel.Id @@ -247,10 +195,6 @@ final class SwapSetupInteractor: SwapBaseInteractor { } extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { - func setupXcm() { - setupXcmTransfersSyncService() - } - func update(receiveChainAsset: ChainAsset?) { self.receiveChainAsset = receiveChainAsset receiveChainAsset.map { @@ -265,8 +209,6 @@ extension SwapSetupInteractor: SwapSetupInteractorInputProtocol { set(payChainAsset: payChainAsset) provideCanPayFee(for: payChainAsset) } - - provideAvailableTransfers() } func update(feeChainAsset: ChainAsset?) { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index bea55ae077..000316f47e 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -2,11 +2,10 @@ import Foundation import SoraFoundation import BigInt -final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { +final class SwapSetupPresenter: SwapBasePresenter { weak var view: SwapSetupViewProtocol? let wireframe: SwapSetupWireframeProtocol let interactor: SwapSetupInteractorInputProtocol - let purchaseProvider: PurchaseProviderProtocol private(set) var viewModelFactory: SwapsSetupViewModelFactoryProtocol @@ -26,10 +25,6 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { private var feeIdentifier: SwapSetupFeeIdentifier? private var slippage: BigRational - private var depositOperations: [DepositOperationModel] = [] - private var purchaseActions: [PurchaseAction] = [] - private var depositCrossChainAssets: [ChainAsset] = [] - private var xcmTransfers: XcmTransfers? init( payChainAsset: ChainAsset?, @@ -40,7 +35,6 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { localizationManager: LocalizationManagerProtocol, selectedWallet: MetaAccountModel, slippageConfig: SlippageConfig, - purchaseProvider: PurchaseProviderProtocol, logger: LoggerProtocol ) { self.payChainAsset = payChainAsset @@ -49,7 +43,6 @@ final class SwapSetupPresenter: SwapBasePresenter, PurchaseFlowManaging { self.wireframe = wireframe self.viewModelFactory = viewModelFactory slippage = slippageConfig.defaultSlippage - self.purchaseProvider = purchaseProvider super.init( selectedWallet: selectedWallet, @@ -830,35 +823,15 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { } func depositInsufficientToken() { - guard - let payChainAsset = payChainAsset, - let accountId = selectedWallet.fetch(for: payChainAsset.chain.accountRequest())?.accountId else { + guard let payChainAsset = payChainAsset else { return } - purchaseActions = purchaseProvider.buildPurchaseActions(for: payChainAsset, accountId: accountId) - let sendAvailable = TokenOperation.checkTransferOperationAvailable() - let crossChainSendAvailable = depositCrossChainAssets.first != nil && sendAvailable - - let recieveAvailable = TokenOperation.checkReceiveOperationAvailable( - walletType: selectedWallet.type, - chainAsset: payChainAsset - ).available - let buyAvailable = TokenOperation.checkBuyOperationAvailable( - purchaseActions: purchaseActions, - walletType: selectedWallet.type, - chainAsset: payChainAsset - ).available - depositOperations = [ - .init(operation: .send, active: crossChainSendAvailable), - .init(operation: .receive, active: recieveAvailable), - .init(operation: .buy, active: buyAvailable) - ] - wireframe.showTokenDepositOptions( + wireframe.showGetTokenOptions( form: view, - operations: depositOperations, - token: payChainAsset.asset.symbol, - delegate: self + purchaseHadler: self, + destinationChainAsset: payChainAsset, + locale: selectedLocale ) } } @@ -874,10 +847,6 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { self?.interactor.update(payChainAsset: payChainAsset) } } - case .xcm: - wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in - self?.interactor.setupXcm() - } case .blockNumber: wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in self?.interactor.retryBlockNumberSubscription() @@ -897,11 +866,6 @@ extension SwapSetupPresenter: SwapSetupInteractorOutputProtocol { } } - func didReceiveAvailableXcm(origins: [ChainAsset], xcmTransfers: XcmTransfers?) { - depositCrossChainAssets = origins - self.xcmTransfers = xcmTransfers - } - func didReceiveBlockNumber(_ blockNumber: BlockNumber?, chainId _: ChainModel.Id) { logger.debug("New block number: \(String(describing: blockNumber))") @@ -918,53 +882,20 @@ extension SwapSetupPresenter: Localizable { } } -extension SwapSetupPresenter: ModalPickerViewControllerDelegate { - func modalPickerDidSelectModelAtIndex(_ index: Int, context _: AnyObject?) { - guard let operation = depositOperations[safe: index], operation.active else { +extension SwapSetupPresenter: PurchaseFlowManaging, PurchaseDelegate, ModalPickerViewControllerDelegate { + func modalPickerDidSelectModelAtIndex(_ index: Int, context: AnyObject?) { + guard let actions = context as? [PurchaseAction] else { return } - switch operation.operation { - case .buy: - startPuchaseFlow( - from: view, - purchaseActions: purchaseActions, - wireframe: wireframe, - locale: selectedLocale - ) - case .receive: - guard let payChainAsset = payChainAsset, - let metaChainAccountResponse = selectedWallet.fetchMetaChainAccount( - for: payChainAsset.chain.accountRequest() - ) else { - return - } - wireframe.showDepositTokensByReceive( - from: view, - chainAsset: payChainAsset, - metaChainAccountResponse: metaChainAccountResponse - ) - case .send: - guard let payChainAsset = payChainAsset, - let accountId = selectedWallet.fetch(for: payChainAsset.chain.accountRequest()), - let address = accountId.toAddress(), - let origin = depositCrossChainAssets.first, - let xcmTransfers = xcmTransfers else { - return - } - - wireframe.showDepositTokensBySend( - from: view, - origin: origin, - destination: payChainAsset, - recepient: .init(address: address, username: ""), - xcmTransfers: xcmTransfers - ) - } + startPuchaseFlow( + from: view, + purchaseAction: actions[index], + wireframe: wireframe, + locale: selectedLocale + ) } -} -extension SwapSetupPresenter: PurchaseDelegate { func purchaseDidComplete() { wireframe.presentPurchaseDidComplete(view: view, locale: selectedLocale) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index e3f36e9fc9..553f79253d 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -43,13 +43,11 @@ protocol SwapSetupInteractorInputProtocol: SwapBaseInteractorInputProtocol { func update(receiveChainAsset: ChainAsset?) func update(payChainAsset: ChainAsset?) func update(feeChainAsset: ChainAsset?) - func setupXcm() func retryRemoteSubscription() func retryBlockNumberSubscription() } protocol SwapSetupInteractorOutputProtocol: SwapBaseInteractorOutputProtocol { - func didReceiveAvailableXcm(origins: [ChainAsset], xcmTransfers: XcmTransfers?) func didReceiveCanPayFeeInPayAsset(_ value: Bool, chainAssetId: ChainAssetId) func didReceiveBlockNumber(_ blockNumber: BlockNumber?, chainId: ChainModel.Id) func didReceive(setupError: SwapSetupError) @@ -85,28 +83,16 @@ protocol SwapSetupWireframeProtocol: SwapBaseWireframeProtocol, ShortTextInfoPre form view: ControllerBackedProtocol?, viewModel: SwapNetworkFeeSheetViewModel ) - func showTokenDepositOptions( + + func showGetTokenOptions( form view: ControllerBackedProtocol?, - operations: [DepositOperationModel], - token: String, - delegate: ModalPickerViewControllerDelegate? - ) - func showDepositTokensByReceive( - from view: ControllerBackedProtocol?, - chainAsset: ChainAsset, - metaChainAccountResponse: MetaChainAccountResponse - ) - func showDepositTokensBySend( - from view: ControllerBackedProtocol?, - origin: ChainAsset, - destination: ChainAsset, - recepient: DisplayAddress?, - xcmTransfers: XcmTransfers + purchaseHadler: PurchaseFlowManaging, + destinationChainAsset: ChainAsset, + locale: Locale ) } enum SwapSetupError: Error { - case xcm(Error) case payAssetSetFailed(Error) case remoteSubscription(Error) case blockNumber(Error) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index ab65cd6e7e..4bffe4691d 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -58,7 +58,6 @@ struct SwapSetupViewFactory { localizationManager: LocalizationManager.shared, selectedWallet: selectedWallet, slippageConfig: .defaultConfig, - purchaseProvider: PurchaseAggregator.defaultAggregator(), logger: Logger.shared ) @@ -96,18 +95,12 @@ struct SwapSetupViewFactory { operationQueue: operationQueue ) - let xcmTransfersSyncService = XcmTransfersSyncService( - remoteUrl: ApplicationConfig.shared.xcmTransfersURL, - operationQueue: operationQueue - ) - let assetStorageFactory = AssetStorageInfoOperationFactory( chainRegistry: chainRegistry, operationQueue: operationQueue ) let interactor = SwapSetupInteractor( - xcmTransfersSyncService: xcmTransfersSyncService, assetConversionAggregatorFactory: assetConversionAggregator, assetConversionFeeService: feeService, chainRegistry: ChainRegistryFacade.sharedRegistry, diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index c7ca661494..85eeceed7c 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -102,44 +102,71 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { view?.controller.present(bottomSheet.controller, animated: true) } - func showTokenDepositOptions( + func showGetTokenOptions( form view: ControllerBackedProtocol?, - operations: [DepositOperationModel], - token: String, - delegate: ModalPickerViewControllerDelegate? + purchaseHadler: PurchaseFlowManaging, + destinationChainAsset: ChainAsset, + locale: Locale ) { - guard let bottomSheet = ModalPickerFactory.createPickerListForOperations( - operations: operations, - delegate: delegate, - token: token, - context: nil + let completion: GetTokenOptionsCompletion = { [weak self, weak purchaseHadler] result in + guard let self = self else { + return + } + + switch result { + case let .crosschains(origins): + self.showGetTokensByCrosschain( + from: view, + origins: origins, + destination: destinationChainAsset + ) + case let .receive(account): + self.showGetTokensByReceive( + from: view, + chainAsset: destinationChainAsset, + metaChainAccountResponse: account + ) + case let .buy(actions): + purchaseHadler?.startPuchaseFlow( + from: view, + purchaseActions: actions, + wireframe: self, + locale: locale + ) + } + } + + guard let bottomSheet = GetTokenOptionsViewFactory.createView( + from: destinationChainAsset, + assetModelObservable: assetListObservable, + completion: completion ) else { return } - view?.controller.present(bottomSheet, animated: true) + view?.controller.present(bottomSheet.controller, animated: true) } - func showDepositTokensBySend( + func showGetTokensByCrosschain( from view: ControllerBackedProtocol?, - origin: ChainAsset, - destination: ChainAsset, - recepient: DisplayAddress?, - xcmTransfers: XcmTransfers + origins: Set, + destination: ChainAsset ) { - guard let transferSetupView = TransferSetupViewFactory.createCrossChainView( - from: origin, + guard let transferView = TransferSetupViewFactory.createCrosschainView( + from: destination, to: destination, - xcmTransfers: xcmTransfers, - recepient: recepient + origins: origins, + transferCompletion: nil ) else { return } - view?.controller.navigationController?.pushViewController(transferSetupView.controller, animated: true) + let navigationController = NovaNavigationController(rootViewController: transferView.controller) + + view?.controller.present(navigationController, animated: true) } - func showDepositTokensByReceive( + func showGetTokensByReceive( from view: ControllerBackedProtocol?, chainAsset: ChainAsset, metaChainAccountResponse: MetaChainAccountResponse @@ -151,6 +178,8 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { return } - view?.controller.navigationController?.pushViewController(receiveTokensView.controller, animated: true) + let navigationController = NovaNavigationController(rootViewController: receiveTokensView.controller) + + view?.controller.present(navigationController, animated: true) } } diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift index 40cf09e92f..9c01d0fb39 100644 --- a/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift @@ -18,26 +18,13 @@ struct TransferSetupViewFactory { } } - static func createCrossChainView( - from chainAsset: ChainAsset, - to destinationChainAsset: ChainAsset, - xcmTransfers: XcmTransfers, - recepient: DisplayAddress?, - transferCompletion: TransferCompletionClosure? = nil + static func createCrosschainView( + from _: ChainAsset, + to _: ChainAsset, + origins _: Set, + transferCompletion _: TransferCompletionClosure? = nil ) -> TransferSetupViewProtocol? { - createView( - from: chainAsset, - recepient: recepient, - transferCompletion: transferCompletion - ) { factory, state, view in - factory.createCrossChainPresenter( - for: chainAsset, - destinationChainAsset: destinationChainAsset, - xcmTransfers: xcmTransfers, - initialState: state, - view: view - ) - } + nil } static func createView( From 8bea759b5a54860c13ae1ce70ab23ae0e31857a2 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sun, 12 Nov 2023 19:22:58 +0100 Subject: [PATCH 158/204] add option to crosschain tokens from get tokens menu --- novawallet.xcodeproj/project.pbxproj | 4 + .../ModalPicker/ModalNetworksFactory.swift | 8 +- .../GetTokenOptionsInteractor.swift | 22 +++- .../GetTokenOptionsPresenter.swift | 7 +- .../Model/GetTokenOptionsModel.swift | 4 +- .../Model/GetTokenOptionsResult.swift | 2 +- .../Swaps/Setup/SwapSetupWireframe.swift | 14 +- .../CrossChainDestinationSelectionState.swift | 10 +- .../Model/TransferSetupPeer.swift | 6 + .../TransferSetupInteractor.swift | 97 ++++++++++++-- .../TransferSetupPresenter.swift | 122 ++++++++++++------ .../TransferSetupProtocols.swift | 12 +- .../TransferSetupViewController.swift | 21 +-- .../TransferSetupViewFactory.swift | 100 ++++++++++---- .../View/TransferNetworkContainerView.swift | 60 ++++++--- .../TransferNetworkContainerViewModel.swift | 18 ++- 16 files changed, 361 insertions(+), 146 deletions(-) create mode 100644 novawallet/Modules/Transfer/TransferSetup/Model/TransferSetupPeer.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index c7b5a6f517..311d626ff3 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -258,6 +258,7 @@ 0CB64E5C2B009DA9008F268F /* GetTokenOptionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E5B2B009DA9008F268F /* GetTokenOptionsModel.swift */; }; 0CB64E5E2B00AA8F008F268F /* GetTokenOptionsResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E5D2B00AA8F008F268F /* GetTokenOptionsResult.swift */; }; 0CB64E602B00AD83008F268F /* GetTokenOptionsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E5F2B00AD83008F268F /* GetTokenOptionsWireframe.swift */; }; + 0CB64E622B012E92008F268F /* TransferSetupPeer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E612B012E92008F268F /* TransferSetupPeer.swift */; }; 0CBC29C62A421B5000F7B1F7 /* StakingMainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC29C52A421B5000F7B1F7 /* StakingMainWireframe.swift */; }; 0CBC29C82A42AE2F00F7B1F7 /* StakingDashboardBuilderResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC29C72A42AE2F00F7B1F7 /* StakingDashboardBuilderResult.swift */; }; 0CBF5DE72AB1A60500087EBF /* SharedOperationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBF5DE62AB1A60500087EBF /* SharedOperationStatus.swift */; }; @@ -4335,6 +4336,7 @@ 0CB64E5B2B009DA9008F268F /* GetTokenOptionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsModel.swift; sourceTree = ""; }; 0CB64E5D2B00AA8F008F268F /* GetTokenOptionsResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsResult.swift; sourceTree = ""; }; 0CB64E5F2B00AD83008F268F /* GetTokenOptionsWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsWireframe.swift; sourceTree = ""; }; + 0CB64E612B012E92008F268F /* TransferSetupPeer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferSetupPeer.swift; sourceTree = ""; }; 0CBC29C52A421B5000F7B1F7 /* StakingMainWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMainWireframe.swift; sourceTree = ""; }; 0CBC29C72A42AE2F00F7B1F7 /* StakingDashboardBuilderResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingDashboardBuilderResult.swift; sourceTree = ""; }; 0CBF5DE62AB1A60500087EBF /* SharedOperationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedOperationStatus.swift; sourceTree = ""; }; @@ -13071,6 +13073,7 @@ 8863C7AF29D49CB70068AD54 /* Web3NameViewModelFactory.swift */, 8846F71D29D5675E00B8B776 /* Web3NameRecipientListViewModel.swift */, 88840D8929DEA975002EFFFD /* TransferSetupRecipientAccount.swift */, + 0CB64E612B012E92008F268F /* TransferSetupPeer.swift */, ); path = Model; sourceTree = ""; @@ -22151,6 +22154,7 @@ 84300B2C26C10C9B00D64514 /* ConnectionStateReporting.swift in Sources */, 9E4E458C92D12B24D5EAD893 /* ControllerAccountInteractor.swift in Sources */, 83A98B972B3EA69B357E5002 /* ControllerAccountViewController.swift in Sources */, + 0CB64E622B012E92008F268F /* TransferSetupPeer.swift in Sources */, 84329ED22832636F0020BC1C /* RoundCountdown.swift in Sources */, 844A539529BF54BA00C77111 /* XcmPalletMetadataQueryFactory.swift in Sources */, 84FBED072927B3B000FBEB83 /* EvmTransactionHistoryUpdaterFactory.swift in Sources */, diff --git a/novawallet/Common/ViewController/ModalPicker/ModalNetworksFactory.swift b/novawallet/Common/ViewController/ModalPicker/ModalNetworksFactory.swift index 895c13d865..2b39696a51 100644 --- a/novawallet/Common/ViewController/ModalPicker/ModalNetworksFactory.swift +++ b/novawallet/Common/ViewController/ModalPicker/ModalNetworksFactory.swift @@ -54,7 +54,7 @@ enum ModalNetworksFactory { let networkViewModelFactory = NetworkViewModelFactory() let onChainViewModel = LocalizableResource { _ in - networkViewModelFactory.createViewModel(from: selectionState.originChain) + networkViewModelFactory.createViewModel(from: selectionState.chain) } let onChainTitle = LocalizableResource { locale in @@ -63,7 +63,7 @@ enum ModalNetworksFactory { viewController.addSection(viewModels: [onChainViewModel], title: onChainTitle) - let crossChainViewModels = selectionState.availableDestChains.map { chain in + let crossChainViewModels = selectionState.availablePeerChains.map { chain in LocalizableResource { _ in networkViewModelFactory.createViewModel(from: chain) } } @@ -73,10 +73,10 @@ enum ModalNetworksFactory { viewController.addSection(viewModels: crossChainViewModels, title: crossChainTitle) - if selectionState.selectedChainId == selectionState.originChain.chainId { + if selectionState.selectedChainId == selectionState.chain.chainId { viewController.selectedIndex = 0 viewController.selectedSection = 0 - } else if let index = selectionState.availableDestChains.firstIndex( + } else if let index = selectionState.availablePeerChains.firstIndex( where: { selectionState.selectedChainId == $0.chainId } ) { viewController.selectedIndex = index diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsInteractor.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsInteractor.swift index 4f2a12d221..ed260b50d3 100644 --- a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsInteractor.swift +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsInteractor.swift @@ -59,6 +59,7 @@ final class GetTokenOptionsInteractor { let model = GetTokenOptionsModel( availableXcmOrigins: availableXcmOrigins, + xcmTransfers: xcmTransfers, receiveAccount: receiveAvailable ? selectedAccount : nil, buyOptions: buyAvailable ? purchaseActions : [] ) @@ -66,24 +67,33 @@ final class GetTokenOptionsInteractor { presenter?.didReceive(model: model) } - private func determineAvailableXcmOrigins() -> Set { + private func determineAvailableXcmOrigins() -> [ChainAsset] { guard let xcmTransfers = xcmTransfers else { return [] } let balances = assetModelObservable.state.value.balances + let chains = assetModelObservable.state.value.allChains let availableOrigins = xcmTransfers .transferChainAssets(to: destinationChainAsset.chainAssetId) - .filter { chainAssetId in - if case let .success(balance) = balances[chainAssetId], balance.transferable > 0 { - return true + .compactMap { chainAssetId in + if + case let .success(balance) = balances[chainAssetId], + balance.transferable > 0, + let chain = chains[chainAssetId.chainId], + let asset = chain.asset(for: chainAssetId.assetId) { + return (ChainAsset(chain: chain, asset: asset), balance.transferable) } else { - return false + return nil } } + .sorted { balance1, balance2 in + balance1.1 > balance2.1 + } + .map(\.0) - return Set(availableOrigins) + return availableOrigins } private func setupBalances() { diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsPresenter.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsPresenter.swift index d9df290ef9..78546f19be 100644 --- a/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/GetTokenOptionsPresenter.swift @@ -71,7 +71,12 @@ extension GetTokenOptionsPresenter: GetTokenOptionsPresenterProtocol { switch allOperations[index] { case .crosschain: - wireframe.complete(on: view, result: .crosschains(model.availableXcmOrigins)) + if let xcmTransfers = model.xcmTransfers { + wireframe.complete( + on: view, + result: .crosschains(model.availableXcmOrigins, xcmTransfers) + ) + } case .receive: if let account = model.receiveAccount { wireframe.complete(on: view, result: .receive(account)) diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOptionsModel.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOptionsModel.swift index 7b2dc0f0cd..aa9343e447 100644 --- a/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOptionsModel.swift +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOptionsModel.swift @@ -1,7 +1,8 @@ import Foundation struct GetTokenOptionsModel { - let availableXcmOrigins: Set + let availableXcmOrigins: [ChainAsset] + let xcmTransfers: XcmTransfers? let receiveAccount: MetaChainAccountResponse? let buyOptions: [PurchaseAction] } @@ -10,6 +11,7 @@ extension GetTokenOptionsModel { static var empty: GetTokenOptionsModel { .init( availableXcmOrigins: [], + xcmTransfers: nil, receiveAccount: nil, buyOptions: [] ) diff --git a/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOptionsResult.swift b/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOptionsResult.swift index 834ec152c0..e4fd320bbc 100644 --- a/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOptionsResult.swift +++ b/novawallet/Modules/Swaps/Setup/GetTokenOptions/Model/GetTokenOptionsResult.swift @@ -1,7 +1,7 @@ import Foundation enum GetTokenOptionsResult { - case crosschains(Set) + case crosschains([ChainAsset], XcmTransfers) case receive(MetaChainAccountResponse) case buy([PurchaseAction]) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index 85eeceed7c..96c1957e03 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -114,11 +114,12 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { } switch result { - case let .crosschains(origins): + case let .crosschains(origins, xcmTransfers): self.showGetTokensByCrosschain( from: view, origins: origins, - destination: destinationChainAsset + destination: destinationChainAsset, + xcmTransfers: xcmTransfers ) case let .receive(account): self.showGetTokensByReceive( @@ -149,13 +150,14 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { func showGetTokensByCrosschain( from view: ControllerBackedProtocol?, - origins: Set, - destination: ChainAsset + origins: [ChainAsset], + destination: ChainAsset, + xcmTransfers: XcmTransfers ) { guard let transferView = TransferSetupViewFactory.createCrosschainView( - from: destination, + from: origins, to: destination, - origins: origins, + xcmTransfers: xcmTransfers, transferCompletion: nil ) else { return diff --git a/novawallet/Modules/Transfer/TransferSetup/Model/CrossChainDestinationSelectionState.swift b/novawallet/Modules/Transfer/TransferSetup/Model/CrossChainDestinationSelectionState.swift index 7e4552fa4e..5a0dcf53fb 100644 --- a/novawallet/Modules/Transfer/TransferSetup/Model/CrossChainDestinationSelectionState.swift +++ b/novawallet/Modules/Transfer/TransferSetup/Model/CrossChainDestinationSelectionState.swift @@ -1,13 +1,13 @@ import Foundation class CrossChainDestinationSelectionState { - let originChain: ChainModel - let availableDestChains: [ChainModel] + let chain: ChainModel + let availablePeerChains: [ChainModel] let selectedChainId: ChainModel.Id - init(originChain: ChainModel, availableDestChains: [ChainModel], selectedChainId: ChainModel.Id) { - self.originChain = originChain - self.availableDestChains = availableDestChains + init(chain: ChainModel, availablePeerChains: [ChainModel], selectedChainId: ChainModel.Id) { + self.chain = chain + self.availablePeerChains = availablePeerChains self.selectedChainId = selectedChainId } } diff --git a/novawallet/Modules/Transfer/TransferSetup/Model/TransferSetupPeer.swift b/novawallet/Modules/Transfer/TransferSetup/Model/TransferSetupPeer.swift new file mode 100644 index 0000000000..41c2ca903a --- /dev/null +++ b/novawallet/Modules/Transfer/TransferSetup/Model/TransferSetupPeer.swift @@ -0,0 +1,6 @@ +import Foundation + +enum TransferSetupPeer { + case origin + case destination +} diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupInteractor.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupInteractor.swift index 31b358d812..cffa03ca4b 100644 --- a/novawallet/Modules/Transfer/TransferSetup/TransferSetupInteractor.swift +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupInteractor.swift @@ -5,7 +5,8 @@ import SubstrateSdk final class TransferSetupInteractor: AccountFetching, AnyCancellableCleaning { weak var presenter: TransferSetupInteractorOutputProtocol? - let originChainAssetId: ChainAssetId + let chainAsset: ChainAsset + let whoChainAssetPeer: TransferSetupPeer let xcmTransfersSyncService: XcmTransfersSyncServiceProtocol let chainsStore: ChainsStoreProtocol let accountRepository: AnyDataProviderRepository @@ -13,17 +14,32 @@ final class TransferSetupInteractor: AccountFetching, AnyCancellableCleaning { let web3NamesService: Web3NameServiceProtocol? private var xcmTransfers: XcmTransfers? - private var destinationChainAsset: ChainAsset? + private var peerChainAsset: ChainAsset? + private var restrictedChainAssetPeers: [ChainAsset]? + + var destinationChainAsset: ChainAsset? { + switch whoChainAssetPeer { + case .origin: + return chainAsset + case .destination: + return peerChainAsset + } + } init( - originChainAssetId: ChainAssetId, + chainAsset: ChainAsset, + whoChainAssetPeer: TransferSetupPeer, + restrictedChainAssetPeers: [ChainAsset]?, xcmTransfersSyncService: XcmTransfersSyncServiceProtocol, chainsStore: ChainsStoreProtocol, accountRepository: AnyDataProviderRepository, web3NamesService: Web3NameServiceProtocol?, operationManager: OperationManagerProtocol ) { - self.originChainAssetId = originChainAssetId + self.chainAsset = chainAsset + self.whoChainAssetPeer = whoChainAssetPeer + peerChainAsset = restrictedChainAssetPeers?.first + self.restrictedChainAssetPeers = restrictedChainAssetPeers self.xcmTransfersSyncService = xcmTransfersSyncService self.chainsStore = chainsStore self.accountRepository = accountRepository @@ -57,14 +73,23 @@ final class TransferSetupInteractor: AccountFetching, AnyCancellableCleaning { private func provideAvailableTransfers() { guard let xcmTransfers = xcmTransfers else { - presenter?.didReceiveAvailableXcm(destinations: [], xcmTransfers: nil) + presenter?.didReceiveAvailableXcm(peerChainAssets: [], xcmTransfers: nil) return } - let transfers = xcmTransfers.transfers(from: originChainAssetId) + switch whoChainAssetPeer { + case .origin: + provideAvailableOrigins(for: xcmTransfers) + case .destination: + provideAvailableDestinations(for: xcmTransfers) + } + } + + private func provideAvailableDestinations(for xcmTransfers: XcmTransfers) { + let transfers = xcmTransfers.transfers(from: chainAsset.chainAssetId) guard !transfers.isEmpty else { - presenter?.didReceiveAvailableXcm(destinations: [], xcmTransfers: xcmTransfers) + presenter?.didReceiveAvailableXcm(peerChainAssets: [], xcmTransfers: xcmTransfers) return } @@ -79,10 +104,54 @@ final class TransferSetupInteractor: AccountFetching, AnyCancellableCleaning { return ChainAsset(chain: chain, asset: asset) } - presenter?.didReceiveAvailableXcm(destinations: destinations, xcmTransfers: xcmTransfers) + providePeerChainAssets(for: destinations, xcmTransfers: xcmTransfers) + } + + private func provideAvailableOrigins(for xcmTransfers: XcmTransfers) { + let transfers = xcmTransfers.transferChainAssets(to: chainAsset.chainAssetId) + + guard !transfers.isEmpty else { + presenter?.didReceiveAvailableXcm(peerChainAssets: [], xcmTransfers: xcmTransfers) + return + } + + let origins: [ChainAsset] = transfers.compactMap { chainAssetId in + guard + let chain = chainsStore.getChain(for: chainAssetId.chainId), + let asset = chain.asset(for: chainAssetId.assetId) + else { + return nil + } + + return ChainAsset(chain: chain, asset: asset) + } + + providePeerChainAssets(for: origins, xcmTransfers: xcmTransfers) } - private func fetchAccounts(for chain: ChainModel) { + private func providePeerChainAssets(for foundChainAssets: [ChainAsset], xcmTransfers: XcmTransfers) { + guard let restrictedChainAssetPeers = restrictedChainAssetPeers else { + presenter?.didReceiveAvailableXcm(peerChainAssets: foundChainAssets, xcmTransfers: xcmTransfers) + return + } + + let chainAssetIds = Set(foundChainAssets.map(\.chainAssetId)) + + let availableChainAssets = restrictedChainAssetPeers.filter { chainAssetIds.contains($0.chainAssetId) } + + presenter?.didReceiveAvailableXcm(peerChainAssets: availableChainAssets, xcmTransfers: xcmTransfers) + } + + private func fetchAccounts(for peerChain: ChainModel) { + let chain: ChainModel + + switch whoChainAssetPeer { + case .origin: + chain = chainAsset.chain + case .destination: + chain = peerChain + } + fetchAllMetaAccountChainResponses( for: chain.accountRequest(), repository: accountRepository, @@ -107,17 +176,17 @@ final class TransferSetupInteractor: AccountFetching, AnyCancellableCleaning { } extension TransferSetupInteractor: TransferSetupInteractorIntputProtocol { - func setup(destinationChainAsset: ChainAsset) { + func setup(peerChainAsset: ChainAsset) { setupChainsStore() setupXcmTransfersSyncService() - fetchAccounts(for: destinationChainAsset.chain) + fetchAccounts(for: peerChainAsset.chain) web3NamesService?.setup() - self.destinationChainAsset = destinationChainAsset + self.peerChainAsset = peerChainAsset } - func destinationChainAssetDidChanged(_ chainAsset: ChainAsset) { + func peerChainAssetDidChanged(_ chainAsset: ChainAsset) { fetchAccounts(for: chainAsset.chain) - destinationChainAsset = chainAsset + peerChainAsset = chainAsset } func search(web3Name: String) { diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupPresenter.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupPresenter.swift index 447303cd9e..c3054ca08d 100644 --- a/novawallet/Modules/Transfer/TransferSetup/TransferSetupPresenter.swift +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupPresenter.swift @@ -11,15 +11,16 @@ final class TransferSetupPresenter { let chainAssetViewModelFactory: ChainAssetViewModelFactoryProtocol let wallet: MetaAccountModel - let originChainAsset: ChainAsset + let chainAsset: ChainAsset + let whoChainAssetPeer: TransferSetupPeer let childPresenterFactory: TransferSetupPresenterFactoryProtocol let logger: LoggerProtocol let web3NameViewModelFactory: Web3NameViewModelFactoryProtocol var childPresenter: TransferSetupChildPresenterProtocol? - private(set) var destinationChainAsset: ChainAsset? - private(set) var availableDestinations: [ChainAsset]? + private(set) var peerChainAsset: ChainAsset? + private(set) var availablePeers: [ChainAsset]? private(set) var xcmTransfers: XcmTransfers? private(set) var recipientAddress: TransferSetupRecipientAccount? { didSet { @@ -35,19 +36,40 @@ final class TransferSetupPresenter { } private var metaChainAccountResponses: [MetaAccountChainResponse] = [] + + private var originChainAsset: ChainAsset? { + switch whoChainAssetPeer { + case .origin: + return peerChainAsset + case .destination: + return chainAsset + } + } + + private var destinationChainAsset: ChainAsset? { + switch whoChainAssetPeer { + case .origin: + return chainAsset + case .destination: + return peerChainAsset + } + } + private var destinationChainName: String { destinationChainAsset?.chain.name ?? "" } private var isOnChainTransfer: Bool { - destinationChainAsset == nil + peerChainAsset == nil } init( interactor: TransferSetupInteractorIntputProtocol, wireframe: TransferSetupWireframeProtocol, wallet: MetaAccountModel, - originChainAsset: ChainAsset, + chainAsset: ChainAsset, + whoChainAssetPeer: TransferSetupPeer, + chainAssetPeers: [ChainAsset]?, childPresenterFactory: TransferSetupPresenterFactoryProtocol, chainAssetViewModelFactory: ChainAssetViewModelFactoryProtocol, networkViewModelFactory: NetworkViewModelFactoryProtocol, @@ -57,7 +79,10 @@ final class TransferSetupPresenter { self.interactor = interactor self.wireframe = wireframe self.wallet = wallet - self.originChainAsset = originChainAsset + self.chainAsset = chainAsset + self.whoChainAssetPeer = whoChainAssetPeer + peerChainAsset = chainAssetPeers?.first + availablePeers = chainAssetPeers self.childPresenterFactory = childPresenterFactory self.chainAssetViewModelFactory = chainAssetViewModelFactory self.networkViewModelFactory = networkViewModelFactory @@ -73,7 +98,7 @@ final class TransferSetupPresenter { let initialState = childPresenter?.inputState ?? TransferSetupInputState() childPresenter = childPresenterFactory.createOnChainPresenter( - for: originChainAsset, + for: chainAsset, initialState: initialState, view: view ) @@ -86,6 +111,7 @@ final class TransferSetupPresenter { private func setupCrossChainChildPresenter() { guard let view = view, + let originChainAsset = originChainAsset, let destinationChainAsset = destinationChainAsset, let xcmTransfers = xcmTransfers else { return @@ -107,19 +133,33 @@ final class TransferSetupPresenter { } private func provideChainsViewModel() { - let originViewModel = chainAssetViewModelFactory.createViewModel(from: originChainAsset) + let mode: TransferNetworkContainerViewModel.Mode + let chainAssetViewModel = networkViewModelFactory.createViewModel(from: chainAsset.chain) - let destinationViewModel: NetworkViewModel? + let optPeerChainAsset: ChainAsset? - if let destinationChainAsset = destinationChainAsset { - destinationViewModel = networkViewModelFactory.createViewModel(from: destinationChainAsset.chain) - } else if let availableDestinations = availableDestinations, !availableDestinations.isEmpty { - destinationViewModel = networkViewModelFactory.createViewModel(from: originChainAsset.chain) + if let availablePeers = availablePeers, !availablePeers.isEmpty { + optPeerChainAsset = peerChainAsset ?? chainAsset } else { - destinationViewModel = nil + optPeerChainAsset = peerChainAsset } - view?.didReceiveOriginChain(originViewModel, destinationChain: destinationViewModel) + if let peerChainAsset = optPeerChainAsset { + let peerAssetViewModel = networkViewModelFactory.createViewModel(from: peerChainAsset.chain) + + switch whoChainAssetPeer { + case .origin: + mode = .selectableOrigin(peerAssetViewModel, chainAssetViewModel) + case .destination: + mode = .selectableDestination(chainAssetViewModel, peerAssetViewModel) + } + } else { + mode = .onchain(chainAssetViewModel) + } + + let viewModel = TransferNetworkContainerViewModel(assetSymbol: chainAsset.asset.symbol, mode: mode) + + view?.didReceiveSelection(viewModel: viewModel) } private func getYourWallets() -> [MetaAccountChainResponse] { @@ -141,7 +181,7 @@ final class TransferSetupPresenter { return } - let chain = destinationChainAsset?.chain ?? originChainAsset.chain + let chain = destinationChainAsset?.chain ?? chainAsset.chain view.didReceiveWeb3NameRecipient(viewModel: .cached(value: nil)) let viewModel = web3NameViewModelFactory.recipientListViewModel( recipients: recipients, @@ -162,7 +202,7 @@ final class TransferSetupPresenter { return } - let chain = destinationChainAsset?.chain ?? originChainAsset.chain + let chain = destinationChainAsset?.chain ?? chainAsset.chain if let account = recipient.normalizedAddress(for: chain.chainFormat) { let recipientViewModel = TransferSetupRecipientAccount.ExternalAccountValue( @@ -198,7 +238,7 @@ extension TransferSetupPresenter: TransferSetupPresenterProtocol { provideChainsViewModel() childPresenter?.setup() - interactor.setup(destinationChainAsset: destinationChainAsset ?? originChainAsset) + interactor.setup(peerChainAsset: peerChainAsset ?? chainAsset) } func updateRecepient(partialAddress: String) { @@ -247,15 +287,15 @@ extension TransferSetupPresenter: TransferSetupPresenterProtocol { } } - func changeDestinationChain() { - let originChain = originChainAsset.chain - let selectedChainId = destinationChainAsset?.chain.chainId ?? originChain.chainId + func selectChain() { + let chain = chainAsset.chain + let selectedChainId = peerChainAsset?.chain.chainId ?? chain.chainId - let availableDestinationChains = availableDestinations?.map(\.chain) ?? [] + let availablePeerChains = availablePeers?.map(\.chain) ?? [] let selectionState = CrossChainDestinationSelectionState( - originChain: originChain, - availableDestChains: availableDestinationChains, + chain: chain, + availablePeerChains: availablePeerChains, selectedChainId: selectedChainId ) @@ -296,7 +336,7 @@ extension TransferSetupPresenter: TransferSetupPresenterProtocol { return } - let chain = destinationChainAsset?.chain ?? originChainAsset.chain + let chain = destinationChainAsset?.chain ?? chainAsset.chain wireframe.presentAccountOptions( from: view, @@ -308,18 +348,18 @@ extension TransferSetupPresenter: TransferSetupPresenterProtocol { } extension TransferSetupPresenter: TransferSetupInteractorOutputProtocol { - func didReceiveAvailableXcm(destinations: [ChainAsset], xcmTransfers: XcmTransfers?) { - let symbol = originChainAsset.asset.symbol - let chainName = originChainAsset.chain.name - logger.debug("(\(chainName) \(symbol) Available destinations: \(destinations.count)") + func didReceiveAvailableXcm(peerChainAssets: [ChainAsset], xcmTransfers: XcmTransfers?) { + let symbol = chainAsset.asset.symbol + let chainName = chainAsset.chain.name + logger.debug("(\(chainName) \(symbol) Available peers: \(peerChainAssets.count)") - availableDestinations = destinations + availablePeers = peerChainAssets self.xcmTransfers = xcmTransfers if - let destinationChainAsset = destinationChainAsset, - !destinations.contains(where: { $0.chainAssetId == destinationChainAsset.chainAssetId }) { - self.destinationChainAsset = nil + let peerChainAsset = peerChainAsset, + !peerChainAssets.contains(where: { $0.chainAssetId == peerChainAsset.chainAssetId }) { + self.peerChainAsset = nil setupOnChainChildPresenter() } @@ -363,7 +403,7 @@ extension TransferSetupPresenter: TransferSetupInteractorOutputProtocol { extension TransferSetupPresenter: ModalPickerViewControllerDelegate { func modalPickerDidSelectModel(at index: Int, section: Int, context: AnyObject?) { - view?.didCompleteDestinationSelection() + view?.didCompleteChainSelection() guard let selectionState = context as? CrossChainDestinationSelectionState else { return @@ -375,23 +415,23 @@ extension TransferSetupPresenter: ModalPickerViewControllerDelegate { } if section == 0 { - destinationChainAsset = nil + peerChainAsset = nil } else { - let selectedChain = selectionState.availableDestChains[index] + let selectedChain = selectionState.availablePeerChains[index] let selectedChainId = selectedChain.chainId - destinationChainAsset = availableDestinations?.first { $0.chain.chainId == selectedChainId } + peerChainAsset = availablePeers?.first { $0.chain.chainId == selectedChainId } } provideChainsViewModel() updateYourWalletsButton() - if let destinationChainAsset = destinationChainAsset { + if let peerChainAsset = peerChainAsset { setupCrossChainChildPresenter() - interactor.destinationChainAssetDidChanged(destinationChainAsset) + interactor.peerChainAssetDidChanged(peerChainAsset) } else { setupOnChainChildPresenter() - interactor.destinationChainAssetDidChanged(originChainAsset) + interactor.peerChainAssetDidChanged(chainAsset) } } @@ -406,7 +446,7 @@ extension TransferSetupPresenter: ModalPickerViewControllerDelegate { func modalPickerDidCancel(context: AnyObject?) { if context is CrossChainDestinationSelectionState { - view?.didCompleteDestinationSelection() + view?.didCompleteChainSelection() } else if context is Web3NameAddressesSelectionState { view?.didReceiveRecipientInputState(focused: true, empty: nil) } diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupProtocols.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupProtocols.swift index 6f046435ca..6cf3d7e216 100644 --- a/novawallet/Modules/Transfer/TransferSetup/TransferSetupProtocols.swift +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupProtocols.swift @@ -15,8 +15,8 @@ protocol TransferSetupChildViewProtocol: ControllerBackedProtocol, Localizable { } protocol TransferSetupViewProtocol: TransferSetupChildViewProtocol { - func didReceiveOriginChain(_ originChain: ChainAssetViewModel, destinationChain: NetworkViewModel?) - func didCompleteDestinationSelection() + func didReceiveSelection(viewModel: TransferNetworkContainerViewModel) + func didCompleteChainSelection() func didSwitchCrossChain() func didSwitchOnChain() func changeYourWalletsViewState(_ state: YourWalletsControl.State) @@ -39,7 +39,7 @@ protocol TransferSetupChildPresenterProtocol: TransferSetupCommonPresenterProtoc } protocol TransferSetupPresenterProtocol: TransferSetupCommonPresenterProtocol { - func changeDestinationChain() + func selectChain() func scanRecepientCode() func applyMyselfRecepient() func didTapOnYourWallets() @@ -48,13 +48,13 @@ protocol TransferSetupPresenterProtocol: TransferSetupCommonPresenterProtocol { } protocol TransferSetupInteractorIntputProtocol: AnyObject { - func setup(destinationChainAsset: ChainAsset) - func destinationChainAssetDidChanged(_ chainAsset: ChainAsset) + func setup(peerChainAsset: ChainAsset) + func peerChainAssetDidChanged(_ chainAsset: ChainAsset) func search(web3Name: String) } protocol TransferSetupInteractorOutputProtocol: AnyObject { - func didReceiveAvailableXcm(destinations: [ChainAsset], xcmTransfers: XcmTransfers?) + func didReceiveAvailableXcm(peerChainAssets: [ChainAsset], xcmTransfers: XcmTransfers?) func didReceive(error: Error) func didReceive(metaChainAccountResponses: [MetaAccountChainResponse]) func didReceive(recipients: [Web3TransferRecipient], for name: String) diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewController.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewController.swift index 442d8f9624..600ae54738 100644 --- a/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewController.swift +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewController.swift @@ -192,8 +192,8 @@ final class TransferSetupViewController: UIViewController, ViewHolder { presenter.proceed() } - @objc func actionChangeDestination() { - presenter.changeDestinationChain() + @objc func actionChangeChain() { + presenter.selectChain() } @objc func actionSendMyself() { @@ -219,25 +219,18 @@ extension TransferSetupViewController: TransferSetupViewProtocol { rootView.yourWalletsControl.apply(state: state) } - func didReceiveOriginChain(_ originChain: ChainAssetViewModel, destinationChain: NetworkViewModel?) { - let assetViewModel = originChain.assetViewModel - let viewModel = TransferNetworkContainerViewModel( - assetSymbol: assetViewModel.symbol, - originNetwork: originChain.networkViewModel, - destNetwork: destinationChain - ) - + func didReceiveSelection(viewModel: TransferNetworkContainerViewModel) { rootView.networkContainerView.bind(viewModel: viewModel) - rootView.networkContainerView.destinationNetworkView?.actionControl.addTarget( + rootView.networkContainerView.selectableNetworkView?.actionControl.addTarget( self, - action: #selector(actionChangeDestination), + action: #selector(actionChangeChain), for: .touchUpInside ) } - func didCompleteDestinationSelection() { - rootView.networkContainerView.destinationNetworkView?.actionControl.deactivate(animated: true) + func didCompleteChainSelection() { + rootView.networkContainerView.selectableNetworkView?.actionControl.deactivate(animated: true) } func didReceiveInputChainAsset(viewModel: ChainAssetViewModel) { diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift index 9c01d0fb39..fb1e41cce4 100644 --- a/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift @@ -1,47 +1,68 @@ import Foundation import SoraFoundation - import RobinHood -struct TransferSetupViewFactory { +struct TransferSetupViewParams { + let chainAsset: ChainAsset + let whoChainAssetPeer: TransferSetupPeer + let chainAssetPeers: [ChainAsset]? + let recepient: DisplayAddress? + let xcmTransfers: XcmTransfers? +} + +enum TransferSetupViewFactory { static func createView( from chainAsset: ChainAsset, recepient: DisplayAddress?, transferCompletion: TransferCompletionClosure? = nil ) -> TransferSetupViewProtocol? { createView( - from: chainAsset, - recepient: recepient, + from: .init( + chainAsset: chainAsset, + whoChainAssetPeer: .destination, + chainAssetPeers: nil, + recepient: recepient, + xcmTransfers: nil + ), transferCompletion: transferCompletion - ) { factory, state, view in - factory.createOnChainPresenter(for: chainAsset, initialState: state, view: view) - } + ) } static func createCrosschainView( - from _: ChainAsset, - to _: ChainAsset, - origins _: Set, - transferCompletion _: TransferCompletionClosure? = nil + from origins: [ChainAsset], + to destination: ChainAsset, + xcmTransfers: XcmTransfers?, + transferCompletion: TransferCompletionClosure? = nil ) -> TransferSetupViewProtocol? { - nil + guard !origins.isEmpty else { + return nil + } + + return createView( + from: .init( + chainAsset: destination, + whoChainAssetPeer: .origin, + chainAssetPeers: origins, + recepient: nil, + xcmTransfers: xcmTransfers + ), + transferCompletion: transferCompletion + ) } static func createView( - from chainAsset: ChainAsset, - recepient: DisplayAddress?, - transferCompletion: TransferCompletionClosure?, - createChildPresenterClosure: (TransferSetupPresenterFactoryProtocol, TransferSetupInputState, TransferSetupChildViewProtocol) -> TransferSetupChildPresenterProtocol? + from params: TransferSetupViewParams, + transferCompletion: TransferCompletionClosure? ) -> TransferSetupViewProtocol? { guard let wallet = SelectedWalletSettings.shared.value else { return nil } - guard let interactor = createInteractor(for: chainAsset) else { + guard let interactor = createInteractor(for: params) else { return nil } - let initPresenterState = TransferSetupInputState(recepient: recepient?.address, amount: nil) + let initPresenterState = TransferSetupInputState(recepient: params.recepient?.address, amount: nil) let presenterFactory = createPresenterFactory(for: wallet, transferCompletion: transferCompletion) @@ -59,7 +80,9 @@ struct TransferSetupViewFactory { interactor: interactor, wireframe: wireframe, wallet: wallet, - originChainAsset: chainAsset, + chainAsset: params.chainAsset, + whoChainAssetPeer: params.whoChainAssetPeer, + chainAssetPeers: params.chainAssetPeers, childPresenterFactory: presenterFactory, chainAssetViewModelFactory: chainAssetViewModelFactory, networkViewModelFactory: networkViewModelFactory, @@ -72,7 +95,36 @@ struct TransferSetupViewFactory { localizationManager: localizationManager ) - presenter.childPresenter = createChildPresenterClosure(presenterFactory, initPresenterState, view) + if + let peerChainAsset = params.chainAssetPeers?.first, + peerChainAsset.chainAssetId != params.chainAsset.chainAssetId, + let xcmTransfers = params.xcmTransfers { + let origin: ChainAsset + let destination: ChainAsset + + switch params.whoChainAssetPeer { + case .origin: + origin = peerChainAsset + destination = params.chainAsset + case .destination: + origin = params.chainAsset + destination = peerChainAsset + } + + presenter.childPresenter = presenterFactory.createCrossChainPresenter( + for: origin, + destinationChainAsset: destination, + xcmTransfers: xcmTransfers, + initialState: initPresenterState, + view: view + ) + } else { + presenter.childPresenter = presenterFactory.createOnChainPresenter( + for: params.chainAsset, + initialState: initPresenterState, + view: view + ) + } presenter.view = view interactor.presenter = presenter @@ -94,9 +146,7 @@ struct TransferSetupViewFactory { ) } - private static func createInteractor( - for chainAsset: ChainAsset - ) -> TransferSetupInteractor? { + private static func createInteractor(for params: TransferSetupViewParams) -> TransferSetupInteractor? { let chainRegistry = ChainRegistryFacade.sharedRegistry let syncService = XcmTransfersSyncService( @@ -113,7 +163,9 @@ struct TransferSetupViewFactory { let web3NameService = createWeb3NameService() return TransferSetupInteractor( - originChainAssetId: chainAsset.chainAssetId, + chainAsset: params.chainAsset, + whoChainAssetPeer: params.whoChainAssetPeer, + restrictedChainAssetPeers: params.chainAssetPeers, xcmTransfersSyncService: syncService, chainsStore: chainsStore, accountRepository: accountRepository, diff --git a/novawallet/Modules/Transfer/View/TransferNetworkContainerView.swift b/novawallet/Modules/Transfer/View/TransferNetworkContainerView.swift index 606258bce8..50aa1abb9a 100644 --- a/novawallet/Modules/Transfer/View/TransferNetworkContainerView.swift +++ b/novawallet/Modules/Transfer/View/TransferNetworkContainerView.swift @@ -9,8 +9,8 @@ final class TransferNetworkContainerView: UIView { var horizontalSpacing: CGFloat = 6.0 var verticalSpacing: CGFloat = 7.0 - let originNetworkView = AssetListChainView() - private(set) var destinationNetworkView: AssetListChainControlView? + let staticNetworkView = AssetListChainView() + private(set) var selectableNetworkView: AssetListChainControlView? var locale = Locale.current { didSet { @@ -24,14 +24,14 @@ final class TransferNetworkContainerView: UIView { private var viewModel: TransferNetworkContainerViewModel? - private var isCrossChain: Bool { viewModel?.destNetwork != nil } + private var isCrossChain: Bool { viewModel?.isCrosschain ?? false } override init(frame: CGRect) { super.init(frame: frame) addSubview(tokenLabel) addSubview(fromLabel) - addSubview(originNetworkView) + addSubview(staticNetworkView) } @available(*, unavailable) @@ -72,29 +72,36 @@ final class TransferNetworkContainerView: UIView { func bind(viewModel: TransferNetworkContainerViewModel) { self.viewModel = viewModel - originNetworkView.bind(viewModel: viewModel.originNetwork) + switch viewModel.mode { + case let .onchain(networkViewModel): + setupOnChain() - if let destViewModel = viewModel.destNetwork { + staticNetworkView.bind(viewModel: networkViewModel) + case let .selectableOrigin(origin, destination): setupCrossChain() - destinationNetworkView?.bind(viewModel: destViewModel) - } else { - setupOnChain() + staticNetworkView.bind(viewModel: destination) + selectableNetworkView?.bind(viewModel: origin) + case let .selectableDestination(origin, destination): + setupCrossChain() + + staticNetworkView.bind(viewModel: origin) + selectableNetworkView?.bind(viewModel: destination) } setupLocalization() } private func setupOnChain() { - destinationNetworkView?.removeFromSuperview() - destinationNetworkView = nil + selectableNetworkView?.removeFromSuperview() + selectableNetworkView = nil toLabel?.removeFromSuperview() toLabel = nil } private func setupCrossChain() { - guard destinationNetworkView == nil else { + guard selectableNetworkView == nil else { return } @@ -102,9 +109,9 @@ final class TransferNetworkContainerView: UIView { addSubview(label) toLabel = label - let destNetworkView = AssetListChainControlView() - addSubview(destNetworkView) - destinationNetworkView = destNetworkView + let networkView = AssetListChainControlView() + addSubview(networkView) + selectableNetworkView = networkView } override var intrinsicContentSize: CGSize { @@ -114,9 +121,20 @@ final class TransferNetworkContainerView: UIView { override func layoutSubviews() { super.layoutSubviews() + let originView: UIView? + let destinationView: UIView? + + if case .selectableOrigin = viewModel?.mode, let selectableNetworkView = selectableNetworkView { + originView = selectableNetworkView + destinationView = staticNetworkView + } else { + originView = staticNetworkView + destinationView = selectableNetworkView + } + let tokenLabelSize = tokenLabel.intrinsicContentSize let fromLabelSize = fromLabel.intrinsicContentSize - let originViewSize = originNetworkView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + let originViewSize = originView?.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) ?? .zero let totalOneLineWidth = tokenLabelSize.width + horizontalSpacing + fromLabelSize.width + horizontalSpacing + originViewSize.width @@ -133,7 +151,7 @@ final class TransferNetworkContainerView: UIView { height: fromLabelSize.height ) - originNetworkView.frame = CGRect( + originView?.frame = CGRect( x: fromLabel.frame.maxX + horizontalSpacing, y: tokenLabel.frame.midY - originViewSize.height / 2.0, width: originViewSize.width, @@ -149,7 +167,7 @@ final class TransferNetworkContainerView: UIView { height: fromLabelSize.height ) - originNetworkView.frame = CGRect( + originView?.frame = CGRect( x: fromLabel.frame.maxX + horizontalSpacing, y: fromLabel.frame.midY - originViewSize.height / 2.0, width: originViewSize.width, @@ -160,9 +178,9 @@ final class TransferNetworkContainerView: UIView { max(fromLabelSize.height, originViewSize.height) } - if let destinationNetworkView = destinationNetworkView, let toLabel = toLabel { + if let destinationView = destinationView, let toLabel = toLabel { let toLabelSize = toLabel.intrinsicContentSize - let destViewSize = destinationNetworkView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + let destViewSize = destinationView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) toLabel.frame = CGRect( x: bounds.minX, @@ -171,7 +189,7 @@ final class TransferNetworkContainerView: UIView { height: toLabelSize.height ) - destinationNetworkView.frame = CGRect( + destinationView.frame = CGRect( x: toLabel.frame.maxX + horizontalSpacing, y: toLabel.frame.midY - originViewSize.height / 2.0, width: destViewSize.width, diff --git a/novawallet/Modules/Transfer/View/TransferNetworkContainerViewModel.swift b/novawallet/Modules/Transfer/View/TransferNetworkContainerViewModel.swift index f8d244a613..f85d629552 100644 --- a/novawallet/Modules/Transfer/View/TransferNetworkContainerViewModel.swift +++ b/novawallet/Modules/Transfer/View/TransferNetworkContainerViewModel.swift @@ -1,7 +1,21 @@ import Foundation struct TransferNetworkContainerViewModel { + enum Mode { + case onchain(NetworkViewModel) + case selectableOrigin(NetworkViewModel, NetworkViewModel) + case selectableDestination(NetworkViewModel, NetworkViewModel) + } + let assetSymbol: String - let originNetwork: NetworkViewModel - let destNetwork: NetworkViewModel? + let mode: Mode + + var isCrosschain: Bool { + switch mode { + case .onchain: + return false + case .selectableDestination, .selectableOrigin: + return true + } + } } From 56248f30f724f0a290eba62815efb6df6e24dcc9 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 13 Nov 2023 07:58:08 +0100 Subject: [PATCH 159/204] add network selection bottomsheet --- novawallet.xcodeproj/project.pbxproj | 68 +++++++++- .../Swaps/Setup/SwapSetupWireframe.swift | 1 + .../TransferNetworkSelectionInteractor.swift | 36 ++++++ .../TransferNetworkSelectionPresenter.swift | 70 +++++++++++ .../TransferNetworkSelectionProtocols.swift | 17 +++ ...ansferNetworkSelectionViewController.swift | 34 +++++ .../TransferNetworkSelectionViewFactory.swift | 75 +++++++++++ .../View/TransferNetworkSelectionCell.swift | 58 +++++++++ .../TransferNetworkSelectionViewModel.swift | 6 + ...e.swift => CrossChainSelectionState.swift} | 10 ++ ...ransferSetupOriginSelectionWireframe.swift | 26 ++++ .../TransferSetupPresenter.swift | 116 +++++++++++------- .../TransferSetupProtocols.swift | 10 +- .../TransferSetupViewFactory.swift | 6 +- .../TransferSetupWireframe.swift | 51 ++++++-- .../TransferNetworkSelectionTests.swift | 16 +++ 16 files changed, 540 insertions(+), 60 deletions(-) create mode 100644 novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionInteractor.swift create mode 100644 novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionPresenter.swift create mode 100644 novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionProtocols.swift create mode 100644 novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionViewController.swift create mode 100644 novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionViewFactory.swift create mode 100644 novawallet/Modules/Transfer/TransferNetworkSelection/View/TransferNetworkSelectionCell.swift create mode 100644 novawallet/Modules/Transfer/TransferNetworkSelection/View/TransferNetworkSelectionViewModel.swift rename novawallet/Modules/Transfer/TransferSetup/Model/{CrossChainDestinationSelectionState.swift => CrossChainSelectionState.swift} (53%) create mode 100644 novawallet/Modules/Transfer/TransferSetup/TransferSetupOriginSelectionWireframe.swift create mode 100644 novawalletTests/Modules/TransferNetworkSelection/TransferNetworkSelectionTests.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 311d626ff3..ebb92da543 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 0B2B9C6E2BA2E924D6A54F4B /* CrowdloanListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E78D69E8EBC3EB4D01F8EF /* CrowdloanListInteractor.swift */; }; 0B48B02E973CB304B765BBC9 /* ReferendumDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ABAD23C0039AFA8351C650 /* ReferendumDetailsProtocols.swift */; }; 0B65DAE0327678679CACE0B1 /* GovernanceDelegateInfoViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E894D4633D04AD4415CE1F2 /* GovernanceDelegateInfoViewFactory.swift */; }; + 0BB2E3FF30B1700D321C526A /* TransferNetworkSelectionViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA463769D0F411429780D7D /* TransferNetworkSelectionViewFactory.swift */; }; 0C0CB37F2AC540B200EAC516 /* AssetConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB37E2AC540B200EAC516 /* AssetConversion.swift */; }; 0C0CB3822AC545A800EAC516 /* AssetConversionExtrinsicService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3812AC545A800EAC516 /* AssetConversionExtrinsicService.swift */; }; 0C0CB3852AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3842AC561FB00EAC516 /* AssetHubSwapOperationFactory.swift */; }; @@ -259,6 +260,9 @@ 0CB64E5E2B00AA8F008F268F /* GetTokenOptionsResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E5D2B00AA8F008F268F /* GetTokenOptionsResult.swift */; }; 0CB64E602B00AD83008F268F /* GetTokenOptionsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E5F2B00AD83008F268F /* GetTokenOptionsWireframe.swift */; }; 0CB64E622B012E92008F268F /* TransferSetupPeer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E612B012E92008F268F /* TransferSetupPeer.swift */; }; + 0CB64E652B01E0CC008F268F /* TransferNetworkSelectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E642B01E0CC008F268F /* TransferNetworkSelectionCell.swift */; }; + 0CB64E672B01E174008F268F /* TransferNetworkSelectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E662B01E174008F268F /* TransferNetworkSelectionViewModel.swift */; }; + 0CB64E692B01F798008F268F /* TransferSetupOriginSelectionWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB64E682B01F798008F268F /* TransferSetupOriginSelectionWireframe.swift */; }; 0CBC29C62A421B5000F7B1F7 /* StakingMainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC29C52A421B5000F7B1F7 /* StakingMainWireframe.swift */; }; 0CBC29C82A42AE2F00F7B1F7 /* StakingDashboardBuilderResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC29C72A42AE2F00F7B1F7 /* StakingDashboardBuilderResult.swift */; }; 0CBF5DE72AB1A60500087EBF /* SharedOperationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBF5DE62AB1A60500087EBF /* SharedOperationStatus.swift */; }; @@ -470,6 +474,7 @@ 3441DDC002503A0DC9A8A925 /* ReferendumSearchViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B179EA3EF793684717BA9D68 /* ReferendumSearchViewFactory.swift */; }; 347BBBBCC84CA155006FDCDB /* GovernanceSelectTracksViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3593D7650F5126266ED9FE84 /* GovernanceSelectTracksViewLayout.swift */; }; 34D6FF85BEA25EFD1D15D460 /* InAppUpdatesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15E09DD01C1CC61EA5CDED9C /* InAppUpdatesInteractor.swift */; }; + 34E4B25CDFE0D7B5F4F18185 /* TransferNetworkSelectionInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 486BF5FBE285713D05CF95A8 /* TransferNetworkSelectionInteractor.swift */; }; 350B8A18C9C91DF07D2E53C5 /* SwapSetupViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD43373A9159E0A592077BB /* SwapSetupViewLayout.swift */; }; 355476A5AECD2FFE4ED3DE39 /* MessageSheetViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A719A9FC28373296AB195CB /* MessageSheetViewLayout.swift */; }; 3592E885646B3ED9F2717412 /* GovernanceRevokeDelegationTracksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CAF58F7D0659E89B66B75E4 /* GovernanceRevokeDelegationTracksViewController.swift */; }; @@ -550,6 +555,7 @@ 493A9637BE5A1BF4B0744A4C /* ChangeWatchOnlyInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E4CD58A9006CEB045E8977 /* ChangeWatchOnlyInteractor.swift */; }; 4A24646D497B26E51926BA52 /* StakingRebagConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74A26B3915B6E0C8C784423 /* StakingRebagConfirmViewFactory.swift */; }; 4A520B7081BE2D7604B69354 /* AccountImportWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F45A5C6145F863760F4409 /* AccountImportWireframe.swift */; }; + 4AEF5B442FA0AF1F515AE6B5 /* TransferNetworkSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214A7ED74890344B6FBE765A /* TransferNetworkSelectionTests.swift */; }; 4B1FA597B618713C75917816 /* GovernanceYourDelegationsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2FCBFC59ED9D7E6F046D2A1 /* GovernanceYourDelegationsProtocols.swift */; }; 4B4189889DEFAF917332D41C /* ChangeWatchOnlyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FECFFBAB264397F9B2646CE /* ChangeWatchOnlyViewController.swift */; }; 4B83231E151422897F71408F /* GovernanceSelectTracksInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D9A2CF1016F4D5A7F075B69 /* GovernanceSelectTracksInteractor.swift */; }; @@ -649,6 +655,7 @@ 65C06FCE82EEC0B476DB1CEF /* DAppBrowserProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E38ABE379CA48E63328C4 /* DAppBrowserProtocols.swift */; }; 65CD159259A06EC3E92FD4B0 /* AssetDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 998CDEAB9F149770B27F5317 /* AssetDetailsProtocols.swift */; }; 663DB041307C59E939BF0BE2 /* ParitySignerAddConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44809BCF44D7329266A60A9D /* ParitySignerAddConfirmInteractor.swift */; }; + 66436210FD782DAF13814711 /* TransferNetworkSelectionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAE1DE626A1960428C9AEC52 /* TransferNetworkSelectionPresenter.swift */; }; 66531C7E2E0E99C89A89A35A /* SwapSetupViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A6BCE8F8DB7576E1E5B5974 /* SwapSetupViewFactory.swift */; }; 671C5788468FE8445A46C09F /* AdvancedWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 597A3C3F2937333D0EC7ABD5 /* AdvancedWalletViewController.swift */; }; 67684F7576ED0252C1050CA5 /* OperationDetailsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D569738955713647612599 /* OperationDetailsViewLayout.swift */; }; @@ -891,6 +898,7 @@ 78D94A761EFECED60F38232D /* CustomValidatorListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 270B309EC85D8897A4ADD98A /* CustomValidatorListViewController.swift */; }; 78E0B6963A8D0A07E742232C /* GovernanceEditDelegationTracksWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3B84FA1F22CC12B16C79AE /* GovernanceEditDelegationTracksWireframe.swift */; }; 790129F3CB6AEA611639E886 /* ParaStkUnstakeViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5467B9B6AEDB33F565D130A1 /* ParaStkUnstakeViewFactory.swift */; }; + 7A47B0731BC8B411CC01C3D1 /* TransferNetworkSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D6106D11494F979A46F7B4C /* TransferNetworkSelectionViewController.swift */; }; 7BD09D3022967C4D90AB4693 /* DAppOperationConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859E0EF774DF0D498FEF8FCB /* DAppOperationConfirmViewLayout.swift */; }; 7C0135CA49EF6B535030643E /* ParaStkYieldBoostSetupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C9EEC5FDEA7DAE42F2880C7 /* ParaStkYieldBoostSetupPresenter.swift */; }; 7C4CB158ED48716626780F40 /* LedgerPerformOperationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4809C2DB0C15A9B2890C1AC6 /* LedgerPerformOperationInteractor.swift */; }; @@ -1244,7 +1252,7 @@ 842AEB81292F34B600C61B0C /* RemoteChainExternalApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842AEB80292F34B600C61B0C /* RemoteChainExternalApi.swift */; }; 842B17FB28648FDC0014CC57 /* ChainAssetViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842B17FA28648FDC0014CC57 /* ChainAssetViewModelFactory.swift */; }; 842B17FD2864980B0014CC57 /* NetworkSelectionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842B17FC2864980B0014CC57 /* NetworkSelectionTableViewCell.swift */; }; - 842B17FF28649CCD0014CC57 /* CrossChainDestinationSelectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842B17FE28649CCD0014CC57 /* CrossChainDestinationSelectionState.swift */; }; + 842B17FF28649CCD0014CC57 /* CrossChainSelectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842B17FE28649CCD0014CC57 /* CrossChainSelectionState.swift */; }; 842B18022864F9950014CC57 /* CrossChainTransferPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842B18012864F9950014CC57 /* CrossChainTransferPresenter.swift */; }; 842B18042864F9A90014CC57 /* CrossChainTransferInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842B18032864F9A90014CC57 /* CrossChainTransferInteractor.swift */; }; 842B1806286506EE0014CC57 /* CrossChainTransferSetupProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842B1805286506EE0014CC57 /* CrossChainTransferSetupProtocols.swift */; }; @@ -3904,6 +3912,7 @@ F382BF4F8C3C46C7C21DE5C0 /* ParaStkUnstakeConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86DCB6F3977BDE1BDC7BC3F9 /* ParaStkUnstakeConfirmPresenter.swift */; }; F3BB50CCA38C9B47FDBEDF53 /* ReferendumVotersInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23CC3812E4DFC26484324D57 /* ReferendumVotersInteractor.swift */; }; F3D2AC37709EAF088A594B73 /* AccountManagementViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FCA2DD3A8898D64CBC9F97 /* AccountManagementViewController.swift */; }; + F3EA441846781BE5D7F3D7AB /* TransferNetworkSelectionProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 850BB5254D6443ACFDBB96CC /* TransferNetworkSelectionProtocols.swift */; }; F400A7C2260CE1670061D576 /* StakingRewardStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F400A7C1260CE1670061D576 /* StakingRewardStatus.swift */; }; F402BC83273ACDC30075F803 /* AstarBonusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F402BC82273ACDC30075F803 /* AstarBonusService.swift */; }; F402BC8B273AD20D0075F803 /* AstarBonusServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F402BC8A273AD20D0075F803 /* AstarBonusServiceError.swift */; }; @@ -4337,6 +4346,9 @@ 0CB64E5D2B00AA8F008F268F /* GetTokenOptionsResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsResult.swift; sourceTree = ""; }; 0CB64E5F2B00AD83008F268F /* GetTokenOptionsWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsWireframe.swift; sourceTree = ""; }; 0CB64E612B012E92008F268F /* TransferSetupPeer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferSetupPeer.swift; sourceTree = ""; }; + 0CB64E642B01E0CC008F268F /* TransferNetworkSelectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferNetworkSelectionCell.swift; sourceTree = ""; }; + 0CB64E662B01E174008F268F /* TransferNetworkSelectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferNetworkSelectionViewModel.swift; sourceTree = ""; }; + 0CB64E682B01F798008F268F /* TransferSetupOriginSelectionWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferSetupOriginSelectionWireframe.swift; sourceTree = ""; }; 0CBC29C52A421B5000F7B1F7 /* StakingMainWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMainWireframe.swift; sourceTree = ""; }; 0CBC29C72A42AE2F00F7B1F7 /* StakingDashboardBuilderResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingDashboardBuilderResult.swift; sourceTree = ""; }; 0CBF5DE62AB1A60500087EBF /* SharedOperationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedOperationStatus.swift; sourceTree = ""; }; @@ -4447,6 +4459,7 @@ 20878E303E9332322655F008 /* ParaStkSelectCollatorsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkSelectCollatorsProtocols.swift; sourceTree = ""; }; 20BB15F2A86E47DE30AE8107 /* GovernanceRemoveVotesConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceRemoveVotesConfirmProtocols.swift; sourceTree = ""; }; 20E4FF68D8A9AD54E4F089BC /* GovernanceDelegateInfoViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateInfoViewLayout.swift; sourceTree = ""; }; + 214A7ED74890344B6FBE765A /* TransferNetworkSelectionTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferNetworkSelectionTests.swift; sourceTree = ""; }; 215C03EE9C9EE024F952CB1C /* ReferendumsFiltersWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumsFiltersWireframe.swift; sourceTree = ""; }; 21621DCC39234CC4B0A1433B /* GovernanceYourDelegationsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceYourDelegationsPresenter.swift; sourceTree = ""; }; 219B9B1D97460F022D40D63E /* DAppTxDetailsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppTxDetailsWireframe.swift; sourceTree = ""; }; @@ -4622,6 +4635,7 @@ 47C56F2A76BA8745F1F708D4 /* DAppWalletAuthPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppWalletAuthPresenter.swift; sourceTree = ""; }; 4809C2DB0C15A9B2890C1AC6 /* LedgerPerformOperationInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerPerformOperationInteractor.swift; sourceTree = ""; }; 48182DE3A1302757558031FD /* TokenManageSinglePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokenManageSinglePresenter.swift; sourceTree = ""; }; + 486BF5FBE285713D05CF95A8 /* TransferNetworkSelectionInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferNetworkSelectionInteractor.swift; sourceTree = ""; }; 48C158C8D1855BCE53636934 /* AccountCreateProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountCreateProtocols.swift; sourceTree = ""; }; 48CECA2C5A0EFEBFDBB3C90C /* DAppOperationConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppOperationConfirmWireframe.swift; sourceTree = ""; }; 48E5BB1EB494B5DB92FC3053 /* Pods-novawalletAll-novawalletIntegrationTests.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawalletIntegrationTests.dev.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests.dev.xcconfig"; sourceTree = ""; }; @@ -5333,7 +5347,7 @@ 842AEB80292F34B600C61B0C /* RemoteChainExternalApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteChainExternalApi.swift; sourceTree = ""; }; 842B17FA28648FDC0014CC57 /* ChainAssetViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainAssetViewModelFactory.swift; sourceTree = ""; }; 842B17FC2864980B0014CC57 /* NetworkSelectionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSelectionTableViewCell.swift; sourceTree = ""; }; - 842B17FE28649CCD0014CC57 /* CrossChainDestinationSelectionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossChainDestinationSelectionState.swift; sourceTree = ""; }; + 842B17FE28649CCD0014CC57 /* CrossChainSelectionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossChainSelectionState.swift; sourceTree = ""; }; 842B18012864F9950014CC57 /* CrossChainTransferPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossChainTransferPresenter.swift; sourceTree = ""; }; 842B18032864F9A90014CC57 /* CrossChainTransferInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossChainTransferInteractor.swift; sourceTree = ""; }; 842B1805286506EE0014CC57 /* CrossChainTransferSetupProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossChainTransferSetupProtocols.swift; sourceTree = ""; }; @@ -7238,6 +7252,7 @@ 84FFE45A2862076F002432BB /* XcmUnweightedTransferRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmUnweightedTransferRequest.swift; sourceTree = ""; }; 84FFE45C28620833002432BB /* XcmTransferResolutionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmTransferResolutionService.swift; sourceTree = ""; }; 84FFE504261290830054EA63 /* NetworkInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkInfoView.swift; sourceTree = ""; }; + 850BB5254D6443ACFDBB96CC /* TransferNetworkSelectionProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferNetworkSelectionProtocols.swift; sourceTree = ""; }; 855FB8DD761E110A42435A02 /* AccountManagementWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountManagementWireframe.swift; sourceTree = ""; }; 856BF961EACEB9703B2B37C7 /* GovernanceUnlockSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnlockSetupWireframe.swift; sourceTree = ""; }; 859E0EF774DF0D498FEF8FCB /* DAppOperationConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppOperationConfirmViewLayout.swift; sourceTree = ""; }; @@ -7599,8 +7614,10 @@ 9B754B68D6F1D1ED5C8577A5 /* AssetListViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetListViewFactory.swift; sourceTree = ""; }; 9BAE0A2542AE83345BCCA549 /* TransactionHistoryViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransactionHistoryViewController.swift; sourceTree = ""; }; 9BCCD837A377C237C18B117E /* OperationDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OperationDetailsViewController.swift; sourceTree = ""; }; + 9CA463769D0F411429780D7D /* TransferNetworkSelectionViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferNetworkSelectionViewFactory.swift; sourceTree = ""; }; 9CDC7A44F6B01FE389F34C3A /* ParaStkStakeConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkStakeConfirmInteractor.swift; sourceTree = ""; }; 9D16A60434C1D9929E65998B /* ParaStkCollatorInfoViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkCollatorInfoViewFactory.swift; sourceTree = ""; }; + 9D6106D11494F979A46F7B4C /* TransferNetworkSelectionViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferNetworkSelectionViewController.swift; sourceTree = ""; }; 9D9131BF410C62A93646CA0A /* NPoolsUnstakeConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeConfirmPresenter.swift; sourceTree = ""; }; 9D93D6B6DB7BACFEA6F2738C /* ParaStkCollatorInfoInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkCollatorInfoInteractor.swift; sourceTree = ""; }; 9DBACA1AB17E90565F133C19 /* WalletsListInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsListInteractor.swift; sourceTree = ""; }; @@ -7865,6 +7882,7 @@ C9990DF2F0214CD51E5388CE /* ReferendumVotersPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVotersPresenter.swift; sourceTree = ""; }; C9A0E36C6D08351DFF7263E5 /* CommonDelegationTracksViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommonDelegationTracksViewLayout.swift; sourceTree = ""; }; CAB80E4CA0D5FA56612318A2 /* ChangeWatchOnlyProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChangeWatchOnlyProtocols.swift; sourceTree = ""; }; + CAE1DE626A1960428C9AEC52 /* TransferNetworkSelectionPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferNetworkSelectionPresenter.swift; sourceTree = ""; }; CAEF44ADECD66B49E3430365 /* MarkdownDescriptionPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MarkdownDescriptionPresenter.swift; sourceTree = ""; }; CB441F15E16B07196DD9CE9D /* ParaStkUnstakeProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkUnstakeProtocols.swift; sourceTree = ""; }; CB9150FEC66FC503CF1BD1D0 /* WalletHistoryFilterPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilterPresenter.swift; sourceTree = ""; }; @@ -8846,6 +8864,15 @@ path = Model; sourceTree = ""; }; + 0CB64E632B01E0AB008F268F /* View */ = { + isa = PBXGroup; + children = ( + 0CB64E642B01E0CC008F268F /* TransferNetworkSelectionCell.swift */, + 0CB64E662B01E174008F268F /* TransferNetworkSelectionViewModel.swift */, + ); + path = View; + sourceTree = ""; + }; 0CCA24592AC6914100AEF23D /* V3 */ = { isa = PBXGroup; children = ( @@ -13067,7 +13094,7 @@ isa = PBXGroup; children = ( 848F8B212863BD1000204BC4 /* TransferSetupInputState.swift */, - 842B17FE28649CCD0014CC57 /* CrossChainDestinationSelectionState.swift */, + 842B17FE28649CCD0014CC57 /* CrossChainSelectionState.swift */, 84B28FC328C54441007A1006 /* OnChainTransferAmount.swift */, 88F33F1229CC1ECD006125D5 /* Web3NameAddressesSelectionState.swift */, 8863C7AF29D49CB70068AD54 /* Web3NameViewModelFactory.swift */, @@ -14801,6 +14828,7 @@ 84B7C705289BFA79001A3566 /* AccountManagement */, 84B7C708289BFA79001A3566 /* WalletList */, 84B7C70A289BFA79001A3566 /* ControllerAccount */, + CE595DB3F344D81A05E638F6 /* TransferNetworkSelection */, ); path = Modules; sourceTree = ""; @@ -16236,6 +16264,7 @@ 8466780D27EB28FF007935D3 /* BaseTransfer */, DA7D18D3AF772CC2385C228C /* TransferSetup */, 34FF81AA0EBFAB3390FD989D /* TransferConfirm */, + BCA8C0E50D704DA8031D1648 /* TransferNetworkSelection */, ); path = Transfer; sourceTree = ""; @@ -18251,6 +18280,19 @@ path = AssetDetails; sourceTree = ""; }; + BCA8C0E50D704DA8031D1648 /* TransferNetworkSelection */ = { + isa = PBXGroup; + children = ( + 0CB64E632B01E0AB008F268F /* View */, + 850BB5254D6443ACFDBB96CC /* TransferNetworkSelectionProtocols.swift */, + CAE1DE626A1960428C9AEC52 /* TransferNetworkSelectionPresenter.swift */, + 486BF5FBE285713D05CF95A8 /* TransferNetworkSelectionInteractor.swift */, + 9D6106D11494F979A46F7B4C /* TransferNetworkSelectionViewController.swift */, + 9CA463769D0F411429780D7D /* TransferNetworkSelectionViewFactory.swift */, + ); + path = TransferNetworkSelection; + sourceTree = ""; + }; BF6F50DD15230CADAC713359 /* AccountImport */ = { isa = PBXGroup; children = ( @@ -18363,6 +18405,14 @@ path = StartStakingInfo; sourceTree = ""; }; + CE595DB3F344D81A05E638F6 /* TransferNetworkSelection */ = { + isa = PBXGroup; + children = ( + 214A7ED74890344B6FBE765A /* TransferNetworkSelectionTests.swift */, + ); + path = TransferNetworkSelection; + sourceTree = ""; + }; CEA238CBBD1DB61D399A69C0 /* NftList */ = { isa = PBXGroup; children = ( @@ -18510,6 +18560,7 @@ 848F8B1A28635A6D00204BC4 /* TransferSetupInteractor.swift */, 848F8B1E2863BB4000204BC4 /* TransferSetupPresenterFactory.swift */, 848F8B282864503A00204BC4 /* TransferSetupWireframe.swift */, + 0CB64E682B01F798008F268F /* TransferSetupOriginSelectionWireframe.swift */, ); path = TransferSetup; sourceTree = ""; @@ -20047,7 +20098,7 @@ 888A3B6528F73DC300E15BD2 /* ReferendumVotingStatusView.swift in Sources */, 0C13DFE12AFBBAF600E5F355 /* SwapBaseViewModelFactory.swift in Sources */, 843E9B3627C8B915009C143A /* NftFileDownloadService.swift in Sources */, - 842B17FF28649CCD0014CC57 /* CrossChainDestinationSelectionState.swift in Sources */, + 842B17FF28649CCD0014CC57 /* CrossChainSelectionState.swift in Sources */, 8437F7C12924FF6400DB6366 /* EvmSubscriptionMessage.swift in Sources */, 84A3034926A834F900E64382 /* ValidatorInfoViewLayout.swift in Sources */, 84D1ABE027E1CB870073C631 /* TitleHorizontalMultiValueView.swift in Sources */, @@ -21000,6 +21051,7 @@ 84E25BF027E8EFB500290BF1 /* SubqueryAccumulateReward.swift in Sources */, 88AC5ADA2948A8CC0056DD40 /* TransactionSectionModel.swift in Sources */, 8499FEE227C0AF4700712589 /* ChainModel+Nft.swift in Sources */, + 0CB64E692B01F798008F268F /* TransferSetupOriginSelectionWireframe.swift in Sources */, 8428765424ADDE0200D91AD8 /* SettingsViewModelFactory.swift in Sources */, 843612C1278FE62900DC739E /* DAppOperationConfirmInteractorError.swift in Sources */, 84E1CD02260DCC62001E81B5 /* SwitchAccount+OnboardingMainWireframe.swift in Sources */, @@ -21125,6 +21177,7 @@ 84B6349D28F4A06D00503306 /* Preimage.swift in Sources */, 8428768524AE046300D91AD8 /* LanguageSelectionProtocols.swift in Sources */, AEA0C8A4267B6B1900F9666F /* SelectedValidatorListProtocols.swift in Sources */, + 0CB64E652B01E0CC008F268F /* TransferNetworkSelectionCell.swift in Sources */, 0C3205EC2A8A122D002EB914 /* FeeOutputModel.swift in Sources */, 84D911AA292C923D0032EF33 /* Data+Fill.swift in Sources */, F4B39C4E27326E8400BB6E10 /* AcalaContributionSetupViewController.swift in Sources */, @@ -21706,6 +21759,7 @@ 848919DB26FB663D004DBAD5 /* CrowdloansChainViewModel.swift in Sources */, 842A736D27DB7B5E006EE1EA /* OperationTransferViewModel.swift in Sources */, 842B18022864F9950014CC57 /* CrossChainTransferPresenter.swift in Sources */, + 0CB64E672B01E174008F268F /* TransferNetworkSelectionViewModel.swift in Sources */, 84C1DBBA29C0A11200F295A5 /* XcmTransferService+Fee.swift in Sources */, 77C9BCD02ACD8A3600022EA2 /* SwapAssetsOperationViewLayout.swift in Sources */, 845B081529190056005785D3 /* Gov1UnlockReferendum.swift in Sources */, @@ -23407,6 +23461,11 @@ 143F6C9044429A337265DF39 /* SwapSlippageViewController.swift in Sources */, F9CEF01779F811AEEED06C43 /* SwapSlippageViewLayout.swift in Sources */, 80603DA36CD481AE310CDFE1 /* SwapSlippageViewFactory.swift in Sources */, + F3EA441846781BE5D7F3D7AB /* TransferNetworkSelectionProtocols.swift in Sources */, + 66436210FD782DAF13814711 /* TransferNetworkSelectionPresenter.swift in Sources */, + 34E4B25CDFE0D7B5F4F18185 /* TransferNetworkSelectionInteractor.swift in Sources */, + 7A47B0731BC8B411CC01C3D1 /* TransferNetworkSelectionViewController.swift in Sources */, + 0BB2E3FF30B1700D321C526A /* TransferNetworkSelectionViewFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -23568,6 +23627,7 @@ 84B7C748289BFA79001A3566 /* WalletListTests.swift in Sources */, 84B7C720289BFA79001A3566 /* ReferralCrowdloanTests.swift in Sources */, F4897BB126AED13D0075F291 /* EraCountdownOperationFactoryStub.swift in Sources */, + 4AEF5B442FA0AF1F515AE6B5 /* TransferNetworkSelectionTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index 96c1957e03..ec2af4ed8c 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -158,6 +158,7 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { from: origins, to: destination, xcmTransfers: xcmTransfers, + assetListObservable: assetListObservable, transferCompletion: nil ) else { return diff --git a/novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionInteractor.swift b/novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionInteractor.swift new file mode 100644 index 0000000000..4e19021525 --- /dev/null +++ b/novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionInteractor.swift @@ -0,0 +1,36 @@ +import UIKit + +final class TransferNetworkSelectionInteractor { + weak var presenter: TransferNetworkSelectionInteractorOutputProtocol? + + let assetListObservable: AssetListModelObservable + + init(assetListObservable: AssetListModelObservable) { + self.assetListObservable = assetListObservable + } + + private func provideModel() { + let balances = assetListObservable.state.value.balances.compactMapValues { balanceResult in + switch balanceResult { + case let .success(balance): + return balance + case let .failure(error): + return nil + } + } + + let prices = (try? assetListObservable.state.value.priceResult?.get()) ?? [:] + + presenter?.didReceive(balances: balances, prices: prices) + } +} + +extension TransferNetworkSelectionInteractor: TransferNetworkSelectionInteractorInputProtocol { + func setup() { + assetListObservable.addObserver(with: self, queue: .main) { [weak self] _, _ in + self?.provideModel() + } + + provideModel() + } +} diff --git a/novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionPresenter.swift b/novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionPresenter.swift new file mode 100644 index 0000000000..86b388215e --- /dev/null +++ b/novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionPresenter.swift @@ -0,0 +1,70 @@ +import Foundation +import SoraFoundation + +final class TransferNetworkSelectionPresenter { + weak var view: TransferNetworkSelectionViewProtocol? + let interactor: TransferNetworkSelectionInteractorInputProtocol + let chainAssets: [ChainAsset] + let balanceViewModeFactoryFacade: BalanceViewModelFactoryFacadeProtocol + let networkViewModelFactory: NetworkViewModelFactoryProtocol + + private var balances: [ChainAssetId: AssetBalance] = [:] + private var prices: [ChainAssetId: PriceData] = [:] + + init( + chainAssets: [ChainAsset], + interactor: TransferNetworkSelectionInteractorInputProtocol, + balanceViewModeFactoryFacade: BalanceViewModelFactoryFacadeProtocol, + networkViewModelFactory: NetworkViewModelFactoryProtocol + ) { + self.chainAssets = chainAssets + self.interactor = interactor + self.balanceViewModeFactoryFacade = balanceViewModeFactoryFacade + self.networkViewModelFactory = networkViewModelFactory + } + + private func provideViewModel() { + let viewModels = chainAssets.map { chainAsset in + + let networkViewModel = networkViewModelFactory.createViewModel(from: chainAsset.chain) + let balanceViewModel: LocalizableResource? + + if let balance = balances[chainAsset.chainAssetId] { + let decimalBalance = balance.transferable.decimal(precision: chainAsset.asset.precision) + balanceViewModel = balanceViewModeFactoryFacade.balanceFromPrice( + targetAssetInfo: chainAsset.assetDisplayInfo, + amount: decimalBalance, + priceData: prices[chainAsset.chainAssetId] + ) + } else { + balanceViewModel = nil + } + + return LocalizableResource { locale in + TransferNetworkSelectionViewModel( + network: networkViewModel, + balance: balanceViewModel?.value(for: locale) + ) + } + } + + view?.didReceive(viewModels: viewModels) + } +} + +extension TransferNetworkSelectionPresenter: TransferNetworkSelectionPresenterProtocol { + func setup() { + provideViewModel() + + interactor.setup() + } +} + +extension TransferNetworkSelectionPresenter: TransferNetworkSelectionInteractorOutputProtocol { + func didReceive(balances: [ChainAssetId: AssetBalance], prices: [ChainAssetId: PriceData]) { + self.balances = balances + self.prices = prices + + provideViewModel() + } +} diff --git a/novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionProtocols.swift b/novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionProtocols.swift new file mode 100644 index 0000000000..81f6a9cdbe --- /dev/null +++ b/novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionProtocols.swift @@ -0,0 +1,17 @@ +import SoraFoundation + +protocol TransferNetworkSelectionViewProtocol: ControllerBackedProtocol { + func didReceive(viewModels: [LocalizableResource]) +} + +protocol TransferNetworkSelectionPresenterProtocol: AnyObject { + func setup() +} + +protocol TransferNetworkSelectionInteractorInputProtocol: AnyObject { + func setup() +} + +protocol TransferNetworkSelectionInteractorOutputProtocol: AnyObject { + func didReceive(balances: [ChainAssetId: AssetBalance], prices: [ChainAssetId: PriceData]) +} diff --git a/novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionViewController.swift b/novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionViewController.swift new file mode 100644 index 0000000000..16e03bde26 --- /dev/null +++ b/novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionViewController.swift @@ -0,0 +1,34 @@ +import UIKit +import SoraFoundation + +final class TransferNetworkSelectionViewController: ModalPickerViewController< + TransferNetworkSelectionCell, TransferNetworkSelectionViewModel +> { + let viewModelPresenter: TransferNetworkSelectionPresenterProtocol + + init(viewModelPresenter: TransferNetworkSelectionPresenterProtocol) { + self.viewModelPresenter = viewModelPresenter + + let nib = R.nib.modalPickerViewController + super.init(nibName: nib.name, bundle: nib.bundle) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + viewModelPresenter.setup() + } +} + +extension TransferNetworkSelectionViewController: TransferNetworkSelectionViewProtocol { + func didReceive(viewModels: [LocalizableResource]) { + self.viewModels = viewModels + + reload() + } +} diff --git a/novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionViewFactory.swift b/novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionViewFactory.swift new file mode 100644 index 0000000000..bd2e945ed7 --- /dev/null +++ b/novawallet/Modules/Transfer/TransferNetworkSelection/TransferNetworkSelectionViewFactory.swift @@ -0,0 +1,75 @@ +import Foundation +import SoraUI +import SoraFoundation + +struct TransferNetworkSelectionViewFactory { + static func createView( + for selectionState: CrossChainOriginSelectionState, + assetListObservable: AssetListModelObservable, + delegate: ModalPickerViewControllerDelegate + ) -> TransferNetworkSelectionViewProtocol? { + guard let currencyManager = CurrencyManager.shared else { + return nil + } + + let interactor = TransferNetworkSelectionInteractor(assetListObservable: assetListObservable) + + let balanceViewModelFactory = BalanceViewModelFactoryFacade( + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager) + ) + + let presenter = TransferNetworkSelectionPresenter( + chainAssets: selectionState.availablePeerChainAssets, + interactor: interactor, + balanceViewModeFactoryFacade: balanceViewModelFactory, + networkViewModelFactory: NetworkViewModelFactory() + ) + + let viewController = TransferNetworkSelectionViewController(viewModelPresenter: presenter) + + presenter.view = viewController + interactor.presenter = presenter + + viewController.localizedTitle = LocalizableResource { locale in + R.string.localizable.commonFromNetwork(preferredLanguages: locale.rLanguages) + } + + viewController.modalPresentationStyle = .custom + viewController.separatorStyle = .none + viewController.headerBorderType = [] + viewController.actionType = .none + viewController.delegate = delegate + viewController.context = selectionState + viewController.isScrollEnabled = true + viewController.cellHeight = 52 + + let sectionTitle = LocalizableResource { locale in + R.string.localizable.commonCrossChain(preferredLanguages: locale.rLanguages) + } + + viewController.addSection(viewModels: [], title: sectionTitle) + + if let index = selectionState.availablePeerChainAssets.firstIndex( + where: { selectionState.selectedChainAssetId == $0.chainAssetId } + ) { + viewController.selectedIndex = index + } else { + viewController.selectedIndex = NSNotFound + } + + let factory = ModalSheetPresentationFactory(configuration: .nova) + viewController.modalTransitioningFactory = factory + + let itemsCount = selectionState.availablePeerChainAssets.count + let sectionsCount = 1 + let height = viewController.headerHeight + CGFloat(itemsCount) * viewController.cellHeight + + CGFloat(sectionsCount) * viewController.sectionHeaderHeight + + let maxHeight = ModalSheetPresentationConfiguration.maximumContentHeight + viewController.preferredContentSize = CGSize(width: 0.0, height: min(height, maxHeight)) + + viewController.localizationManager = LocalizationManager.shared + + return viewController + } +} diff --git a/novawallet/Modules/Transfer/TransferNetworkSelection/View/TransferNetworkSelectionCell.swift b/novawallet/Modules/Transfer/TransferNetworkSelection/View/TransferNetworkSelectionCell.swift new file mode 100644 index 0000000000..7b3a25249b --- /dev/null +++ b/novawallet/Modules/Transfer/TransferNetworkSelection/View/TransferNetworkSelectionCell.swift @@ -0,0 +1,58 @@ +import Foundation +import UIKit + +typealias TransferNetworkSelectionContentView = GenericTitleValueView< + AssetListChainView, + GenericPairValueView +> + +final class TransferNetworkSelectionCell: PlainBaseTableViewCell, + ModalPickerCellProtocol { + typealias Model = TransferNetworkSelectionViewModel + + var networkView: AssetListChainView { + contentDisplayView.titleView + } + + var selectorView: RadioSelectorView { + contentDisplayView.valueView.sView + } + + var balanceView: MultiValueView { + contentDisplayView.valueView.fView + } + + var checkmarked: Bool { + get { + selectorView.selected + } + + set { + selectorView.selected = newValue + } + } + + func bind(model: Model) { + networkView.bind(viewModel: model.network) + balanceView.bind(topValue: model.balance?.amount ?? "", bottomValue: model.balance?.price) + } + + override func setupStyle() { + super.setupStyle() + + backgroundColor = .clear + + contentDisplayView.valueView.setHorizontalAndSpacing(12) + contentDisplayView.valueView.stackView.alignment = .center + } + + override func setupLayout() { + super.setupLayout() + + let selectorSize = 2 * selectorView.outerRadius + + selectorView.snp.makeConstraints { make in + make.size.equalTo(selectorSize) + } + } +} diff --git a/novawallet/Modules/Transfer/TransferNetworkSelection/View/TransferNetworkSelectionViewModel.swift b/novawallet/Modules/Transfer/TransferNetworkSelection/View/TransferNetworkSelectionViewModel.swift new file mode 100644 index 0000000000..b847abc9fa --- /dev/null +++ b/novawallet/Modules/Transfer/TransferNetworkSelection/View/TransferNetworkSelectionViewModel.swift @@ -0,0 +1,6 @@ +import Foundation + +struct TransferNetworkSelectionViewModel { + let network: NetworkViewModel + let balance: BalanceViewModelProtocol? +} diff --git a/novawallet/Modules/Transfer/TransferSetup/Model/CrossChainDestinationSelectionState.swift b/novawallet/Modules/Transfer/TransferSetup/Model/CrossChainSelectionState.swift similarity index 53% rename from novawallet/Modules/Transfer/TransferSetup/Model/CrossChainDestinationSelectionState.swift rename to novawallet/Modules/Transfer/TransferSetup/Model/CrossChainSelectionState.swift index 5a0dcf53fb..a065c7f611 100644 --- a/novawallet/Modules/Transfer/TransferSetup/Model/CrossChainDestinationSelectionState.swift +++ b/novawallet/Modules/Transfer/TransferSetup/Model/CrossChainSelectionState.swift @@ -11,3 +11,13 @@ class CrossChainDestinationSelectionState { self.selectedChainId = selectedChainId } } + +class CrossChainOriginSelectionState { + let availablePeerChainAssets: [ChainAsset] + let selectedChainAssetId: ChainAssetId + + init(availablePeerChainAssets: [ChainAsset], selectedChainAssetId: ChainAssetId) { + self.availablePeerChainAssets = availablePeerChainAssets + self.selectedChainAssetId = selectedChainAssetId + } +} diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupOriginSelectionWireframe.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupOriginSelectionWireframe.swift new file mode 100644 index 0000000000..c02372c8ff --- /dev/null +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupOriginSelectionWireframe.swift @@ -0,0 +1,26 @@ +import Foundation + +final class TransferSetupOriginSelectionWireframe: TransferSetupWireframe { + let assetListObservable: AssetListModelObservable + + init(assetListObservable: AssetListModelObservable) { + self.assetListObservable = assetListObservable + } + + override func showOriginChainSelection( + from view: TransferSetupViewProtocol?, + chainAsset _: ChainAsset, + selectionState: CrossChainOriginSelectionState, + delegate: ModalPickerViewControllerDelegate + ) { + guard let networkSelectionView = TransferNetworkSelectionViewFactory.createView( + for: selectionState, + assetListObservable: assetListObservable, + delegate: delegate + ) else { + return + } + + view?.controller.present(networkSelectionView.controller, animated: true, completion: nil) + } +} diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupPresenter.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupPresenter.swift index c3054ca08d..3343aff712 100644 --- a/novawallet/Modules/Transfer/TransferSetup/TransferSetupPresenter.swift +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupPresenter.swift @@ -231,6 +231,61 @@ final class TransferSetupPresenter { break } } + + private func selectDestinationChain() { + let chain = chainAsset.chain + let selectedChainId = peerChainAsset?.chain.chainId ?? chain.chainId + + let availablePeerChains = availablePeers?.map(\.chain) ?? [] + + let selectionState = CrossChainDestinationSelectionState( + chain: chain, + availablePeerChains: availablePeerChains, + selectedChainId: selectedChainId + ) + + wireframe.showDestinationChainSelection( + from: view, + selectionState: selectionState, + delegate: self + ) + } + + private func selectOriginChain() { + let selectedChainAssetId = peerChainAsset?.chainAssetId ?? chainAsset.chainAssetId + + let selectionState = CrossChainOriginSelectionState( + availablePeerChainAssets: availablePeers ?? [], + selectedChainAssetId: selectedChainAssetId + ) + + wireframe.showOriginChainSelection( + from: view, + chainAsset: chainAsset, + selectionState: selectionState, + delegate: self + ) + } + + private func handleNewChainAssetSelection(_ newPeerChainAsset: ChainAsset?) { + if recipientAddress?.isExternal == true { + recipientAddress = nil + childPresenter?.updateRecepient(partialAddress: "") + } + + peerChainAsset = newPeerChainAsset + + provideChainsViewModel() + updateYourWalletsButton() + + if let peerChainAsset = peerChainAsset { + setupCrossChainChildPresenter() + interactor.peerChainAssetDidChanged(peerChainAsset) + } else { + setupOnChainChildPresenter() + interactor.peerChainAssetDidChanged(chainAsset) + } + } } extension TransferSetupPresenter: TransferSetupPresenterProtocol { @@ -288,23 +343,12 @@ extension TransferSetupPresenter: TransferSetupPresenterProtocol { } func selectChain() { - let chain = chainAsset.chain - let selectedChainId = peerChainAsset?.chain.chainId ?? chain.chainId - - let availablePeerChains = availablePeers?.map(\.chain) ?? [] - - let selectionState = CrossChainDestinationSelectionState( - chain: chain, - availablePeerChains: availablePeerChains, - selectedChainId: selectedChainId - ) - - wireframe.showDestinationChainSelection( - from: view, - selectionState: selectionState, - delegate: self, - context: selectionState - ) + switch whoChainAssetPeer { + case .destination: + selectDestinationChain() + case .origin: + selectOriginChain() + } } func didTapOnYourWallets() { @@ -405,33 +449,19 @@ extension TransferSetupPresenter: ModalPickerViewControllerDelegate { func modalPickerDidSelectModel(at index: Int, section: Int, context: AnyObject?) { view?.didCompleteChainSelection() - guard let selectionState = context as? CrossChainDestinationSelectionState else { - return - } - - if recipientAddress?.isExternal == true { - recipientAddress = nil - childPresenter?.updateRecepient(partialAddress: "") - } - - if section == 0 { - peerChainAsset = nil - } else { - let selectedChain = selectionState.availablePeerChains[index] - let selectedChainId = selectedChain.chainId - - peerChainAsset = availablePeers?.first { $0.chain.chainId == selectedChainId } - } + if let selectionState = context as? CrossChainDestinationSelectionState { + if section == 0 { + handleNewChainAssetSelection(nil) + } else { + let selectedChain = selectionState.availablePeerChains[index] + let selectedChainId = selectedChain.chainId - provideChainsViewModel() - updateYourWalletsButton() + let newPeerChainAsset = availablePeers?.first { $0.chain.chainId == selectedChainId } - if let peerChainAsset = peerChainAsset { - setupCrossChainChildPresenter() - interactor.peerChainAssetDidChanged(peerChainAsset) - } else { - setupOnChainChildPresenter() - interactor.peerChainAssetDidChanged(chainAsset) + handleNewChainAssetSelection(newPeerChainAsset) + } + } else if let selectionState = context as? CrossChainOriginSelectionState { + handleNewChainAssetSelection(selectionState.availablePeerChainAssets[index]) } } @@ -445,7 +475,7 @@ extension TransferSetupPresenter: ModalPickerViewControllerDelegate { } func modalPickerDidCancel(context: AnyObject?) { - if context is CrossChainDestinationSelectionState { + if context is CrossChainDestinationSelectionState || context is CrossChainOriginSelectionState { view?.didCompleteChainSelection() } else if context is Web3NameAddressesSelectionState { view?.didReceiveRecipientInputState(focused: true, empty: nil) diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupProtocols.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupProtocols.swift index 6cf3d7e216..b1864fb0ff 100644 --- a/novawallet/Modules/Transfer/TransferSetup/TransferSetupProtocols.swift +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupProtocols.swift @@ -65,8 +65,14 @@ protocol TransferSetupWireframeProtocol: AlertPresentable, ErrorPresentable, Add func showDestinationChainSelection( from view: TransferSetupViewProtocol?, selectionState: CrossChainDestinationSelectionState, - delegate: ModalPickerViewControllerDelegate, - context: AnyObject? + delegate: ModalPickerViewControllerDelegate + ) + + func showOriginChainSelection( + from view: TransferSetupViewProtocol?, + chainAsset: ChainAsset, + selectionState: CrossChainOriginSelectionState, + delegate: ModalPickerViewControllerDelegate ) func showRecepientScan(from view: TransferSetupViewProtocol?, delegate: AddressScanDelegate) diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift index fb1e41cce4..bb4fb510bd 100644 --- a/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift @@ -24,6 +24,7 @@ enum TransferSetupViewFactory { recepient: recepient, xcmTransfers: nil ), + wireframe: TransferSetupWireframe(), transferCompletion: transferCompletion ) } @@ -32,6 +33,7 @@ enum TransferSetupViewFactory { from origins: [ChainAsset], to destination: ChainAsset, xcmTransfers: XcmTransfers?, + assetListObservable: AssetListModelObservable, transferCompletion: TransferCompletionClosure? = nil ) -> TransferSetupViewProtocol? { guard !origins.isEmpty else { @@ -46,12 +48,14 @@ enum TransferSetupViewFactory { recepient: nil, xcmTransfers: xcmTransfers ), + wireframe: TransferSetupOriginSelectionWireframe(assetListObservable: assetListObservable), transferCompletion: transferCompletion ) } static func createView( from params: TransferSetupViewParams, + wireframe: TransferSetupWireframeProtocol, transferCompletion: TransferCompletionClosure? ) -> TransferSetupViewProtocol? { guard let wallet = SelectedWalletSettings.shared.value else { @@ -68,8 +72,6 @@ enum TransferSetupViewFactory { let localizationManager = LocalizationManager.shared - let wireframe = TransferSetupWireframe() - let networkViewModelFactory = NetworkViewModelFactory() let chainAssetViewModelFactory = ChainAssetViewModelFactory(networkViewModelFactory: networkViewModelFactory) let viewModelFactory = Web3NameViewModelFactory( diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupWireframe.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupWireframe.swift index dd96082f46..14271f7eb3 100644 --- a/novawallet/Modules/Transfer/TransferSetup/TransferSetupWireframe.swift +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupWireframe.swift @@ -2,22 +2,38 @@ import Foundation import SoraFoundation import SoraUI -final class TransferSetupWireframe: TransferSetupWireframeProtocol { +class TransferSetupWireframe: TransferSetupWireframeProtocol { func showDestinationChainSelection( from view: TransferSetupViewProtocol?, selectionState: CrossChainDestinationSelectionState, - delegate: ModalPickerViewControllerDelegate, - context: AnyObject? + delegate: ModalPickerViewControllerDelegate ) { - guard let viewController = ModalNetworksFactory.createNetworkSelectionList( + showChainSelection( + from: view, selectionState: selectionState, delegate: delegate, - context: context - ) else { - return - } + context: selectionState + ) + } - view?.controller.present(viewController, animated: true, completion: nil) + func showOriginChainSelection( + from view: TransferSetupViewProtocol?, + chainAsset: ChainAsset, + selectionState: CrossChainOriginSelectionState, + delegate: ModalPickerViewControllerDelegate + ) { + let mappedState = CrossChainDestinationSelectionState( + chain: chainAsset.chain, + availablePeerChains: selectionState.availablePeerChainAssets.map(\.chain), + selectedChainId: selectionState.selectedChainAssetId.chainId + ) + + showChainSelection( + from: view, + selectionState: mappedState, + delegate: delegate, + context: selectionState + ) } func showRecepientScan(from view: TransferSetupViewProtocol?, delegate: AddressScanDelegate) { @@ -68,4 +84,21 @@ final class TransferSetupWireframe: TransferSetupWireframeProtocol { func checkDismissing(view: TransferSetupViewProtocol?) -> Bool { view?.controller.navigationController?.isBeingDismissed ?? true } + + private func showChainSelection( + from view: TransferSetupViewProtocol?, + selectionState: CrossChainDestinationSelectionState, + delegate: ModalPickerViewControllerDelegate, + context: AnyObject? + ) { + guard let viewController = ModalNetworksFactory.createNetworkSelectionList( + selectionState: selectionState, + delegate: delegate, + context: context + ) else { + return + } + + view?.controller.present(viewController, animated: true, completion: nil) + } } diff --git a/novawalletTests/Modules/TransferNetworkSelection/TransferNetworkSelectionTests.swift b/novawalletTests/Modules/TransferNetworkSelection/TransferNetworkSelectionTests.swift new file mode 100644 index 0000000000..14cd64af76 --- /dev/null +++ b/novawalletTests/Modules/TransferNetworkSelection/TransferNetworkSelectionTests.swift @@ -0,0 +1,16 @@ +import XCTest + +class TransferNetworkSelectionTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + XCTFail("Did you forget to add tests?") + } +} From 51132c2013ce7693317cf8340813a5bf52ff2433 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 13 Nov 2023 08:00:08 +0100 Subject: [PATCH 160/204] fix tests --- novawallet.xcodeproj/project.pbxproj | 12 ------------ .../TransferNetworkSelectionTests.swift | 16 ---------------- 2 files changed, 28 deletions(-) delete mode 100644 novawalletTests/Modules/TransferNetworkSelection/TransferNetworkSelectionTests.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index ebb92da543..53d24b1213 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -555,7 +555,6 @@ 493A9637BE5A1BF4B0744A4C /* ChangeWatchOnlyInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E4CD58A9006CEB045E8977 /* ChangeWatchOnlyInteractor.swift */; }; 4A24646D497B26E51926BA52 /* StakingRebagConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74A26B3915B6E0C8C784423 /* StakingRebagConfirmViewFactory.swift */; }; 4A520B7081BE2D7604B69354 /* AccountImportWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F45A5C6145F863760F4409 /* AccountImportWireframe.swift */; }; - 4AEF5B442FA0AF1F515AE6B5 /* TransferNetworkSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214A7ED74890344B6FBE765A /* TransferNetworkSelectionTests.swift */; }; 4B1FA597B618713C75917816 /* GovernanceYourDelegationsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2FCBFC59ED9D7E6F046D2A1 /* GovernanceYourDelegationsProtocols.swift */; }; 4B4189889DEFAF917332D41C /* ChangeWatchOnlyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FECFFBAB264397F9B2646CE /* ChangeWatchOnlyViewController.swift */; }; 4B83231E151422897F71408F /* GovernanceSelectTracksInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D9A2CF1016F4D5A7F075B69 /* GovernanceSelectTracksInteractor.swift */; }; @@ -4459,7 +4458,6 @@ 20878E303E9332322655F008 /* ParaStkSelectCollatorsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkSelectCollatorsProtocols.swift; sourceTree = ""; }; 20BB15F2A86E47DE30AE8107 /* GovernanceRemoveVotesConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceRemoveVotesConfirmProtocols.swift; sourceTree = ""; }; 20E4FF68D8A9AD54E4F089BC /* GovernanceDelegateInfoViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateInfoViewLayout.swift; sourceTree = ""; }; - 214A7ED74890344B6FBE765A /* TransferNetworkSelectionTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferNetworkSelectionTests.swift; sourceTree = ""; }; 215C03EE9C9EE024F952CB1C /* ReferendumsFiltersWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumsFiltersWireframe.swift; sourceTree = ""; }; 21621DCC39234CC4B0A1433B /* GovernanceYourDelegationsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceYourDelegationsPresenter.swift; sourceTree = ""; }; 219B9B1D97460F022D40D63E /* DAppTxDetailsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppTxDetailsWireframe.swift; sourceTree = ""; }; @@ -14828,7 +14826,6 @@ 84B7C705289BFA79001A3566 /* AccountManagement */, 84B7C708289BFA79001A3566 /* WalletList */, 84B7C70A289BFA79001A3566 /* ControllerAccount */, - CE595DB3F344D81A05E638F6 /* TransferNetworkSelection */, ); path = Modules; sourceTree = ""; @@ -18405,14 +18402,6 @@ path = StartStakingInfo; sourceTree = ""; }; - CE595DB3F344D81A05E638F6 /* TransferNetworkSelection */ = { - isa = PBXGroup; - children = ( - 214A7ED74890344B6FBE765A /* TransferNetworkSelectionTests.swift */, - ); - path = TransferNetworkSelection; - sourceTree = ""; - }; CEA238CBBD1DB61D399A69C0 /* NftList */ = { isa = PBXGroup; children = ( @@ -23627,7 +23616,6 @@ 84B7C748289BFA79001A3566 /* WalletListTests.swift in Sources */, 84B7C720289BFA79001A3566 /* ReferralCrowdloanTests.swift in Sources */, F4897BB126AED13D0075F291 /* EraCountdownOperationFactoryStub.swift in Sources */, - 4AEF5B442FA0AF1F515AE6B5 /* TransferNetworkSelectionTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/novawalletTests/Modules/TransferNetworkSelection/TransferNetworkSelectionTests.swift b/novawalletTests/Modules/TransferNetworkSelection/TransferNetworkSelectionTests.swift deleted file mode 100644 index 14cd64af76..0000000000 --- a/novawalletTests/Modules/TransferNetworkSelection/TransferNetworkSelectionTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -import XCTest - -class TransferNetworkSelectionTests: XCTestCase { - - override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() { - XCTFail("Did you forget to add tests?") - } -} From 237ec93db212485da8324947cc1a5ade11813b06 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 13 Nov 2023 11:13:41 +0100 Subject: [PATCH 161/204] insert recepient address by default --- .../Transfer/TransferSetup/TransferSetupViewFactory.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift index bb4fb510bd..6fabbf39b0 100644 --- a/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift @@ -36,16 +36,18 @@ enum TransferSetupViewFactory { assetListObservable: AssetListModelObservable, transferCompletion: TransferCompletionClosure? = nil ) -> TransferSetupViewProtocol? { - guard !origins.isEmpty else { + guard let originChainAsset = origins.first, let wallet = SelectedWalletSettings.shared.value else { return nil } + let recepient = try? wallet.fetch(for: originChainAsset.chain.accountRequest())?.toDisplayAddress() + return createView( from: .init( chainAsset: destination, whoChainAssetPeer: .origin, chainAssetPeers: origins, - recepient: nil, + recepient: recepient, xcmTransfers: xcmTransfers ), wireframe: TransferSetupOriginSelectionWireframe(assetListObservable: assetListObservable), From f0c6ccbf3165f112124de0b7f804469ddc1ffc81 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 13 Nov 2023 11:45:27 +0100 Subject: [PATCH 162/204] fix selection --- .../TransferSetup/TransferSetupPresenter.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupPresenter.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupPresenter.swift index 3343aff712..5c990724d1 100644 --- a/novawallet/Modules/Transfer/TransferSetup/TransferSetupPresenter.swift +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupPresenter.swift @@ -254,8 +254,10 @@ final class TransferSetupPresenter { private func selectOriginChain() { let selectedChainAssetId = peerChainAsset?.chainAssetId ?? chainAsset.chainAssetId + let peers = availablePeers ?? [] + let selectionState = CrossChainOriginSelectionState( - availablePeerChainAssets: availablePeers ?? [], + availablePeerChainAssets: peers, selectedChainAssetId: selectedChainAssetId ) @@ -460,18 +462,16 @@ extension TransferSetupPresenter: ModalPickerViewControllerDelegate { handleNewChainAssetSelection(newPeerChainAsset) } - } else if let selectionState = context as? CrossChainOriginSelectionState { - handleNewChainAssetSelection(selectionState.availablePeerChainAssets[index]) } } func modalPickerDidSelectModelAtIndex(_ index: Int, context: AnyObject?) { - guard let selectionState = context as? Web3NameAddressesSelectionState else { - return + if let selectionState = context as? Web3NameAddressesSelectionState { + let selectedAccount = selectionState.accounts[index] + provideWeb3NameRecipientViewModel(selectedAccount, name: selectionState.name) + } else if let selectionState = context as? CrossChainOriginSelectionState { + handleNewChainAssetSelection(selectionState.availablePeerChainAssets[index]) } - - let selectedAccount = selectionState.accounts[index] - provideWeb3NameRecipientViewModel(selectedAccount, name: selectionState.name) } func modalPickerDidCancel(context: AnyObject?) { From 7854ddada41be1cd12e43619ba83a779f41f7fab Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Mon, 13 Nov 2023 17:06:00 +0300 Subject: [PATCH 163/204] navigation --- .../AssetDetailsViewFactory.swift | 8 +++-- .../AssetDetails/AssetDetailsWireframe.swift | 10 ++++-- .../AssetDetailsContainerProtocols.swift | 3 +- .../AssetDetailsContainerViewFactory.swift | 9 +++-- .../AssetList/AssetListProtocols.swift | 1 + .../AssetList/AssetListWireframe.swift | 33 +++++++++++++++---- .../OperationDetailsViewFactory.swift | 8 +++-- .../OperationDetailsWireframe.swift | 10 ++++-- .../Swaps/Confirm/SwapConfirmPresenter.swift | 9 ++++- .../Swaps/Confirm/SwapConfirmProtocols.swift | 6 +++- .../Confirm/SwapConfirmViewFactory.swift | 5 +-- .../Swaps/Confirm/SwapConfirmWireframe.swift | 13 +++++++- .../Swaps/Setup/SwapSetupViewFactory.swift | 12 ++++--- .../Swaps/Setup/SwapSetupWireframe.swift | 11 +++++-- .../TransactionHistoryViewFactory.swift | 6 ++-- .../TransactionHistoryWireframe.swift | 8 +++-- 16 files changed, 119 insertions(+), 33 deletions(-) diff --git a/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift b/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift index 119db6906c..d3eee67a0b 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift @@ -5,7 +5,8 @@ struct AssetDetailsViewFactory { static func createView( assetListObservable: AssetListModelObservable, chain: ChainModel, - asset: AssetModel + asset: AssetModel, + swapCompletionClosure: SwapCompletionClosure? ) -> AssetDetailsViewProtocol? { guard let currencyManager = CurrencyManager.shared else { return nil @@ -33,7 +34,10 @@ struct AssetDetailsViewFactory { currencyManager: currencyManager ) - let wireframe = AssetDetailsWireframe(assetListObservable: assetListObservable) + let wireframe = AssetDetailsWireframe( + assetListObservable: assetListObservable, + swapCompletionClosure: swapCompletionClosure + ) let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) let viewModelFactory = AssetDetailsViewModelFactory( diff --git a/novawallet/Modules/AssetDetails/AssetDetailsWireframe.swift b/novawallet/Modules/AssetDetails/AssetDetailsWireframe.swift index 948e904cba..fb3113034d 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsWireframe.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsWireframe.swift @@ -5,9 +5,14 @@ import SoraFoundation final class AssetDetailsWireframe: AssetDetailsWireframeProtocol { let assetListObservable: AssetListModelObservable + let swapCompletionClosure: SwapCompletionClosure? - init(assetListObservable: AssetListModelObservable) { + init( + assetListObservable: AssetListModelObservable, + swapCompletionClosure: SwapCompletionClosure? + ) { self.assetListObservable = assetListObservable + self.swapCompletionClosure = swapCompletionClosure } func showPurchaseTokens( @@ -102,7 +107,8 @@ final class AssetDetailsWireframe: AssetDetailsWireframeProtocol { func showSwaps(from view: AssetDetailsViewProtocol?, chainAsset: ChainAsset) { guard let swapsView = SwapSetupViewFactory.createView( assetListObservable: assetListObservable, - payChainAsset: chainAsset + payChainAsset: chainAsset, + swapCompletionClosure: swapCompletionClosure ) else { return } diff --git a/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerProtocols.swift b/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerProtocols.swift index 146772530d..a82d330c71 100644 --- a/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerProtocols.swift +++ b/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerProtocols.swift @@ -2,7 +2,8 @@ protocol AssetDetailsContainerViewFactoryProtocol { static func createView( assetListObservable: AssetListModelObservable, chain: ChainModel, - asset: AssetModel + asset: AssetModel, + swapCompletionClosure: SwapCompletionClosure? ) -> AssetDetailsContainerViewProtocol? } diff --git a/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift b/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift index 6d20e2231a..14ea919963 100644 --- a/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift +++ b/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift @@ -4,17 +4,20 @@ final class AssetDetailsContainerViewFactory: AssetDetailsContainerViewFactoryPr static func createView( assetListObservable: AssetListModelObservable, chain: ChainModel, - asset: AssetModel + asset: AssetModel, + swapCompletionClosure: SwapCompletionClosure? ) -> AssetDetailsContainerViewProtocol? { guard let accountView = AssetDetailsViewFactory.createView( assetListObservable: assetListObservable, chain: chain, - asset: asset + asset: asset, + swapCompletionClosure: swapCompletionClosure ), let historyView = TransactionHistoryViewFactory.createView( chainAsset: .init(chain: chain, asset: asset), - assetListObservable: assetListObservable + assetListObservable: assetListObservable, + swapCompletionClosure: swapCompletionClosure ) else { return nil } diff --git a/novawallet/Modules/AssetList/AssetListProtocols.swift b/novawallet/Modules/AssetList/AssetListProtocols.swift index b8bc981c57..d8b0c46b95 100644 --- a/novawallet/Modules/AssetList/AssetListProtocols.swift +++ b/novawallet/Modules/AssetList/AssetListProtocols.swift @@ -89,3 +89,4 @@ protocol AssetListWireframeProtocol: AnyObject, WalletSwitchPresentable, AlertPr typealias WalletConnectSessionsError = WalletConnectSessionsInteractorError typealias TransferCompletionClosure = (ChainAsset) -> Void typealias BuyTokensClosure = () -> Void +typealias SwapCompletionClosure = (ChainAsset) -> Void diff --git a/novawallet/Modules/AssetList/AssetListWireframe.swift b/novawallet/Modules/AssetList/AssetListWireframe.swift index f7bfab1f7b..278666873a 100644 --- a/novawallet/Modules/AssetList/AssetListWireframe.swift +++ b/novawallet/Modules/AssetList/AssetListWireframe.swift @@ -15,10 +15,15 @@ final class AssetListWireframe: AssetListWireframeProtocol { } func showAssetDetails(from view: AssetListViewProtocol?, chain: ChainModel, asset: AssetModel) { + let swapCompletionClosure: (ChainAsset) -> Void = { [weak self] chainAsset in + self?.showAssetDetails(from: view, chain: chainAsset.chain, asset: chainAsset.asset) + } + guard let assetDetailsView = AssetDetailsContainerViewFactory.createView( assetListObservable: assetListModelObservable, chain: chain, - asset: asset + asset: asset, + swapCompletionClosure: swapCompletionClosure ), let navigationController = view?.controller.navigationController else { return @@ -30,9 +35,14 @@ final class AssetListWireframe: AssetListWireframeProtocol { } func showHistory(from view: AssetListViewProtocol?, chain: ChainModel, asset: AssetModel) { + let swapCompletionClosure: (ChainAsset) -> Void = { [weak self] chainAsset in + self?.showAssetDetails(from: view, chain: chainAsset.chain, asset: chainAsset.asset) + } + guard let history = TransactionHistoryViewFactory.createView( chainAsset: .init(chain: chain, asset: asset), - assetListObservable: assetListModelObservable + assetListObservable: assetListModelObservable, + swapCompletionClosure: swapCompletionClosure ) else { return } @@ -130,10 +140,16 @@ final class AssetListWireframe: AssetListWireframeProtocol { } func showSwapTokens(from view: AssetListViewProtocol?) { + let completionClosure: (ChainAsset) -> Void = { [weak self] chainAsset in + self?.showAssetDetails(from: view, chain: chainAsset.chain, asset: chainAsset.asset) + } let selectClosure: (ChainAsset) -> Void = { [weak self] chainAsset in - self?.showSwapTokens(from: view, payAsset: chainAsset) + self?.showSwapTokens( + from: view, + payAsset: chainAsset, + swapCompletionClosure: completionClosure + ) } - guard let swapDirectionsView = SwapAssetsOperationViewFactory.createSelectPayTokenView( for: assetListModelObservable, selectClosureStrategy: .callbackAfterDismissal, @@ -190,10 +206,15 @@ final class AssetListWireframe: AssetListWireframeProtocol { tabBarController.selectedIndex = MainTabBarIndex.staking } - private func showSwapTokens(from view: AssetListViewProtocol?, payAsset: ChainAsset) { + private func showSwapTokens( + from view: AssetListViewProtocol?, + payAsset: ChainAsset, + swapCompletionClosure: SwapCompletionClosure? + ) { guard let swapTokensView = SwapSetupViewFactory.createView( assetListObservable: assetListModelObservable, - payChainAsset: payAsset + payChainAsset: payAsset, + swapCompletionClosure: swapCompletionClosure ) else { return } diff --git a/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift b/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift index 55237adddc..8b80e3c009 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift @@ -7,7 +7,8 @@ struct OperationDetailsViewFactory { static func createView( for transaction: TransactionHistoryItem, chainAsset: ChainAsset, - assetListObservable: AssetListModelObservable + assetListObservable: AssetListModelObservable, + swapCompletionClosure: SwapCompletionClosure? ) -> OperationDetailsViewProtocol? { guard let currencyManager = CurrencyManager.shared, @@ -60,7 +61,10 @@ struct OperationDetailsViewFactory { ) } - let wireframe = OperationDetailsWireframe(assetListObservable: assetListObservable) + let wireframe = OperationDetailsWireframe( + assetListObservable: assetListObservable, + swapCompletionClosure: swapCompletionClosure + ) let localizationManager = LocalizationManager.shared let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) diff --git a/novawallet/Modules/OperationDetails/OperationDetailsWireframe.swift b/novawallet/Modules/OperationDetails/OperationDetailsWireframe.swift index d9d87b7616..592988e9c7 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsWireframe.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsWireframe.swift @@ -2,9 +2,14 @@ import Foundation final class OperationDetailsWireframe: OperationDetailsWireframeProtocol { let assetListObservable: AssetListModelObservable + let swapCompletionClosure: SwapCompletionClosure? - init(assetListObservable: AssetListModelObservable) { + init( + assetListObservable: AssetListModelObservable, + swapCompletionClosure: SwapCompletionClosure? + ) { self.assetListObservable = assetListObservable + self.swapCompletionClosure = swapCompletionClosure } func showSend( @@ -28,7 +33,8 @@ final class OperationDetailsWireframe: OperationDetailsWireframeProtocol { ) { guard let swapView = SwapSetupViewFactory.createView( assetListObservable: assetListObservable, - initState: state + initState: state, + swapCompletionClosure: swapCompletionClosure ) else { return } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 1e0432a2fe..9081341c21 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -425,8 +425,15 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { } func didReceiveConfirmation(hash _: String) { + guard let payChainAsset = getPayChainAsset() else { + return + } view?.didReceiveStopLoading() - wireframe.complete(on: view, locale: selectedLocale) + wireframe.complete( + on: view, + payChainAsset: payChainAsset, + locale: selectedLocale + ) } } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift index 7f45947be3..00fdf43bcc 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift @@ -35,7 +35,11 @@ protocol SwapConfirmInteractorOutputProtocol: SwapBaseInteractorOutputProtocol { protocol SwapConfirmWireframeProtocol: SwapBaseWireframeProtocol, AddressOptionsPresentable, ShortTextInfoPresentable, ModalAlertPresenting, MessageSheetPresentable { - func complete(on view: ControllerBackedProtocol?, locale: Locale) + func complete( + on view: ControllerBackedProtocol?, + payChainAsset: ChainAsset, + locale: Locale + ) } enum SwapConfirmError: Error { diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index 5d26a1c670..a457bb6df7 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -5,7 +5,8 @@ import RobinHood struct SwapConfirmViewFactory { static func createView( initState: SwapConfirmInitState, - generalSubscriptonFactory: GeneralStorageSubscriptionFactoryProtocol + generalSubscriptonFactory: GeneralStorageSubscriptionFactoryProtocol, + completionClosure: SwapCompletionClosure? ) -> SwapConfirmViewProtocol? { guard let currencyManager = CurrencyManager.shared, let wallet = SelectedWalletSettings.shared.value else { return nil @@ -19,7 +20,7 @@ struct SwapConfirmViewFactory { return nil } - let wireframe = SwapConfirmWireframe() + let wireframe = SwapConfirmWireframe(completionClosure: completionClosure) let balanceViewModelFactoryFacade = BalanceViewModelFactoryFacade( priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager) diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift index 34a9fb5af5..ec59831f95 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift @@ -1,13 +1,24 @@ import Foundation final class SwapConfirmWireframe: SwapConfirmWireframeProtocol { - func complete(on view: ControllerBackedProtocol?, locale: Locale) { + let completionClosure: SwapCompletionClosure? + + init(completionClosure: SwapCompletionClosure?) { + self.completionClosure = completionClosure + } + + func complete( + on view: ControllerBackedProtocol?, + payChainAsset: ChainAsset, + locale: Locale + ) { let title = R.string.localizable .commonTransactionSubmitted(preferredLanguages: locale.rLanguages) let presenter = view?.controller.navigationController?.presentingViewController presenter?.dismiss(animated: true) { + self.completionClosure?(payChainAsset) self.presentSuccessNotification(title, from: presenter, completion: nil) } } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift index 6a1ca9b022..49a3d709ca 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewFactory.swift @@ -5,17 +5,20 @@ import RobinHood struct SwapSetupViewFactory { static func createView( assetListObservable: AssetListModelObservable, - payChainAsset: ChainAsset + payChainAsset: ChainAsset, + swapCompletionClosure: SwapCompletionClosure? ) -> SwapSetupViewProtocol? { createView( assetListObservable: assetListObservable, - initState: .init(payChainAsset: payChainAsset) + initState: .init(payChainAsset: payChainAsset), + swapCompletionClosure: swapCompletionClosure ) } static func createView( assetListObservable: AssetListModelObservable, - initState: SwapSetupInitState + initState: SwapSetupInitState, + swapCompletionClosure: SwapCompletionClosure? ) -> SwapSetupViewProtocol? { guard let currencyManager = CurrencyManager.shared, @@ -39,7 +42,8 @@ struct SwapSetupViewFactory { let wireframe = SwapSetupWireframe( assetListObservable: assetListObservable, - state: generalLocalSubscriptionFactory + state: generalLocalSubscriptionFactory, + swapCompletionClosure: swapCompletionClosure ) let issuesViewModelFactory = SwapIssueViewModelFactory( diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index c7ca661494..ae34b8e9d8 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -5,10 +5,16 @@ import SoraUI final class SwapSetupWireframe: SwapSetupWireframeProtocol { let assetListObservable: AssetListModelObservable let state: GeneralStorageSubscriptionFactoryProtocol + let swapCompletionClosure: SwapCompletionClosure? - init(assetListObservable: AssetListModelObservable, state: GeneralStorageSubscriptionFactoryProtocol) { + init( + assetListObservable: AssetListModelObservable, + state: GeneralStorageSubscriptionFactoryProtocol, + swapCompletionClosure: SwapCompletionClosure? + ) { self.assetListObservable = assetListObservable self.state = state + self.swapCompletionClosure = swapCompletionClosure } func showPayTokenSelection( @@ -77,7 +83,8 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { ) { guard let confimView = SwapConfirmViewFactory.createView( initState: initState, - generalSubscriptonFactory: state + generalSubscriptonFactory: state, + completionClosure: swapCompletionClosure ) else { return } diff --git a/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift b/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift index 54aa5b0f0e..7fff637f86 100644 --- a/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift +++ b/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift @@ -6,7 +6,8 @@ import RobinHood struct TransactionHistoryViewFactory { static func createView( chainAsset: ChainAsset, - assetListObservable: AssetListModelObservable + assetListObservable: AssetListModelObservable, + swapCompletionClosure: SwapCompletionClosure? ) -> TransactionHistoryViewProtocol? { guard let selectedMetaAccount = SelectedWalletSettings.shared.value, @@ -24,7 +25,8 @@ struct TransactionHistoryViewFactory { let wireframe = TransactionHistoryWireframe( chainAsset: chainAsset, - assetListObservable: assetListObservable + assetListObservable: assetListObservable, + swapCompletionClosure: swapCompletionClosure ) let balanceViewModelFactory = BalanceViewModelFactory( diff --git a/novawallet/Modules/TransactionHistory/TransactionHistoryWireframe.swift b/novawallet/Modules/TransactionHistory/TransactionHistoryWireframe.swift index a9d0003629..b6e24e9b45 100644 --- a/novawallet/Modules/TransactionHistory/TransactionHistoryWireframe.swift +++ b/novawallet/Modules/TransactionHistory/TransactionHistoryWireframe.swift @@ -3,13 +3,16 @@ import UIKit final class TransactionHistoryWireframe: TransactionHistoryWireframeProtocol { let chainAsset: ChainAsset let assetListObservable: AssetListModelObservable + let swapCompletionClosure: SwapCompletionClosure? init( chainAsset: ChainAsset, - assetListObservable: AssetListModelObservable + assetListObservable: AssetListModelObservable, + swapCompletionClosure: SwapCompletionClosure? ) { self.chainAsset = chainAsset self.assetListObservable = assetListObservable + self.swapCompletionClosure = swapCompletionClosure } func showFilter( @@ -34,7 +37,8 @@ final class TransactionHistoryWireframe: TransactionHistoryWireframeProtocol { guard let operationDetailsView = OperationDetailsViewFactory.createView( for: operation, chainAsset: chainAsset, - assetListObservable: assetListObservable + assetListObservable: assetListObservable, + swapCompletionClosure: swapCompletionClosure ) else { return } From 6effe21a1adde6870ef4b28f2410c03fbfd9b863 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Mon, 13 Nov 2023 19:07:53 +0300 Subject: [PATCH 164/204] fixes --- .../Swaps/Setup/SwapSetupPresenter.swift | 18 ++++++++++++++---- .../Swaps/Setup/SwapSetupProtocols.swift | 2 +- .../Swaps/Setup/SwapSetupViewController.swift | 12 +++++++++++- novawallet/en.lproj/Localizable.strings | 1 + novawallet/ru.lproj/Localizable.strings | 1 + 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 000316f47e..ef9a362211 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -11,7 +11,7 @@ final class SwapSetupPresenter: SwapBasePresenter { private(set) var quoteArgs: AssetConversion.QuoteArgs? { didSet { - provideDetailsViewModel(isAvailable: quoteArgs != nil) + provideDetailsViewModel() } } @@ -25,6 +25,15 @@ final class SwapSetupPresenter: SwapBasePresenter { private var feeIdentifier: SwapSetupFeeIdentifier? private var slippage: BigRational + private var issues: [SwapSetupViewIssue] = [] { + didSet { + provideDetailsViewModel() + } + } + + private var detailsAvailable: Bool { + !issues.contains(.noLiqudity) && quoteArgs != nil + } init( payChainAsset: ChainAsset?, @@ -419,8 +428,8 @@ extension SwapSetupPresenter { view?.didReceiveSettingsState(isAvailable: payChainAsset != nil) } - private func provideDetailsViewModel(isAvailable: Bool) { - view?.didReceiveDetailsState(isAvailable: isAvailable) + private func provideDetailsViewModel() { + view?.didReceiveDetailsState(isAvailable: detailsAvailable) } private func provideRateViewModel() { @@ -466,6 +475,7 @@ extension SwapSetupPresenter { private func provideIssues() { let issues = viewModelFactory.detectIssues(in: getIssueParams(), locale: selectedLocale) + self.issues = issues view?.didReceive(issues: issues) } @@ -591,7 +601,7 @@ extension SwapSetupPresenter { private func updateViews() { providePayAssetViews() provideReceiveAssetViews() - provideDetailsViewModel(isAvailable: quoteArgs != nil) + provideDetailsViewModel() provideButtonState() provideSettingsState() provideIssues() diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 553f79253d..9f1499050a 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -98,7 +98,7 @@ enum SwapSetupError: Error { case blockNumber(Error) } -enum SwapSetupViewIssue { +enum SwapSetupViewIssue: Equatable { case zeroBalance case insufficientBalance case minBalanceViolation(String) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 4a3e23d6bf..135c867b10 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -7,6 +7,7 @@ final class SwapSetupViewController: UIViewController, ViewHolder { let presenter: SwapSetupPresenterProtocol private var toggledDetailsManually: Bool = false + private var depositTokenSymbol: String = "" init( presenter: SwapSetupPresenterProtocol, @@ -99,6 +100,7 @@ final class SwapSetupViewController: UIViewController, ViewHolder { title = R.string.localizable.commonSwap(preferredLanguages: selectedLocale.rLanguages) rootView.setup(locale: selectedLocale) setupAccessoryView() + setupDepositTokenButton() } private func setupAccessoryView() { @@ -121,6 +123,13 @@ final class SwapSetupViewController: UIViewController, ViewHolder { ) } + private func setupDepositTokenButton() { + rootView.depositTokenButton.imageWithTitleView?.title = R.string.localizable.swapsSetupDepositButtonTitle( + depositTokenSymbol, + preferredLanguages: selectedLocale.rLanguages + ) + } + @objc private func selectPayTokenAction() { rootView.receiveAmountInputView.endEditing(true) presenter.selectPayToken() @@ -200,7 +209,8 @@ extension SwapSetupViewController: SwapSetupViewProtocol { switch viewModel { case let .asset(assetViewModel): rootView.payAmountInputView.bind(assetViewModel: assetViewModel) - rootView.depositTokenButton.imageWithTitleView?.title = "Get \(assetViewModel.symbol)" + depositTokenSymbol = assetViewModel.symbol + setupDepositTokenButton() case let .empty(emptySwapsAssetViewModel): rootView.payAmountInputView.bind(emptyViewModel: emptySwapsAssetViewModel) rootView.depositTokenButton.imageWithTitleView?.title = nil diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index f45ce30ce1..c8fdaa6109 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1425,3 +1425,4 @@ "swaps.not.enough.tokens" = "Not enough tokens to swap"; "swaps.not.enough.liquidity" = "Not enough liquidity"; "swaps.pay.asset.fee.ed.message" = "To pay network fee with %@, Nova will automatically swap %@ for %@ to maintain your account's minimum %@ balance."; +"swaps.setup.deposit.button.title" = "Get %@"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index be8c644e37..1eb98a88b4 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1425,3 +1425,4 @@ "swaps.not.enough.tokens" = "Недостаточно токенов для обмена"; "swaps.not.enough.liquidity" = "Недостаточно ликвидности"; "swaps.pay.asset.fee.ed.message" = "Для оплаты комиссии сети %@ токеном, Nova автоматически поменяет %@ в %@ для сохранения минимального %@ баланса аккаунта."; +"swaps.setup.deposit.button.title" = "Получить %@"; From 3128acafd50ea31edcc5eadb095c17f570d5bd28 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 13 Nov 2023 18:21:21 +0100 Subject: [PATCH 165/204] fix swaps parsing --- novawallet.xcodeproj/project.pbxproj | 8 +- .../iconSwapHistory.imageset/Contents.json | 15 +++ .../iconSwapHistory.pdf | Bin 0 -> 2338 bytes .../Contents.json | 2 +- .../iconSwapOnDetails.pdf} | Bin ...sactionHistoryItem+CoreDataDecodable.swift | 3 + ...ft => PriceHistoryCalculatorFactory.swift} | 11 +- .../Common/Model/TransactionHistoryItem.swift | 2 +- .../Subquery/Filter/SubqueryFilter.swift | 8 ++ .../Subquery/Models/SubqueryHistory.swift | 12 +- .../Subquery/SubqueryHistory+Wallet.swift | 24 ++-- .../SubqueryHistoryOperationFactory.swift | 115 +++++++++--------- .../OperationDetailsContractProvider.swift | 2 +- ...OperationDetailsDataProviderProtocol.swift | 2 +- ...perationDetailsDirectStakingProvider.swift | 2 +- .../OperationDetailsExtrinsicProvider.swift | 2 +- .../OperationDetailsPoolStakingProvider.swift | 2 +- .../OperationDetailsSwapProvider.swift | 6 +- .../OperationDetailsTransferProvider.swift | 2 +- .../OperationDetailsBaseInteractor.swift | 5 +- .../OperationSwapDetailsInteractor.swift | 2 +- .../OperationDetailsViewModelFactory.swift | 2 +- .../TransactionHistoryViewModelFactory.swift | 2 +- .../Service/AssetHistoryFactoryFacade.swift | 3 +- .../TransactionHistoryFetcherFactory.swift | 20 ++- 25 files changed, 155 insertions(+), 97 deletions(-) create mode 100644 novawallet/Assets.xcassets/iconSwapHistory.imageset/Contents.json create mode 100644 novawallet/Assets.xcassets/iconSwapHistory.imageset/iconSwapHistory.pdf rename novawallet/Assets.xcassets/{iconSwap.imageset => iconSwapOnDetails.imageset}/Contents.json (73%) rename novawallet/Assets.xcassets/{iconSwap.imageset/flip-swap.pdf => iconSwapOnDetails.imageset/iconSwapOnDetails.pdf} (100%) rename novawallet/Common/Helpers/TransactionHistory/{CalculatorFactory.swift => PriceHistoryCalculatorFactory.swift} (51%) diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 329e80df9d..7052e0bed8 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -810,7 +810,7 @@ 77AAE2202AFB00CB006872CC /* OperationSwapModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AAE21F2AFB00CB006872CC /* OperationSwapModel.swift */; }; 77AAE2222AFB026E006872CC /* OperationDetailsSwapProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AAE2212AFB026E006872CC /* OperationDetailsSwapProvider.swift */; }; 77AAE2242AFB67BE006872CC /* OperationSwapDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AAE2232AFB67BE006872CC /* OperationSwapDetailsInteractor.swift */; }; - 77AAE2262AFC10EE006872CC /* CalculatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AAE2252AFC10EE006872CC /* CalculatorFactory.swift */; }; + 77AAE2262AFC10EE006872CC /* PriceHistoryCalculatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AAE2252AFC10EE006872CC /* PriceHistoryCalculatorFactory.swift */; }; 77AAE2282AFC1167006872CC /* ChainModel+historyId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AAE2272AFC1167006872CC /* ChainModel+historyId.swift */; }; 77AAE22A2AFC36EE006872CC /* OperationDetailsBaseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AAE2292AFC36EE006872CC /* OperationDetailsBaseInteractor.swift */; }; 77AB55592AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */; }; @@ -4886,7 +4886,7 @@ 77AAE21F2AFB00CB006872CC /* OperationSwapModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationSwapModel.swift; sourceTree = ""; }; 77AAE2212AFB026E006872CC /* OperationDetailsSwapProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsSwapProvider.swift; sourceTree = ""; }; 77AAE2232AFB67BE006872CC /* OperationSwapDetailsInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationSwapDetailsInteractor.swift; sourceTree = ""; }; - 77AAE2252AFC10EE006872CC /* CalculatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatorFactory.swift; sourceTree = ""; }; + 77AAE2252AFC10EE006872CC /* PriceHistoryCalculatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceHistoryCalculatorFactory.swift; sourceTree = ""; }; 77AAE2272AFC1167006872CC /* ChainModel+historyId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChainModel+historyId.swift"; sourceTree = ""; }; 77AAE2292AFC36EE006872CC /* OperationDetailsBaseInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsBaseInteractor.swift; sourceTree = ""; }; 77AAE22B2AFCD7AA006872CC /* SubstrateDataModel21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel21.xcdatamodel; sourceTree = ""; }; @@ -14354,7 +14354,7 @@ children = ( 84FD3DB02540C09800A234E3 /* TransactionHistoryMergeManager.swift */, 849B036F2A15EE39009624D9 /* TokenPriceCalculator.swift */, - 77AAE2252AFC10EE006872CC /* CalculatorFactory.swift */, + 77AAE2252AFC10EE006872CC /* PriceHistoryCalculatorFactory.swift */, 77AAE2272AFC1167006872CC /* ChainModel+historyId.swift */, ); path = TransactionHistory; @@ -22204,7 +22204,7 @@ AE4C53E5268C6F8300B03CE8 /* ValidatorListFilterSortCell.swift in Sources */, 885A6C3229A374B600B65C1A /* ReferendumVotersLocalWrapperFactory.swift in Sources */, 842B17FB28648FDC0014CC57 /* ChainAssetViewModelFactory.swift in Sources */, - 77AAE2262AFC10EE006872CC /* CalculatorFactory.swift in Sources */, + 77AAE2262AFC10EE006872CC /* PriceHistoryCalculatorFactory.swift in Sources */, 1062C095BC566A1EA8DE1C06 /* CrowdloanContributionSetupViewController.swift in Sources */, 849C7BDB2A1B236900434621 /* GladingBaseView.swift in Sources */, 84EE2FB32891442F00A98816 /* WalletManageViewFactory.swift in Sources */, diff --git a/novawallet/Assets.xcassets/iconSwapHistory.imageset/Contents.json b/novawallet/Assets.xcassets/iconSwapHistory.imageset/Contents.json new file mode 100644 index 0000000000..c7f2c02f5f --- /dev/null +++ b/novawallet/Assets.xcassets/iconSwapHistory.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "iconSwapHistory.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "original" + } +} diff --git a/novawallet/Assets.xcassets/iconSwapHistory.imageset/iconSwapHistory.pdf b/novawallet/Assets.xcassets/iconSwapHistory.imageset/iconSwapHistory.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6bf538326dceb9c1ef3264df1a1d37ac8f1eadfe GIT binary patch literal 2338 zcmbVOO>Y}F5WVlO;Ke|4$ci)MkV7Ca(AZ8;v_)OHx1a}A-Z(03Z7XdGx4%AbNPRgj zkf3{zsJHX^W`?7)>&v&VsLx$T&b#sVKRV}Lzjm{$$LZb8w3vqWi|f~N;d~$1<8kNb zkbL&cuIO7N&(BSNSl^n%j-Pn`aai9?A6)=AH>vHk85R%S>~i>fwH)T}-nfgK(_hPN z_g80r@3tNF78|1T`@^fl>Es@7EsQVf?Ia6&oRPS#VBxg_rOAmCB=%xR5iVDDg{W80tmLe zPf5*IW<*}9Eo6d-fGKN?rP?ygt0V3`i9`>OdR8w%TIk>X>? zhImgo1L2Yqj6si)r6mB2qGn%hCAfes<2h>K0#H&E@y;M5KoL+;&LzPb2}d6s!A6eC z+{|$`2QWw~Io2fLqlh>HHA1Xh!Ki^j4!}f3C4&?T7~C5aS)`x}c%@XOBB^G96O=}M zNnGuL%@AM@Ie`?DG|xIm@v z8Uf`*2;-|GKo(~KOCUJJtl$zl+M{%eY6~iotmOd}Uj(cJLdRel37Pp2#Z)6nfYh=x zP=(OOW-oBCIRGeVqm%(6?WC}04OI%p1QkYWfha(TK%iRZCgNSQm%$?eM7waQZEtmt zl5Id(bL%aFZA$1vfSw9sw5~v@p$=q6vg00?Q#4Ot4B5sHM|Lgk=x3Kt%nDp$5Dr&SJ|mZXpar#L#o zMEjtivT-Wbf(9xkvf0DYMiU{!eemLN;RV9`BZ%3h&?`k;2~W|8DN3B`6v0qv^CUzi zR=Sc%F1TZmofzAVn~YMmAw#8nbB}op3B|0kc7RR9j8SfuI7{J%l^D=Uh^Tl)Jhnv4bC#%z-in!cS zIeQMa*kcU$(6m{s?uSih|6;zs1RXE7xDq_OS^Ya8`ubmE Rv6*T*SO%#(JG=V%?PpdK TokenPriceCalculatorProtocol? + func replace(history: PriceHistory, priceId: AssetModel.PriceId) } -final class CalculatorFactory: CalculatorFactoryProtocol { - var priceHistory: [AssetModel.PriceId: PriceHistory?] = [:] +final class PriceHistoryCalculatorFactory: PriceHistoryCalculatorFactoryProtocol { + let priceHistory: [AssetModel.PriceId: PriceHistory?] = [:] + + func replace(history: PriceHistory, priceId: AssetModel.PriceId) { + priceHistory[priceId] = history + } func createPriceCalculator(for priceId: String?) -> TokenPriceCalculatorProtocol? { guard let priceId = priceId, diff --git a/novawallet/Common/Model/TransactionHistoryItem.swift b/novawallet/Common/Model/TransactionHistoryItem.swift index 13f6153d49..4a115c1999 100644 --- a/novawallet/Common/Model/TransactionHistoryItem.swift +++ b/novawallet/Common/Model/TransactionHistoryItem.swift @@ -46,7 +46,7 @@ struct TransactionHistoryItem: Codable { let txHash: String let timestamp: Int64 let fee: String? - let feeAssetId: String? + let feeAssetId: UInt32? let blockNumber: UInt64? let txIndex: UInt16? let callPath: CallCodingPath diff --git a/novawallet/Common/Network/Subquery/Filter/SubqueryFilter.swift b/novawallet/Common/Network/Subquery/Filter/SubqueryFilter.swift index 23bad00945..a8488e58e2 100644 --- a/novawallet/Common/Network/Subquery/Filter/SubqueryFilter.swift +++ b/novawallet/Common/Network/Subquery/Filter/SubqueryFilter.swift @@ -61,6 +61,14 @@ struct SubqueryNotFilter: SubqueryFilter { } } +struct SubqueryNotWithCompoundFilter: SubqueryFilter { + let inner: SubqueryCompoundFilter + + func rawSubqueryFilter() -> String { + "not: { \(inner.rawSubqueryFilter()) }" + } +} + struct SubqueryContainsFilter: SubqueryFilter { let fieldName: String let inner: SubqueryFilter diff --git a/novawallet/Common/Network/Subquery/Models/SubqueryHistory.swift b/novawallet/Common/Network/Subquery/Models/SubqueryHistory.swift index 9f98ebedda..a0ea5d26f2 100644 --- a/novawallet/Common/Network/Subquery/Models/SubqueryHistory.swift +++ b/novawallet/Common/Network/Subquery/Models/SubqueryHistory.swift @@ -49,17 +49,25 @@ struct SubqueryPoolRewardOrSlash: Codable { } struct SubquerySwap: Codable { - let assetIdIn: String? + let assetIdIn: String let amountIn: String - let assetIdOut: String? + let assetIdOut: String let amountOut: String let sender: String let receiver: String + let assetIdFee: String let fee: String let eventIdx: Int + let success: Bool + + var isFeeNative: Bool { + assetIdFee == SubqueryHistoryElement.nativeFeeAssetId + } } struct SubqueryHistoryElement: Decodable { + static let nativeFeeAssetId = "native" + enum CodingKeys: String, CodingKey { case identifier = "id" case blockNumber diff --git a/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift b/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift index 5039eb1f4e..5cd83addb4 100644 --- a/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift +++ b/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift @@ -118,7 +118,15 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { let source = TransactionHistoryItemSource.substrate let remoteIdentifier = TransactionHistoryItem.createIdentifier(from: identifier, source: source) let assetIdIn = chainAsset.chain.asset(byHistoryAssetId: swap.assetIdIn) ?? chainAsset.chain.utilityAsset() - let direction: AssetConversion.Direction = assetIdIn?.assetId == chainAsset.asset.assetId ? .sell : .buy + + let feeAsset: AssetModel? + + if swap.isFeeNative { + feeAsset = chainAsset.chain.utilityAsset() + } else { + feeAsset = chainAsset.chain.asset(byHistoryAssetId: swap.assetIdFee) + } + return .init( identifier: remoteIdentifier, source: source, @@ -127,22 +135,20 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { sender: swap.sender.normalize(for: chainFormat) ?? swap.sender, receiver: swap.receiver.normalize(for: chainFormat) ?? swap.receiver, amountInPlank: nil, - // TODO: Status decoding - status: .success, + status: swap.success ? .success : .failed, txHash: extrinsicHash ?? identifier, timestamp: itemTimestamp, fee: swap.fee, - // TODO: feeAssetId decoding - feeAssetId: nil, + feeAssetId: feeAsset?.assetId, blockNumber: blockNumber, - txIndex: UInt16(swap.eventIdx), - callPath: CallCodingPath.swap(direction: direction), + txIndex: nil, + callPath: CallCodingPath.swap(direction: .sell), call: nil, swap: .init( amountIn: swap.amountIn, - assetIdIn: swap.assetIdIn, + assetIdIn: swap.assetIdIn == SubqueryHistoryElement.nativeFeeAssetId ? nil : swap.assetIdIn, amountOut: swap.amountOut, - assetIdOut: swap.assetIdOut + assetIdOut: swap.assetIdOut == SubqueryHistoryElement.nativeFeeAssetId ? nil : swap.assetIdOut ) ) } diff --git a/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift b/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift index a9107f4a15..08a0689ee4 100644 --- a/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift +++ b/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift @@ -16,13 +16,11 @@ final class SubqueryHistoryOperationFactory { let assetId: String? let hasPoolStaking: Bool let hasSwaps: Bool - let isUtilityAsset: Bool init( url: URL, filter: WalletHistoryFilter, assetId: String?, - isUtilityAsset: Bool, hasPoolStaking: Bool, hasSwaps: Bool ) { @@ -31,35 +29,52 @@ final class SubqueryHistoryOperationFactory { self.assetId = assetId self.hasPoolStaking = hasPoolStaking self.hasSwaps = hasSwaps - self.isUtilityAsset = isUtilityAsset } private func prepareExtrinsicInclusionFilter() -> String { - """ - { - and: [ - { - extrinsic: {isNull: false} - }, - { - not: { - and: [ - { extrinsic: { contains: {module: "balances"} } }, - { - or: [ - { extrinsic: {contains: {call: "transfer"} } }, - { extrinsic: {contains: {call: "transferKeepAlive"} } }, - { extrinsic: {contains: {call: "forceTransfer"} } }, - { extrinsic: {contains: {call: "transferAll"} } }, - { extrinsic: {contains: {call: "transferAllowDeath"} } } - ] - } - ] - } - } - ] + let transferCallNames = ["transfer", "transferKeepAlive", "forceTransfer", "transferAll", "transferAllowDeath"] + + let transferFilters = transferCallNames.map { transferCallName in + SubqueryContainsFilter( + fieldName: "extrinsic", + inner: SubqueryValueFilter(fieldName: "call", value: transferCallName) + ) } - """ + + let swapCallNames = ["swapExactTokensForTokens", "swapTokensForExactTokens"] + + let swapFilters = swapCallNames.map { swapCallName in + SubqueryContainsFilter( + fieldName: "extrinsic", + inner: SubqueryValueFilter(fieldName: "call", value: swapCallName) + ) + } + + return SubqueryInnerFilter( + inner: SubqueryCompoundFilter.and( + [ + SubqueryIsNotNullFilter(fieldName: "extrinsic"), + SubqueryNotWithCompoundFilter( + inner: .or([ + SubqueryCompoundFilter.and([ + SubqueryContainsFilter( + fieldName: "extrinsic", + inner: SubqueryValueFilter(fieldName: "module", value: "balances") + ), + SubqueryCompoundFilter.or(transferFilters) + ]), + SubqueryCompoundFilter.and([ + SubqueryContainsFilter( + fieldName: "extrinsic", + inner: SubqueryValueFilter(fieldName: "module", value: "assetConversion") + ), + SubqueryCompoundFilter.or(swapFilters) + ]) + ]) + ) + ] + ) + ).rawSubqueryFilter() } private func prepareAssetIdFilter(_ assetId: String) -> String { @@ -71,32 +86,18 @@ final class SubqueryHistoryOperationFactory { } private func prepareSwapAssetIdFilter(_ assetId: String?) -> String { - let filters: [SubqueryFilter] - if let assetId = assetId { - filters = [ - SubqueryContainsFilter( - fieldName: "swap", - inner: SubqueryValueFilter(fieldName: "assetIdIn", value: assetId) - ), - SubqueryContainsFilter( - fieldName: "swap", - inner: SubqueryValueFilter(fieldName: "assetIdOut", value: assetId) - ) - ] - } else { - filters = [ - SubqueryContainsFilter( - fieldName: "swap", - inner: SubqueryIsNullFilter(fieldName: "assetIdIn") - ), - SubqueryContainsFilter( - fieldName: "swap", - inner: SubqueryIsNullFilter(fieldName: "assetIdOut") - ), - SubqueryNotFilter(fieldName: "swap", inner: SubqueryContainsKeyFilter(fieldName: "assetIdIn")), - SubqueryNotFilter(fieldName: "swap", inner: SubqueryContainsKeyFilter(fieldName: "assetIdOut")) - ] - } + let assetIdOrNative = assetId ?? SubqueryHistoryElement.nativeFeeAssetId + + let filters = [ + SubqueryContainsFilter( + fieldName: "swap", + inner: SubqueryValueFilter(fieldName: "assetIdIn", value: assetIdOrNative) + ), + SubqueryContainsFilter( + fieldName: "swap", + inner: SubqueryValueFilter(fieldName: "assetIdOut", value: assetIdOrNative) + ) + ] return SubqueryInnerFilter(inner: SubqueryCompoundFilter.or(filters)).rawSubqueryFilter() } @@ -130,13 +131,7 @@ final class SubqueryHistoryOperationFactory { } if filter.contains(.swaps), hasSwaps { - if let assetId = assetId { - filterStrings.append(prepareSwapAssetIdFilter(assetId)) - } else if isUtilityAsset { - filterStrings.append(prepareSwapAssetIdFilter(nil)) - } else { - filterStrings.append("{ swap: { isNull: false } }") - } + filterStrings.append(prepareSwapAssetIdFilter(assetId)) } return filterStrings.joined(separator: ",") diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsContractProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsContractProvider.swift index 6423269b8b..12460bddae 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsContractProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsContractProvider.swift @@ -6,7 +6,7 @@ final class OperationDetailsContractProvider: OperationDetailsBaseProvider {} extension OperationDetailsContractProvider: OperationDetailsDataProviderProtocol { func extractOperationData( replacingWith newFee: BigUInt?, - calculatorFactory: CalculatorFactoryProtocol, + calculatorFactory: PriceHistoryCalculatorFactoryProtocol, progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void ) { let priceCalculator = calculatorFactory.createPriceCalculator(for: chainAsset.asset.priceId) diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderProtocol.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderProtocol.swift index 88aa21e0dc..56c41777a8 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderProtocol.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderProtocol.swift @@ -4,7 +4,7 @@ import BigInt protocol OperationDetailsDataProviderProtocol { func extractOperationData( replacingWith newFee: BigUInt?, - calculatorFactory: CalculatorFactoryProtocol, + calculatorFactory: PriceHistoryCalculatorFactoryProtocol, progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void ) } diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDirectStakingProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDirectStakingProvider.swift index afabbc7b74..406b5bb053 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDirectStakingProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDirectStakingProvider.swift @@ -52,7 +52,7 @@ final class OperationDetailsDirectStakingProvider: OperationDetailsBaseProvider, extension OperationDetailsDirectStakingProvider: OperationDetailsDataProviderProtocol { func extractOperationData( replacingWith _: BigUInt?, - calculatorFactory: CalculatorFactoryProtocol, + calculatorFactory: PriceHistoryCalculatorFactoryProtocol, progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void ) { let context = try? transaction.call.map { diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsExtrinsicProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsExtrinsicProvider.swift index 5d37ba0ade..ea5e2a9e2c 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsExtrinsicProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsExtrinsicProvider.swift @@ -6,7 +6,7 @@ final class OperationDetailsExtrinsicProvider: OperationDetailsBaseProvider {} extension OperationDetailsExtrinsicProvider: OperationDetailsDataProviderProtocol { func extractOperationData( replacingWith newFee: BigUInt?, - calculatorFactory: CalculatorFactoryProtocol, + calculatorFactory: PriceHistoryCalculatorFactoryProtocol, progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void ) { guard let accountAddress = accountAddress else { diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsPoolStakingProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsPoolStakingProvider.swift index 040471af22..5545dd25b7 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsPoolStakingProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsPoolStakingProvider.swift @@ -133,7 +133,7 @@ final class OperationDetailsPoolStakingProvider: OperationDetailsBaseProvider, A extension OperationDetailsPoolStakingProvider: OperationDetailsDataProviderProtocol { func extractOperationData( replacingWith _: BigUInt?, - calculatorFactory: CalculatorFactoryProtocol, + calculatorFactory: PriceHistoryCalculatorFactoryProtocol, progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void ) { let optContext = try? transaction.call.map { diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift index 1835a6f0df..ee4a86b531 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift @@ -22,14 +22,14 @@ final class OperationDetailsSwapProvider { extension OperationDetailsSwapProvider: OperationDetailsDataProviderProtocol { func extractOperationData( replacingWith newFee: BigUInt?, - calculatorFactory: CalculatorFactoryProtocol, + calculatorFactory: PriceHistoryCalculatorFactoryProtocol, progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void ) { guard let swap = transaction.swap, let assetIn = chain.asset(byHistoryAssetId: swap.assetIdIn) ?? chain.utilityAsset(), let assetOut = chain.asset(byHistoryAssetId: swap.assetIdOut) ?? chain.utilityAsset(), - let feeAsset = chain.asset(byHistoryAssetId: transaction.feeAssetId) ?? chain.utilityAsset(), + let feeAsset = transaction.feeAssetId.flatMap({ chain.asset(for: $0) }) ?? chain.utilityAsset(), let wallet = WalletDisplayAddress(response: selectedAccount) else { progressClosure(nil) return @@ -78,7 +78,7 @@ extension OperationDetailsSwapProvider: OperationDetailsDataProviderProtocol { } private func calculatePrice( - calculatorFactory: CalculatorFactoryProtocol, + calculatorFactory: PriceHistoryCalculatorFactoryProtocol, assetModel: AssetModel?, timestamp: UInt64 ) -> PriceData? { diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsTransferProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsTransferProvider.swift index f71437e8ed..f8889ce08f 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsTransferProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsTransferProvider.swift @@ -27,7 +27,7 @@ final class OperationDetailsTransferProvider: OperationDetailsBaseProvider, Acco extension OperationDetailsTransferProvider: OperationDetailsDataProviderProtocol { func extractOperationData( replacingWith newFee: BigUInt?, - calculatorFactory: CalculatorFactoryProtocol, + calculatorFactory: PriceHistoryCalculatorFactoryProtocol, progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void ) { guard let accountAddress = accountAddress else { diff --git a/novawallet/Modules/OperationDetails/OperationDetailsBaseInteractor.swift b/novawallet/Modules/OperationDetails/OperationDetailsBaseInteractor.swift index 3c42a60d4d..5e960fb05f 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsBaseInteractor.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsBaseInteractor.swift @@ -21,7 +21,7 @@ class OperationDetailsBaseInteractor: AccountFetching, AnyCancellableCleaning { private var transactionProvider: StreamableProvider? private var priceCalculators: [AssetModel.PriceId: TokenPriceCalculatorProtocol] = [:] - private var calculatorFactory = CalculatorFactory() + private var calculatorFactory = PriceHistoryCalculatorFactory() init( transaction: TransactionHistoryItem, @@ -141,7 +141,8 @@ extension OperationDetailsBaseInteractor: PriceLocalStorageSubscriber, PriceLoca ) { switch result { case let .success(history): - calculatorFactory.priceHistory[priceId] = history + calculatorFactory.replace(history: history, priceId: priceId) + provideModel(overridingBy: nil, newFee: nil) case let .failure(error): presenter?.didReceiveDetails(result: .failure(error)) } diff --git a/novawallet/Modules/OperationDetails/OperationSwapDetailsInteractor.swift b/novawallet/Modules/OperationDetails/OperationSwapDetailsInteractor.swift index d4ed8e13e0..707b2178ac 100644 --- a/novawallet/Modules/OperationDetails/OperationSwapDetailsInteractor.swift +++ b/novawallet/Modules/OperationDetails/OperationSwapDetailsInteractor.swift @@ -9,7 +9,7 @@ final class OperationSwapDetailsInteractor: OperationDetailsBaseInteractor { } let priceAssetIn = chain.asset(byHistoryAssetId: swap.assetIdIn)?.priceId let priceAssetOut = chain.asset(byHistoryAssetId: swap.assetIdOut)?.priceId - let feeAsset = chain.asset(byHistoryAssetId: transaction.feeAssetId) ?? chain.utilityAsset() + let feeAsset = transaction.feeAssetId.flatMap { chain.asset(for: $0) } ?? chain.utilityAsset() let feePriceId = feeAsset?.priceId let prices = [ priceAssetIn, diff --git a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift index 219b009686..ddd1ec9848 100644 --- a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift +++ b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift @@ -56,7 +56,7 @@ final class OperationDetailsViewModelFactory { return nil } case .swap: - let image = R.image.iconSwap()! + let image = R.image.iconSwapOnDetails()! return StaticImageViewModel(image: image) } } diff --git a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift index 9860825911..89f9b8c105 100644 --- a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift +++ b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift @@ -121,7 +121,7 @@ final class TransactionHistoryViewModelFactory { timestamp: data.timestamp, locale: locale ) - let icon = R.image.iconSwap() + let icon = R.image.iconSwapHistory() let imageViewModel = icon.map { StaticImageViewModel(image: $0) } let amountDetails = amountDetails(price: balance.price, time: time, locale: locale) let subtitle = [assetIn?.symbol, assetOut?.symbol].compactMap { $0 }.joined(separator: " → ") diff --git a/novawallet/Modules/TransactionHistory/Service/AssetHistoryFactoryFacade.swift b/novawallet/Modules/TransactionHistory/Service/AssetHistoryFactoryFacade.swift index 3882bb9745..162b189453 100644 --- a/novawallet/Modules/TransactionHistory/Service/AssetHistoryFactoryFacade.swift +++ b/novawallet/Modules/TransactionHistory/Service/AssetHistoryFactoryFacade.swift @@ -29,14 +29,13 @@ final class AssetHistoryFacade { let assetMapper = CustomAssetMapper(type: asset.type, typeExtras: asset.typeExtras) let historyAssetId = try assetMapper.historyAssetId() - // we support only transfers for non utility assets + // we support only transfers and swaps for non utility assets let mappedFilter = asset.isUtility ? filter : [.transfers, .swaps] return SubqueryHistoryOperationFactory( url: url, filter: mappedFilter, assetId: historyAssetId, - isUtilityAsset: asset.isUtility, hasPoolStaking: asset.hasPoolStaking, hasSwaps: chainAsset.chain.hasSwaps ) diff --git a/novawallet/Modules/TransactionHistory/Service/TransactionHistoryFetcherFactory.swift b/novawallet/Modules/TransactionHistory/Service/TransactionHistoryFetcherFactory.swift index 408d19acc9..a14749ba58 100644 --- a/novawallet/Modules/TransactionHistory/Service/TransactionHistoryFetcherFactory.swift +++ b/novawallet/Modules/TransactionHistory/Service/TransactionHistoryFetcherFactory.swift @@ -88,7 +88,25 @@ final class TransactionHistoryFetcherFactory { ) -> TransactionHistoryHybridFetcher { let localProvider = createLocalProvider(from: address, chainAsset: chainAsset, filter: .all) - let repository = repositoryFactory.createTxRepository() + let source = TransactionHistoryItemSource(assetTypeString: chainAsset.asset.type) + + let repository: AnyDataProviderRepository + + if chainAsset.isUtilityAsset { + repository = repositoryFactory.createUtilityAssetTxRepository( + for: address, + chainId: chainAsset.chain.chainId, + assetId: chainAsset.asset.assetId, + source: source + ) + } else { + repository = repositoryFactory.createCustomAssetTxRepository( + for: address, + chainId: chainAsset.chain.chainId, + assetId: chainAsset.asset.assetId, + source: source + ) + } return .init( address: address, From 2131e1570235adea8d145481949412deb4c0bda3 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 14 Nov 2023 06:38:26 +0100 Subject: [PATCH 166/204] fix local query for swaps --- .../Predicate/NSPredicate+Transaction.swift | 31 ++++++++++++++++++- ...sactionHistoryItem+CoreDataDecodable.swift | 19 +++++++++--- .../PriceHistoryCalculatorFactory.swift | 4 +-- .../ChainRegistry/LocalChain/ChainModel.swift | 16 ++++++++++ .../Common/Model/TransactionHistoryItem.swift | 4 +-- .../Subquery/SubqueryHistory+Wallet.swift | 21 +++++++------ .../SubstrateDataModel21.xcdatamodel/contents | 6 ++-- .../OperationDetailsSwapProvider.swift | 6 ++-- .../OperationDetailsBaseInteractor.swift | 6 ++-- .../OperationSwapDetailsInteractor.swift | 7 ++--- .../TransactionHistoryViewModelFactory.swift | 4 +-- 11 files changed, 91 insertions(+), 33 deletions(-) diff --git a/novawallet/Common/Extension/Foundation/Predicate/NSPredicate+Transaction.swift b/novawallet/Common/Extension/Foundation/Predicate/NSPredicate+Transaction.swift index 4fdc503341..10ef76dd0f 100644 --- a/novawallet/Common/Extension/Foundation/Predicate/NSPredicate+Transaction.swift +++ b/novawallet/Common/Extension/Foundation/Predicate/NSPredicate+Transaction.swift @@ -60,11 +60,13 @@ extension NSPredicate { let receiverPredicate = filterTransactionsByReceiver(address: address) let chainPredicate = filterTransactionsByChainId(chainId) let assetPredicate = filterTransactionsByAssetId(assetId) + let swapPredicate = filterSwapTransactionsByAssetId(assetId) let orPredicates = [senderPredicate, receiverPredicate] + let assetsAndSwapsPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: [assetPredicate, swapPredicate]) let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ chainPredicate, - assetPredicate, + assetsAndSwapsPredicate, NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates) ]) @@ -109,6 +111,7 @@ extension NSPredicate { let receiverPredicate = filterTransactionsByReceiver(address: address) let chainPredicate = filterTransactionsByChainId(chainId) let assetPredicate = filterTransactionsByAssetId(utilityAssetId) + let swapPredicate = filterSwapTransactionsByAssetId(utilityAssetId) let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ chainPredicate, @@ -118,6 +121,10 @@ extension NSPredicate { NSCompoundPredicate(andPredicateWithSubpredicates: [ assetPredicate, receiverPredicate + ]), + NSCompoundPredicate(andPredicateWithSubpredicates: [ + swapPredicate, + receiverPredicate ]) ] ) @@ -159,6 +166,20 @@ extension NSPredicate { NSPredicate(format: "%K == %d", #keyPath(CDTransactionItem.source), source.rawValue) } + static func filterSwapTransactionsByAssetId(_ assetId: UInt32) -> NSPredicate { + let assetIdIn = NSPredicate( + format: "%K == %d", #keyPath(CDTransactionItem.swap.assetIdIn), + Int32(bitPattern: assetId) + ) + + let assetIdOut = NSPredicate( + format: "%K == %d", #keyPath(CDTransactionItem.swap.assetIdOut), + Int32(bitPattern: assetId) + ) + + return NSCompoundPredicate(orPredicateWithSubpredicates: [assetIdIn, assetIdOut]) + } + static func filterTransactionsByType(_ type: WalletHistoryFilter) -> NSPredicate { var orPredicates: [NSPredicate] = [] @@ -174,6 +195,10 @@ extension NSPredicate { orPredicates.append(filterExtrinsicTransactions()) } + if type.contains(.swaps) { + orPredicates.append(filterSwapTransactions()) + } + return NSCompoundPredicate(orPredicateWithSubpredicates: orPredicates) } @@ -196,6 +221,10 @@ extension NSPredicate { return NSCompoundPredicate(orPredicateWithSubpredicates: predicates) } + static func filterSwapTransactions() -> NSPredicate { + NSPredicate(format: "%K != nil", #keyPath(CDTransactionItem.swap)) + } + static func filterRewardOrSlashTransactions() -> NSPredicate { let paths = [CallCodingPath.reward, CallCodingPath.slash] diff --git a/novawallet/Common/Extension/Storage/CDTransactionHistoryItem+CoreDataDecodable.swift b/novawallet/Common/Extension/Storage/CDTransactionHistoryItem+CoreDataDecodable.swift index 1a1d1da00d..3302a22983 100644 --- a/novawallet/Common/Extension/Storage/CDTransactionHistoryItem+CoreDataDecodable.swift +++ b/novawallet/Common/Extension/Storage/CDTransactionHistoryItem+CoreDataDecodable.swift @@ -58,8 +58,12 @@ extension CDTransactionItem: CoreDataCodable { } swap?.amountIn = try swapContainer.decode(String.self, forKey: .amountIn) swap?.amountOut = try swapContainer.decode(String.self, forKey: .amountOut) - swap?.assetIdIn = try swapContainer.decodeIfPresent(String.self, forKey: .assetIdIn) - swap?.assetIdOut = try swapContainer.decodeIfPresent(String.self, forKey: .assetIdOut) + + let assetIdIn = try swapContainer.decodeIfPresent(UInt32.self, forKey: .assetIdIn) + swap?.assetIdIn = assetIdIn.map { NSNumber(value: Int32(bitPattern: $0)) } + + let assetIdOut = try swapContainer.decodeIfPresent(UInt32.self, forKey: .assetIdOut) + swap?.assetIdOut = assetIdOut.map { NSNumber(value: Int32(bitPattern: $0)) } } } @@ -94,8 +98,15 @@ extension CDTransactionItem: CoreDataCodable { var nestedSwap = container.nestedContainer(keyedBy: SwapHistoryData.CodingKeys.self, forKey: .swap) try nestedSwap.encode(swap.amountIn, forKey: .amountIn) try nestedSwap.encode(swap.amountOut, forKey: .amountOut) - try nestedSwap.encodeIfPresent(swap.assetIdIn, forKey: .assetIdIn) - try nestedSwap.encodeIfPresent(swap.assetIdOut, forKey: .assetIdOut) + try nestedSwap.encodeIfPresent( + swap.assetIdIn.map { UInt32(bitPattern: $0.int32Value) }, + forKey: .assetIdIn + ) + + try nestedSwap.encodeIfPresent( + swap.assetIdOut.map { UInt32(bitPattern: $0.int32Value) }, + forKey: .assetIdOut + ) } } } diff --git a/novawallet/Common/Helpers/TransactionHistory/PriceHistoryCalculatorFactory.swift b/novawallet/Common/Helpers/TransactionHistory/PriceHistoryCalculatorFactory.swift index 9685919840..3dbbb60b14 100644 --- a/novawallet/Common/Helpers/TransactionHistory/PriceHistoryCalculatorFactory.swift +++ b/novawallet/Common/Helpers/TransactionHistory/PriceHistoryCalculatorFactory.swift @@ -4,8 +4,8 @@ protocol PriceHistoryCalculatorFactoryProtocol { } final class PriceHistoryCalculatorFactory: PriceHistoryCalculatorFactoryProtocol { - let priceHistory: [AssetModel.PriceId: PriceHistory?] = [:] - + private var priceHistory: [AssetModel.PriceId: PriceHistory?] = [:] + func replace(history: PriceHistory, priceId: AssetModel.PriceId) { priceHistory[priceId] = history } diff --git a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift index f089f9588b..5bb9a02f5b 100644 --- a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift +++ b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift @@ -110,6 +110,22 @@ struct ChainModel: Equatable, Codable, Hashable { assets.first { $0.assetId == assetId } } + func assetOrNil(for assetId: AssetModel.Id?) -> AssetModel? { + guard let assetId = assetId else { + return nil + } + + return assets.first { $0.assetId == assetId } + } + + func assetOrNative(for assetId: AssetModel.Id?) -> AssetModel? { + guard let assetId = assetId else { + return utilityAsset() + } + + return assets.first { $0.assetId == assetId } + } + func hasEnabledAsset() -> Bool { assets.contains { $0.enabled } } diff --git a/novawallet/Common/Model/TransactionHistoryItem.swift b/novawallet/Common/Model/TransactionHistoryItem.swift index 4a115c1999..729e6ce580 100644 --- a/novawallet/Common/Model/TransactionHistoryItem.swift +++ b/novawallet/Common/Model/TransactionHistoryItem.swift @@ -97,7 +97,7 @@ struct SwapHistoryData: Codable { } let amountIn: String - let assetIdIn: String? + let assetIdIn: AssetModel.Id? let amountOut: String - let assetIdOut: String? + let assetIdOut: AssetModel.Id? } diff --git a/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift b/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift index 5cd83addb4..7a8f1c807f 100644 --- a/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift +++ b/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift @@ -117,15 +117,8 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { ) -> TransactionHistoryItem { let source = TransactionHistoryItemSource.substrate let remoteIdentifier = TransactionHistoryItem.createIdentifier(from: identifier, source: source) - let assetIdIn = chainAsset.chain.asset(byHistoryAssetId: swap.assetIdIn) ?? chainAsset.chain.utilityAsset() - let feeAsset: AssetModel? - - if swap.isFeeNative { - feeAsset = chainAsset.chain.utilityAsset() - } else { - feeAsset = chainAsset.chain.asset(byHistoryAssetId: swap.assetIdFee) - } + let feeAsset = mapFromSwapHistoryAssetId(swap.assetIdFee, chain: chainAsset.chain) return .init( identifier: remoteIdentifier, @@ -146,13 +139,21 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { call: nil, swap: .init( amountIn: swap.amountIn, - assetIdIn: swap.assetIdIn == SubqueryHistoryElement.nativeFeeAssetId ? nil : swap.assetIdIn, + assetIdIn: mapFromSwapHistoryAssetId(swap.assetIdIn, chain: chainAsset.chain)?.assetId, amountOut: swap.amountOut, - assetIdOut: swap.assetIdOut == SubqueryHistoryElement.nativeFeeAssetId ? nil : swap.assetIdOut + assetIdOut: mapFromSwapHistoryAssetId(swap.assetIdOut, chain: chainAsset.chain)?.assetId ) ) } + private func mapFromSwapHistoryAssetId(_ assetId: String, chain: ChainModel) -> AssetModel? { + if assetId == SubqueryHistoryElement.nativeFeeAssetId { + return chain.utilityAsset() + } else { + return chain.asset(byHistoryAssetId: assetId) + } + } + private func createTransactionFromReward( _ reward: SubqueryRewardOrSlash, chainAssetId: ChainAssetId, diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel21.xcdatamodel/contents b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel21.xcdatamodel/contents index c53f8a08b5..d042fae025 100644 --- a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel21.xcdatamodel/contents +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel21.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -195,8 +195,8 @@ - - + + \ No newline at end of file diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift index ee4a86b531..5998fb5b76 100644 --- a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsSwapProvider.swift @@ -27,9 +27,9 @@ extension OperationDetailsSwapProvider: OperationDetailsDataProviderProtocol { ) { guard let swap = transaction.swap, - let assetIn = chain.asset(byHistoryAssetId: swap.assetIdIn) ?? chain.utilityAsset(), - let assetOut = chain.asset(byHistoryAssetId: swap.assetIdOut) ?? chain.utilityAsset(), - let feeAsset = transaction.feeAssetId.flatMap({ chain.asset(for: $0) }) ?? chain.utilityAsset(), + let assetIn = chain.assetOrNil(for: swap.assetIdIn), + let assetOut = chain.assetOrNil(for: swap.assetIdOut), + let feeAsset = chain.assetOrNative(for: transaction.feeAssetId), let wallet = WalletDisplayAddress(response: selectedAccount) else { progressClosure(nil) return diff --git a/novawallet/Modules/OperationDetails/OperationDetailsBaseInteractor.swift b/novawallet/Modules/OperationDetails/OperationDetailsBaseInteractor.swift index 5e960fb05f..61947b8ed9 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsBaseInteractor.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsBaseInteractor.swift @@ -141,8 +141,10 @@ extension OperationDetailsBaseInteractor: PriceLocalStorageSubscriber, PriceLoca ) { switch result { case let .success(history): - calculatorFactory.replace(history: history, priceId: priceId) - provideModel(overridingBy: nil, newFee: nil) + if let history = history { + calculatorFactory.replace(history: history, priceId: priceId) + provideModel(overridingBy: nil, newFee: nil) + } case let .failure(error): presenter?.didReceiveDetails(result: .failure(error)) } diff --git a/novawallet/Modules/OperationDetails/OperationSwapDetailsInteractor.swift b/novawallet/Modules/OperationDetails/OperationSwapDetailsInteractor.swift index 707b2178ac..a19159377e 100644 --- a/novawallet/Modules/OperationDetails/OperationSwapDetailsInteractor.swift +++ b/novawallet/Modules/OperationDetails/OperationSwapDetailsInteractor.swift @@ -7,10 +7,9 @@ final class OperationSwapDetailsInteractor: OperationDetailsBaseInteractor { guard let swap = transaction.swap else { return } - let priceAssetIn = chain.asset(byHistoryAssetId: swap.assetIdIn)?.priceId - let priceAssetOut = chain.asset(byHistoryAssetId: swap.assetIdOut)?.priceId - let feeAsset = transaction.feeAssetId.flatMap { chain.asset(for: $0) } ?? chain.utilityAsset() - let feePriceId = feeAsset?.priceId + let priceAssetIn = chain.assetOrNil(for: swap.assetIdIn)?.priceId + let priceAssetOut = chain.assetOrNil(for: swap.assetIdOut)?.priceId + let feePriceId = chain.assetOrNative(for: transaction.feeAssetId)?.priceId let prices = [ priceAssetIn, priceAssetOut, diff --git a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift index 89f9b8c105..a4c4ddbaf8 100644 --- a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift +++ b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift @@ -103,8 +103,8 @@ final class TransactionHistoryViewModelFactory { locale: Locale, txType: TransactionType ) -> TransactionItemViewModel { - let assetIn = chainAsset.chain.asset(byHistoryAssetId: data.swap?.assetIdIn) ?? chainAsset.chain.utilityAsset() - let assetOut = chainAsset.chain.asset(byHistoryAssetId: data.swap?.assetIdOut) ?? chainAsset.chain.utilityAsset() + let assetIn = chainAsset.chain.assetOrNil(for: data.swap?.assetIdIn) + let assetOut = chainAsset.chain.assetOrNil(for: data.swap?.assetIdOut) let isOutgoing = assetIn?.assetId == chainAsset.asset.assetId let optAmountInPlank = isOutgoing ? data.swap?.amountIn : data.swap?.amountOut let amountInPlank = optAmountInPlank.map { BigUInt($0) ?? 0 } ?? 0 From 68f5d3ccf19f9ab18be8ce72ccb10e325ba90bf8 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 14 Nov 2023 06:50:54 +0100 Subject: [PATCH 167/204] fix details view --- .../View/OperationDetailsSwapView.swift | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/novawallet/Modules/OperationDetails/View/OperationDetailsSwapView.swift b/novawallet/Modules/OperationDetails/View/OperationDetailsSwapView.swift index aa268cd9e1..1e971e6964 100644 --- a/novawallet/Modules/OperationDetails/View/OperationDetailsSwapView.swift +++ b/novawallet/Modules/OperationDetails/View/OperationDetailsSwapView.swift @@ -69,20 +69,14 @@ final class OperationDetailsSwapView: LocalizableView { )) transactionHashView.bind(details: viewModel.transactionHash) - switch viewModel.direction { - case .sell: - pairsView.leftAssetView.valueLabel.textColor = R.color.colorTextPositive() - pairsView.rigthAssetView.valueLabel.textColor = R.color.colorTextPrimary() - case .buy: - pairsView.leftAssetView.valueLabel.textColor = R.color.colorTextPrimary() - pairsView.rigthAssetView.valueLabel.textColor = R.color.colorTextPositive() - } + pairsView.leftAssetView.valueLabel.textColor = R.color.colorTextPrimary() + pairsView.rigthAssetView.valueLabel.textColor = R.color.colorTextPositive() } private func setup(locale: Locale) { rateCell.titleButton.imageWithTitleView?.title = R.string.localizable.swapsSetupDetailsRate( preferredLanguages: locale.rLanguages) - networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetwork( + networkFeeCell.titleButton.imageWithTitleView?.title = R.string.localizable.commonNetworkFee( preferredLanguages: locale.rLanguages) rateCell.titleButton.invalidateLayout() networkFeeCell.titleButton.invalidateLayout() From ff8146e3ed427ff066d81376a731a414a94d6b9b Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 14 Nov 2023 09:47:42 +0300 Subject: [PATCH 168/204] missing change --- 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 1eb98a88b4..a86362d400 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1425,4 +1425,4 @@ "swaps.not.enough.tokens" = "Недостаточно токенов для обмена"; "swaps.not.enough.liquidity" = "Недостаточно ликвидности"; "swaps.pay.asset.fee.ed.message" = "Для оплаты комиссии сети %@ токеном, Nova автоматически поменяет %@ в %@ для сохранения минимального %@ баланса аккаунта."; -"swaps.setup.deposit.button.title" = "Получить %@"; +"swaps.setup.deposit.button.title" = "Пополнить %@"; From 729ea40658c4311fbcab74e2ebc578fce4d29aea Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 14 Nov 2023 10:59:14 +0300 Subject: [PATCH 169/204] PR fixes --- novawallet.xcodeproj/project.pbxproj | 4 ++ .../AssetDetailsViewFactory.swift | 8 +--- .../AssetDetails/AssetDetailsWireframe.swift | 15 +++---- .../AssetDetailsContainerProtocols.swift | 3 +- .../AssetDetailsContainerViewFactory.swift | 9 ++-- .../Model/AssetOperationState.swift | 12 +++++ .../AssetList/AssetListWireframe.swift | 15 +++++-- .../OperationDetailsPresenter.swift | 44 +++++++++---------- .../OperationDetailsProtocols.swift | 3 +- .../OperationDetailsViewController.swift | 12 ++--- .../OperationDetailsViewFactory.swift | 6 +-- .../OperationDetailsWireframe.swift | 13 +++--- .../Swaps/Confirm/SwapConfirmPresenter.swift | 3 +- .../TransactionHistoryViewFactory.swift | 6 +-- .../TransactionHistoryWireframe.swift | 12 ++--- 15 files changed, 79 insertions(+), 86 deletions(-) create mode 100644 novawallet/Modules/AssetDetails/Model/AssetOperationState.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 5081faebdc..e076ae8e76 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -810,6 +810,7 @@ 77A0B2F32A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A0B2F22A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift */; }; 77A0B2F52A3CA39D00CBF653 /* StakingMoreOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A0B2F42A3CA39D00CBF653 /* StakingMoreOptionCollectionViewCell.swift */; }; 77A0B2F92A3CA40E00CBF653 /* StakingMoreOptionsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A0B2F82A3CA40E00CBF653 /* StakingMoreOptionsSection.swift */; }; + 77A4F4012B035027006294BC /* AssetOperationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A4F4002B035027006294BC /* AssetOperationState.swift */; }; 77A6F5AB2A2E0B31004AFD1A /* ReceiveAssetOperationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5AA2A2E0B31004AFD1A /* ReceiveAssetOperationPresenter.swift */; }; 77A6F5AE2A2E0C7C004AFD1A /* ReceiveAssetOperationWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5AD2A2E0C7C004AFD1A /* ReceiveAssetOperationWireframe.swift */; }; 77A6F5B92A2E2AAD004AFD1A /* BuyAssetOperationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5B72A2E2AAD004AFD1A /* BuyAssetOperationPresenter.swift */; }; @@ -4902,6 +4903,7 @@ 77A0B2F22A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionsViewModelFactory.swift; sourceTree = ""; }; 77A0B2F42A3CA39D00CBF653 /* StakingMoreOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionCollectionViewCell.swift; sourceTree = ""; }; 77A0B2F82A3CA40E00CBF653 /* StakingMoreOptionsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionsSection.swift; sourceTree = ""; }; + 77A4F4002B035027006294BC /* AssetOperationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetOperationState.swift; sourceTree = ""; }; 77A6F5AA2A2E0B31004AFD1A /* ReceiveAssetOperationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveAssetOperationPresenter.swift; sourceTree = ""; }; 77A6F5AD2A2E0C7C004AFD1A /* ReceiveAssetOperationWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveAssetOperationWireframe.swift; sourceTree = ""; }; 77A6F5B72A2E2AAD004AFD1A /* BuyAssetOperationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuyAssetOperationPresenter.swift; sourceTree = ""; }; @@ -17377,6 +17379,7 @@ 88D02FED2942F00D00E26390 /* AssetDetailsOperation.swift */, 88D02FF129431FC900E26390 /* AssetDetailsLocksViewModel.swift */, 846AF8452525C93A00868F37 /* BalanceContext.swift */, + 77A4F4002B035027006294BC /* AssetOperationState.swift */, ); path = Model; sourceTree = ""; @@ -22279,6 +22282,7 @@ 846DA5592A2098BE006CD6C1 /* StakingResolvedAccountMapper.swift in Sources */, D83B47B07C0D40A327AC44F7 /* CustomValidatorListViewFactory.swift in Sources */, 8881043829EBC6BD000FA9BC /* EquilibriumTokenTransfer.swift in Sources */, + 77A4F4012B035027006294BC /* AssetOperationState.swift in Sources */, 8487583427F06AF300495306 /* QRScannerPresenter.swift in Sources */, 0E6C2939AFB3D125C760D5A0 /* CrowdloanContributionSetupProtocols.swift in Sources */, 8410562C27AF1C15004F5CA3 /* Ethereum+Checksum.swift in Sources */, diff --git a/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift b/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift index d3eee67a0b..7d58826751 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift @@ -3,10 +3,9 @@ import SoraFoundation struct AssetDetailsViewFactory { static func createView( - assetListObservable: AssetListModelObservable, chain: ChainModel, asset: AssetModel, - swapCompletionClosure: SwapCompletionClosure? + operationState: AssetOperationState ) -> AssetDetailsViewProtocol? { guard let currencyManager = CurrencyManager.shared else { return nil @@ -34,10 +33,7 @@ struct AssetDetailsViewFactory { currencyManager: currencyManager ) - let wireframe = AssetDetailsWireframe( - assetListObservable: assetListObservable, - swapCompletionClosure: swapCompletionClosure - ) + let wireframe = AssetDetailsWireframe(operationState: operationState) let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) let viewModelFactory = AssetDetailsViewModelFactory( diff --git a/novawallet/Modules/AssetDetails/AssetDetailsWireframe.swift b/novawallet/Modules/AssetDetails/AssetDetailsWireframe.swift index fb3113034d..3f9fe138a4 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsWireframe.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsWireframe.swift @@ -4,15 +4,10 @@ import SoraUI import SoraFoundation final class AssetDetailsWireframe: AssetDetailsWireframeProtocol { - let assetListObservable: AssetListModelObservable - let swapCompletionClosure: SwapCompletionClosure? + let operationState: AssetOperationState - init( - assetListObservable: AssetListModelObservable, - swapCompletionClosure: SwapCompletionClosure? - ) { - self.assetListObservable = assetListObservable - self.swapCompletionClosure = swapCompletionClosure + init(operationState: AssetOperationState) { + self.operationState = operationState } func showPurchaseTokens( @@ -106,9 +101,9 @@ final class AssetDetailsWireframe: AssetDetailsWireframeProtocol { func showSwaps(from view: AssetDetailsViewProtocol?, chainAsset: ChainAsset) { guard let swapsView = SwapSetupViewFactory.createView( - assetListObservable: assetListObservable, + assetListObservable: operationState.assetListObservable, payChainAsset: chainAsset, - swapCompletionClosure: swapCompletionClosure + swapCompletionClosure: operationState.swapCompletionClosure ) else { return } diff --git a/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerProtocols.swift b/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerProtocols.swift index a82d330c71..d9190de52b 100644 --- a/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerProtocols.swift +++ b/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerProtocols.swift @@ -1,9 +1,8 @@ protocol AssetDetailsContainerViewFactoryProtocol { static func createView( - assetListObservable: AssetListModelObservable, chain: ChainModel, asset: AssetModel, - swapCompletionClosure: SwapCompletionClosure? + operationState: AssetOperationState ) -> AssetDetailsContainerViewProtocol? } diff --git a/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift b/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift index 14ea919963..e31993bc87 100644 --- a/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift +++ b/novawallet/Modules/AssetDetails/Container/AssetDetailsContainerViewFactory.swift @@ -2,22 +2,19 @@ import SoraFoundation final class AssetDetailsContainerViewFactory: AssetDetailsContainerViewFactoryProtocol { static func createView( - assetListObservable: AssetListModelObservable, chain: ChainModel, asset: AssetModel, - swapCompletionClosure: SwapCompletionClosure? + operationState: AssetOperationState ) -> AssetDetailsContainerViewProtocol? { guard let accountView = AssetDetailsViewFactory.createView( - assetListObservable: assetListObservable, chain: chain, asset: asset, - swapCompletionClosure: swapCompletionClosure + operationState: operationState ), let historyView = TransactionHistoryViewFactory.createView( chainAsset: .init(chain: chain, asset: asset), - assetListObservable: assetListObservable, - swapCompletionClosure: swapCompletionClosure + operationState: operationState ) else { return nil } diff --git a/novawallet/Modules/AssetDetails/Model/AssetOperationState.swift b/novawallet/Modules/AssetDetails/Model/AssetOperationState.swift new file mode 100644 index 0000000000..5e2574c787 --- /dev/null +++ b/novawallet/Modules/AssetDetails/Model/AssetOperationState.swift @@ -0,0 +1,12 @@ +final class AssetOperationState { + let assetListObservable: AssetListModelObservable + let swapCompletionClosure: SwapCompletionClosure? + + init( + assetListObservable: AssetListModelObservable, + swapCompletionClosure: SwapCompletionClosure? + ) { + self.assetListObservable = assetListObservable + self.swapCompletionClosure = swapCompletionClosure + } +} diff --git a/novawallet/Modules/AssetList/AssetListWireframe.swift b/novawallet/Modules/AssetList/AssetListWireframe.swift index 278666873a..8f995d71af 100644 --- a/novawallet/Modules/AssetList/AssetListWireframe.swift +++ b/novawallet/Modules/AssetList/AssetListWireframe.swift @@ -19,11 +19,15 @@ final class AssetListWireframe: AssetListWireframeProtocol { self?.showAssetDetails(from: view, chain: chainAsset.chain, asset: chainAsset.asset) } - guard let assetDetailsView = AssetDetailsContainerViewFactory.createView( + let operationState = AssetOperationState( assetListObservable: assetListModelObservable, + swapCompletionClosure: swapCompletionClosure + ) + + guard let assetDetailsView = AssetDetailsContainerViewFactory.createView( chain: chain, asset: asset, - swapCompletionClosure: swapCompletionClosure + operationState: operationState ), let navigationController = view?.controller.navigationController else { return @@ -38,11 +42,14 @@ final class AssetListWireframe: AssetListWireframeProtocol { let swapCompletionClosure: (ChainAsset) -> Void = { [weak self] chainAsset in self?.showAssetDetails(from: view, chain: chainAsset.chain, asset: chainAsset.asset) } + let operationState = AssetOperationState( + assetListObservable: assetListModelObservable, + swapCompletionClosure: swapCompletionClosure + ) guard let history = TransactionHistoryViewFactory.createView( chainAsset: .init(chain: chain, asset: asset), - assetListObservable: assetListModelObservable, - swapCompletionClosure: swapCompletionClosure + operationState: operationState ) else { return } diff --git a/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift b/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift index 85a274c847..d0a13d7e8c 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift @@ -144,8 +144,9 @@ extension OperationDetailsPresenter: OperationDetailsPresenterProtocol { } } - func send() { - if case let .transfer(transferModel) = model?.operation { + func repeatOperation() { + switch model?.operation { + case let .transfer(transferModel): let peer = transferModel.outgoing ? transferModel.receiver : transferModel.sender wireframe.showSend( @@ -153,6 +154,24 @@ extension OperationDetailsPresenter: OperationDetailsPresenterProtocol { displayAddress: peer, chainAsset: chainAsset ) + case let .swap(swapModel): + let payChainAsset = ChainAsset(chain: swapModel.chain, asset: swapModel.assetIn) + let receiveChainAsset = ChainAsset(chain: swapModel.chain, asset: swapModel.assetOut) + let feeChainAsset = ChainAsset(chain: swapModel.chain, asset: swapModel.feeAsset) + let amount = swapModel.direction == .sell ? + swapModel.amountIn.decimal(precision: payChainAsset.asset.precision) : + swapModel.amountOut.decimal(precision: receiveChainAsset.asset.precision) + let swapSetupInitState = SwapSetupInitState( + payChainAsset: payChainAsset, + receiveChainAsset: receiveChainAsset, + feeChainAsset: feeChainAsset, + amount: amount, + direction: swapModel.direction + ) + + wireframe.showSwapSetup(from: view, state: swapSetupInitState) + default: + break } } @@ -163,27 +182,6 @@ extension OperationDetailsPresenter: OperationDetailsPresenterProtocol { func showNetworkFeeInfo() { wireframe.showFeeInfo(from: view) } - - func repeatOperation() { - guard case let .swap(swapModel) = model?.operation else { - return - } - let payChainAsset = ChainAsset(chain: swapModel.chain, asset: swapModel.assetIn) - let receiveChainAsset = ChainAsset(chain: swapModel.chain, asset: swapModel.assetOut) - let feeChainAsset = ChainAsset(chain: swapModel.chain, asset: swapModel.feeAsset) - let amount = swapModel.direction == .sell ? - swapModel.amountIn.decimal(precision: payChainAsset.asset.precision) : - swapModel.amountOut.decimal(precision: receiveChainAsset.asset.precision) - let swapSetupInitState = SwapSetupInitState( - payChainAsset: payChainAsset, - receiveChainAsset: receiveChainAsset, - feeChainAsset: feeChainAsset, - amount: amount, - direction: swapModel.direction - ) - - wireframe.showSwapSetup(from: view, state: swapSetupInitState) - } } extension OperationDetailsPresenter: OperationDetailsInteractorOutputProtocol { diff --git a/novawallet/Modules/OperationDetails/OperationDetailsProtocols.swift b/novawallet/Modules/OperationDetails/OperationDetailsProtocols.swift index a4586b6a2d..c7bf89b54b 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsProtocols.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsProtocols.swift @@ -8,10 +8,9 @@ protocol OperationDetailsPresenterProtocol: AnyObject { func showSenderActions() func showRecepientActions() func showOperationActions() - func send() + func repeatOperation() func showRateInfo() func showNetworkFeeInfo() - func repeatOperation() } protocol OperationDetailsInteractorInputProtocol: AnyObject { diff --git a/novawallet/Modules/OperationDetails/OperationDetailsViewController.swift b/novawallet/Modules/OperationDetails/OperationDetailsViewController.swift index 4994453a09..3cf1854c56 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsViewController.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsViewController.swift @@ -127,7 +127,7 @@ final class OperationDetailsViewController: UIViewController, ViewHolder { sendButton.addTarget( self, - action: #selector(actionSend), + action: #selector(actionRepeatOperation), for: .touchUpInside ) } @@ -297,7 +297,7 @@ final class OperationDetailsViewController: UIViewController, ViewHolder { ) repeatOperationButton.addTarget( self, - action: #selector(actionRepeatSwapOperation), + action: #selector(actionRepeatOperation), for: .touchUpInside ) } @@ -314,8 +314,8 @@ final class OperationDetailsViewController: UIViewController, ViewHolder { presenter.showRecepientActions() } - @objc func actionSend() { - presenter.send() + @objc func actionRepeatOperation() { + presenter.repeatOperation() } @objc func actionRate() { @@ -325,10 +325,6 @@ final class OperationDetailsViewController: UIViewController, ViewHolder { @objc func actionNetworkFee() { presenter.showNetworkFeeInfo() } - - @objc func actionRepeatSwapOperation() { - presenter.repeatOperation() - } } extension OperationDetailsViewController: OperationDetailsViewProtocol { diff --git a/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift b/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift index 8b80e3c009..0f0e0927cc 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift @@ -7,8 +7,7 @@ struct OperationDetailsViewFactory { static func createView( for transaction: TransactionHistoryItem, chainAsset: ChainAsset, - assetListObservable: AssetListModelObservable, - swapCompletionClosure: SwapCompletionClosure? + operationState: AssetOperationState ) -> OperationDetailsViewProtocol? { guard let currencyManager = CurrencyManager.shared, @@ -62,8 +61,7 @@ struct OperationDetailsViewFactory { } let wireframe = OperationDetailsWireframe( - assetListObservable: assetListObservable, - swapCompletionClosure: swapCompletionClosure + operationState: operationState ) let localizationManager = LocalizationManager.shared diff --git a/novawallet/Modules/OperationDetails/OperationDetailsWireframe.swift b/novawallet/Modules/OperationDetails/OperationDetailsWireframe.swift index 592988e9c7..904c444e74 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsWireframe.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsWireframe.swift @@ -1,15 +1,12 @@ import Foundation final class OperationDetailsWireframe: OperationDetailsWireframeProtocol { - let assetListObservable: AssetListModelObservable - let swapCompletionClosure: SwapCompletionClosure? + let operationState: AssetOperationState init( - assetListObservable: AssetListModelObservable, - swapCompletionClosure: SwapCompletionClosure? + operationState: AssetOperationState ) { - self.assetListObservable = assetListObservable - self.swapCompletionClosure = swapCompletionClosure + self.operationState = operationState } func showSend( @@ -32,9 +29,9 @@ final class OperationDetailsWireframe: OperationDetailsWireframeProtocol { state: SwapSetupInitState ) { guard let swapView = SwapSetupViewFactory.createView( - assetListObservable: assetListObservable, + assetListObservable: operationState.assetListObservable, initState: state, - swapCompletionClosure: swapCompletionClosure + swapCompletionClosure: operationState.swapCompletionClosure ) else { return } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index c3322e3d22..9705e0e96d 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -425,10 +425,11 @@ extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol { } func didReceiveConfirmation(hash _: String) { + view?.didReceiveStopLoading() + guard let payChainAsset = getPayChainAsset() else { return } - view?.didReceiveStopLoading() wireframe.complete( on: view, payChainAsset: payChainAsset, diff --git a/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift b/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift index 7fff637f86..24202b4bfc 100644 --- a/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift +++ b/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift @@ -6,8 +6,7 @@ import RobinHood struct TransactionHistoryViewFactory { static func createView( chainAsset: ChainAsset, - assetListObservable: AssetListModelObservable, - swapCompletionClosure: SwapCompletionClosure? + operationState: AssetOperationState ) -> TransactionHistoryViewProtocol? { guard let selectedMetaAccount = SelectedWalletSettings.shared.value, @@ -25,8 +24,7 @@ struct TransactionHistoryViewFactory { let wireframe = TransactionHistoryWireframe( chainAsset: chainAsset, - assetListObservable: assetListObservable, - swapCompletionClosure: swapCompletionClosure + operationState: operationState ) let balanceViewModelFactory = BalanceViewModelFactory( diff --git a/novawallet/Modules/TransactionHistory/TransactionHistoryWireframe.swift b/novawallet/Modules/TransactionHistory/TransactionHistoryWireframe.swift index b6e24e9b45..d5b42bdb65 100644 --- a/novawallet/Modules/TransactionHistory/TransactionHistoryWireframe.swift +++ b/novawallet/Modules/TransactionHistory/TransactionHistoryWireframe.swift @@ -2,17 +2,14 @@ import UIKit final class TransactionHistoryWireframe: TransactionHistoryWireframeProtocol { let chainAsset: ChainAsset - let assetListObservable: AssetListModelObservable - let swapCompletionClosure: SwapCompletionClosure? + let operationState: AssetOperationState init( chainAsset: ChainAsset, - assetListObservable: AssetListModelObservable, - swapCompletionClosure: SwapCompletionClosure? + operationState: AssetOperationState ) { self.chainAsset = chainAsset - self.assetListObservable = assetListObservable - self.swapCompletionClosure = swapCompletionClosure + self.operationState = operationState } func showFilter( @@ -37,8 +34,7 @@ final class TransactionHistoryWireframe: TransactionHistoryWireframeProtocol { guard let operationDetailsView = OperationDetailsViewFactory.createView( for: operation, chainAsset: chainAsset, - assetListObservable: assetListObservable, - swapCompletionClosure: swapCompletionClosure + operationState: operationState ) else { return } From 704dc8acf06c3ca5fa87c566ae4769c65cc3fc6d Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 14 Nov 2023 12:11:28 +0300 Subject: [PATCH 170/204] PR fix --- novawallet.xcodeproj/project.pbxproj | 4 ++++ .../Common/Extension/Error/Optional+Result.swift | 12 ++++++++++++ .../Modules/Swaps/Setup/SwapSetupPresenter.swift | 10 ++-------- .../Swaps/Setup/SwapSetupViewController.swift | 15 ++++----------- 4 files changed, 22 insertions(+), 19 deletions(-) create mode 100644 novawallet/Common/Extension/Error/Optional+Result.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 53d24b1213..83f1d38b6b 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -810,6 +810,7 @@ 77A0B2F32A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A0B2F22A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift */; }; 77A0B2F52A3CA39D00CBF653 /* StakingMoreOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A0B2F42A3CA39D00CBF653 /* StakingMoreOptionCollectionViewCell.swift */; }; 77A0B2F92A3CA40E00CBF653 /* StakingMoreOptionsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A0B2F82A3CA40E00CBF653 /* StakingMoreOptionsSection.swift */; }; + 77A4F4032B036615006294BC /* Optional+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A4F4022B036615006294BC /* Optional+Result.swift */; }; 77A6F5AB2A2E0B31004AFD1A /* ReceiveAssetOperationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5AA2A2E0B31004AFD1A /* ReceiveAssetOperationPresenter.swift */; }; 77A6F5AE2A2E0C7C004AFD1A /* ReceiveAssetOperationWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5AD2A2E0C7C004AFD1A /* ReceiveAssetOperationWireframe.swift */; }; 77A6F5B92A2E2AAD004AFD1A /* BuyAssetOperationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5B72A2E2AAD004AFD1A /* BuyAssetOperationPresenter.swift */; }; @@ -4894,6 +4895,7 @@ 77A0B2F22A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionsViewModelFactory.swift; sourceTree = ""; }; 77A0B2F42A3CA39D00CBF653 /* StakingMoreOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionCollectionViewCell.swift; sourceTree = ""; }; 77A0B2F82A3CA40E00CBF653 /* StakingMoreOptionsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionsSection.swift; sourceTree = ""; }; + 77A4F4022B036615006294BC /* Optional+Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Result.swift"; sourceTree = ""; }; 77A6F5AA2A2E0B31004AFD1A /* ReceiveAssetOperationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveAssetOperationPresenter.swift; sourceTree = ""; }; 77A6F5AD2A2E0C7C004AFD1A /* ReceiveAssetOperationWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveAssetOperationWireframe.swift; sourceTree = ""; }; 77A6F5B72A2E2AAD004AFD1A /* BuyAssetOperationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuyAssetOperationPresenter.swift; sourceTree = ""; }; @@ -13751,6 +13753,7 @@ isa = PBXGroup; children = ( 8490146C24A9487A008F705E /* ErrorPresentable+AlertText.swift */, + 77A4F4022B036615006294BC /* Optional+Result.swift */, ); path = Error; sourceTree = ""; @@ -22152,6 +22155,7 @@ 845B08042918C308005785D3 /* Gov1ActionOperationFactory.swift in Sources */, F0C3DB0CEE1975626B0014A8 /* StakingUnbondConfirmInteractor.swift in Sources */, 0C0CB3822AC545A800EAC516 /* AssetConversionExtrinsicService.swift in Sources */, + 77A4F4032B036615006294BC /* Optional+Result.swift in Sources */, 849FA21628A26CB500F83EAA /* CountdownTimerMediator.swift in Sources */, D3B48F82A875E301D749AC0B /* StakingUnbondConfirmViewController.swift in Sources */, 842AEB81292F34B600C61B0C /* RemoteChainExternalApi.swift in Sources */, diff --git a/novawallet/Common/Extension/Error/Optional+Result.swift b/novawallet/Common/Extension/Error/Optional+Result.swift new file mode 100644 index 0000000000..82bb3b12d8 --- /dev/null +++ b/novawallet/Common/Extension/Error/Optional+Result.swift @@ -0,0 +1,12 @@ +import Foundation + +extension Optional { + func hasError() -> Bool where Wrapped == Result { + switch self { + case .success, .none: + return false + case .failure: + return true + } + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index ef9a362211..994d9edafc 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -25,14 +25,9 @@ final class SwapSetupPresenter: SwapBasePresenter { private var feeIdentifier: SwapSetupFeeIdentifier? private var slippage: BigRational - private var issues: [SwapSetupViewIssue] = [] { - didSet { - provideDetailsViewModel() - } - } private var detailsAvailable: Bool { - !issues.contains(.noLiqudity) && quoteArgs != nil + !quoteResult.hasError() && quoteArgs != nil } init( @@ -185,7 +180,7 @@ final class SwapSetupPresenter: SwapBasePresenter { provideRateViewModel() provideButtonState() - + provideDetailsViewModel() estimateFee() } @@ -475,7 +470,6 @@ extension SwapSetupPresenter { private func provideIssues() { let issues = viewModelFactory.detectIssues(in: getIssueParams(), locale: selectedLocale) - self.issues = issues view?.didReceive(issues: issues) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 135c867b10..6539dddcaf 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -7,7 +7,6 @@ final class SwapSetupViewController: UIViewController, ViewHolder { let presenter: SwapSetupPresenterProtocol private var toggledDetailsManually: Bool = false - private var depositTokenSymbol: String = "" init( presenter: SwapSetupPresenterProtocol, @@ -100,7 +99,6 @@ final class SwapSetupViewController: UIViewController, ViewHolder { title = R.string.localizable.commonSwap(preferredLanguages: selectedLocale.rLanguages) rootView.setup(locale: selectedLocale) setupAccessoryView() - setupDepositTokenButton() } private func setupAccessoryView() { @@ -123,13 +121,6 @@ final class SwapSetupViewController: UIViewController, ViewHolder { ) } - private func setupDepositTokenButton() { - rootView.depositTokenButton.imageWithTitleView?.title = R.string.localizable.swapsSetupDepositButtonTitle( - depositTokenSymbol, - preferredLanguages: selectedLocale.rLanguages - ) - } - @objc private func selectPayTokenAction() { rootView.receiveAmountInputView.endEditing(true) presenter.selectPayToken() @@ -209,8 +200,10 @@ extension SwapSetupViewController: SwapSetupViewProtocol { switch viewModel { case let .asset(assetViewModel): rootView.payAmountInputView.bind(assetViewModel: assetViewModel) - depositTokenSymbol = assetViewModel.symbol - setupDepositTokenButton() + rootView.depositTokenButton.imageWithTitleView?.title = R.string.localizable.swapsSetupDepositButtonTitle( + assetViewModel.symbol, + preferredLanguages: selectedLocale.rLanguages + ) case let .empty(emptySwapsAssetViewModel): rootView.payAmountInputView.bind(emptyViewModel: emptySwapsAssetViewModel) rootView.depositTokenButton.imageWithTitleView?.title = nil From 8aae70f4a4aaa2cbc057fbc587b7e915962b1bd9 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 14 Nov 2023 11:57:12 +0100 Subject: [PATCH 171/204] fix localization and navigation --- novawallet/Modules/AssetList/AssetListWireframe.swift | 3 ++- novawallet/ru.lproj/Localizable.strings | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/novawallet/Modules/AssetList/AssetListWireframe.swift b/novawallet/Modules/AssetList/AssetListWireframe.swift index 8f995d71af..763c900f22 100644 --- a/novawallet/Modules/AssetList/AssetListWireframe.swift +++ b/novawallet/Modules/AssetList/AssetListWireframe.swift @@ -15,7 +15,8 @@ final class AssetListWireframe: AssetListWireframeProtocol { } func showAssetDetails(from view: AssetListViewProtocol?, chain: ChainModel, asset: AssetModel) { - let swapCompletionClosure: (ChainAsset) -> Void = { [weak self] chainAsset in + let swapCompletionClosure: (ChainAsset) -> Void = { [weak self, weak view] chainAsset in + view?.controller.navigationController?.popToRootViewController(animated: false) self?.showAssetDetails(from: view, chain: chainAsset.chain, asset: chainAsset.asset) } diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 85e3741bdd..0dc1cc26cc 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1409,8 +1409,8 @@ "swaps.setup.network.fee.token.title" = "Токен для оплаты комиссии сети"; "swaps.setup.network.fee.token.hint" = "Комиссия сети добавляется к введенной сумме."; "swaps.setup.price.difference.description" = "Разница в цене относится к разнице в цене между двумя различными активами. При совершении обмена в криптовалюте разница в цене обычно представляет собой разницу между ценой актива, на который вы меняете, и ценой актива, на который вы меняетесь."; -"swaps.setup.error.rate.was.updated.title" = "Обменный курс был обновлен"; -"swaps.setup.error.rate.was.updated.message" = "Было: %@.\nСтало:%@"; +"swaps.error.rate.was.updated.title" = "Обменный курс был обновлен"; +"swaps.error.rate.was.updated.message" = "Было: %@.\nСтало: %@"; "common.action.repeat.operation" = "Повторить операцию"; "swaps.setup.deposit.by.cross.chain.transfer.title" = "Перевод между сетями"; "swaps.setup.deposit.by.cross.chain.transfer.subtitle" = "Перевести %@ из другой сети"; From 9049f6d800777434393ff7e4949c09639a5c0372 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 14 Nov 2023 12:15:29 +0100 Subject: [PATCH 172/204] remove unused code --- .../iconSwapHistory.imageset/Contents.json | 3 --- .../AssetList/AssetListWireframe.swift | 25 ------------------- 2 files changed, 28 deletions(-) diff --git a/novawallet/Assets.xcassets/iconSwapHistory.imageset/Contents.json b/novawallet/Assets.xcassets/iconSwapHistory.imageset/Contents.json index c7f2c02f5f..b7858e5993 100644 --- a/novawallet/Assets.xcassets/iconSwapHistory.imageset/Contents.json +++ b/novawallet/Assets.xcassets/iconSwapHistory.imageset/Contents.json @@ -8,8 +8,5 @@ "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "original" } } diff --git a/novawallet/Modules/AssetList/AssetListWireframe.swift b/novawallet/Modules/AssetList/AssetListWireframe.swift index 763c900f22..97f31f2de2 100644 --- a/novawallet/Modules/AssetList/AssetListWireframe.swift +++ b/novawallet/Modules/AssetList/AssetListWireframe.swift @@ -39,31 +39,6 @@ final class AssetListWireframe: AssetListWireframeProtocol { ) } - func showHistory(from view: AssetListViewProtocol?, chain: ChainModel, asset: AssetModel) { - let swapCompletionClosure: (ChainAsset) -> Void = { [weak self] chainAsset in - self?.showAssetDetails(from: view, chain: chainAsset.chain, asset: chainAsset.asset) - } - let operationState = AssetOperationState( - assetListObservable: assetListModelObservable, - swapCompletionClosure: swapCompletionClosure - ) - - guard let history = TransactionHistoryViewFactory.createView( - chainAsset: .init(chain: chain, asset: asset), - operationState: operationState - ) else { - return - } - guard let navigationController = view?.controller.navigationController else { - return - } - - navigationController.pushViewController( - history.controller, - animated: true - ) - } - func showAssetsSettings(from view: AssetListViewProtocol?) { guard let assetsManageView = AssetsSettingsViewFactory.createView() else { return From 68588fb06c25c9235e4853d16619875aa213058a Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 14 Nov 2023 14:20:55 +0300 Subject: [PATCH 173/204] check swaps --- .../AssetList/Models/AssetListBuilderResult.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift b/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift index 537fff4e47..47f1aed902 100644 --- a/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift +++ b/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift @@ -50,7 +50,16 @@ struct AssetListBuilderResult { } func hasSwaps() -> Bool { - allChains.values.contains { $0.hasSwaps } + balanceResults.contains { + guard let chain = allChains[$0.key.chainId] else { + return false + } + if case .success = $0.value { + return chain.hasSwaps + } else { + return false + } + } } } From 834d3fffc2c1c9feca74ba3e89041a8d15a08b1b Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 14 Nov 2023 14:36:35 +0100 Subject: [PATCH 174/204] fix details display --- novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 1737a8e996..88d15583c5 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -153,6 +153,7 @@ final class SwapSetupPresenter: SwapBasePresenter { ) provideIssues() + provideDetailsViewModel() } override func handleNewQuote(_ quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs) { From 4b5830eabb2e71eb9947a5f9ad4335e63a71988f Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 14 Nov 2023 15:41:20 +0100 Subject: [PATCH 175/204] remove unused code --- .../Substrate/Types/CallCodingPath.swift | 31 ------------------- .../Model/SwapIssueViewModelFactory.swift | 9 ++++++ .../Swaps/Setup/SwapSetupProtocols.swift | 1 + .../Swaps/Setup/SwapSetupViewController.swift | 6 ++++ .../TransactionHistoryViewModelFactory.swift | 8 +++-- novawallet/en.lproj/Localizable.strings | 1 + novawallet/ru.lproj/Localizable.strings | 1 + 7 files changed, 23 insertions(+), 34 deletions(-) diff --git a/novawallet/Common/Substrate/Types/CallCodingPath.swift b/novawallet/Common/Substrate/Types/CallCodingPath.swift index 5a5e484069..d812aba86c 100644 --- a/novawallet/Common/Substrate/Types/CallCodingPath.swift +++ b/novawallet/Common/Substrate/Types/CallCodingPath.swift @@ -26,13 +26,6 @@ extension CallCodingPath { PalletAssets.possibleTransferCallPaths().contains(self) } - var isSwap: Bool { - [ - Self.swap(direction: .buy), - Self.swap(direction: .sell) - ].contains(self) - } - var isTokensTransfer: Bool { [ .tokensTransfer, @@ -139,27 +132,3 @@ extension CallCodingPath { [.slash, .reward, .poolReward, .poolSlash].contains(self) } } - -// MARK: Filter - -extension CallCodingPath { - func matches(filter: WalletHistoryFilter) -> Bool { - if !filter.contains(.transfers), isSubstrateOrEvmTransfer { - return false - } - - if !filter.contains(.rewardsAndSlashes), isAnyStakingRewardOrSlash { - return false - } - - if !filter.contains(.extrinsics), !isSubstrateOrEvmTransfer, !isAnyStakingRewardOrSlash { - return false - } - - if !filter.contains(.swaps), !isSwap { - return false - } - - return true - } -} diff --git a/novawallet/Modules/Swaps/Setup/Model/SwapIssueViewModelFactory.swift b/novawallet/Modules/Swaps/Setup/Model/SwapIssueViewModelFactory.swift index 62444277c9..371f75d4d8 100644 --- a/novawallet/Modules/Swaps/Setup/Model/SwapIssueViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Setup/Model/SwapIssueViewModelFactory.swift @@ -19,6 +19,14 @@ final class SwapIssueViewModelFactory { } } + func detectZeroReceiveAmount(in model: SwapIssueCheckParams) -> SwapSetupViewIssue? { + if let receiveAmount = model.receiveAmount, receiveAmount == 0 { + return .zeroReceiveAmount + } else { + return nil + } + } + func detectInsufficientBalance(in model: SwapIssueCheckParams) -> SwapSetupViewIssue? { if let payAmount = model.payAmount, let payChainAsset = model.payChainAsset, @@ -68,6 +76,7 @@ extension SwapIssueViewModelFactory: SwapIssueViewModelFactoryProtocol { func detectIssues(in model: SwapIssueCheckParams, locale: Locale) -> [SwapSetupViewIssue] { [ detectZeroBalance(in: model), + detectZeroReceiveAmount(in: model), detectInsufficientBalance(in: model), detectMinBalanceViolationOnReceive(in: model, locale: locale), detectNoLiquidity(in: model) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index 9f1499050a..b15568591d 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -103,4 +103,5 @@ enum SwapSetupViewIssue: Equatable { case insufficientBalance case minBalanceViolation(String) case noLiqudity + case zeroReceiveAmount } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 6539dddcaf..78fe7fc840 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -283,6 +283,12 @@ extension SwapSetupViewController: SwapSetupViewProtocol { switch issue { case .zeroBalance: rootView.changeDepositTokenButtonVisibility(hidden: false) + case .zeroReceiveAmount: + let message = R.string.localizable.commonPositiveAmount( + preferredLanguages: selectedLocale.rLanguages + ) + + rootView.displayReceiveIssue(with: message) case .insufficientBalance: rootView.changeDepositTokenButtonVisibility(hidden: false) diff --git a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift index a4c4ddbaf8..17a6046651 100644 --- a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift +++ b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift @@ -405,6 +405,10 @@ extension TransactionHistoryViewModelFactory: TransactionHistoryViewModelFactory extension TransactionHistoryItem { func type(for address: AccountAddress) -> TransactionType? { + if swap != nil { + return .swap + } + switch callPath { case .slash: return .slash @@ -415,9 +419,7 @@ extension TransactionHistoryItem { case .poolSlash: return .poolSlash default: - if callPath.isSwap { - return .swap - } else if callPath.isSubstrateOrEvmTransfer { + if callPath.isSubstrateOrEvmTransfer { return sender == address ? .outgoing : .incoming } else { return TransactionType.extrinsic diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 7c12fff925..ea454214c7 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1427,3 +1427,4 @@ "swaps.not.enough.liquidity" = "Not enough liquidity"; "swaps.pay.asset.fee.ed.message" = "To pay network fee with %@, Nova will automatically swap %@ for %@ to maintain your account's minimum %@ balance."; "swaps.setup.deposit.button.title" = "Get %@"; +"common.positive.amount" = "Amount must be positive"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 990a3f81be..e299d31dcb 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1427,3 +1427,4 @@ "swaps.not.enough.liquidity" = "Недостаточно ликвидности"; "swaps.pay.asset.fee.ed.message" = "Для оплаты комиссии сети %@ токеном, Nova автоматически поменяет %@ в %@ для сохранения минимального %@ баланса аккаунта."; "swaps.setup.deposit.button.title" = "Пополнить %@"; +"common.positive.amount" = "Значение должно быть положительным"; From 6429f63996be3c80aa364d64eed010be07ac10a0 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 14 Nov 2023 15:52:59 +0100 Subject: [PATCH 176/204] fix icons --- .../iconSwapHistory.pdf | Bin 2338 -> 2226 bytes .../iconSwapOnDetails.pdf | Bin 2381 -> 2269 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/novawallet/Assets.xcassets/iconSwapHistory.imageset/iconSwapHistory.pdf b/novawallet/Assets.xcassets/iconSwapHistory.imageset/iconSwapHistory.pdf index 6bf538326dceb9c1ef3264df1a1d37ac8f1eadfe..db39ff218e585db8ea5f61ba97672f1c60564d8d 100644 GIT binary patch literal 2226 zcmbVOO>Y}F5WVlO;Ke|4$ci(ZA%{Q^ps|~xXp6c^Z$S^Lym3_6+E!AEw7))YxbjMN z3k1j-u-8wVugCXDU0h$ic|m>YItnq2zyHyN@bYDN^{N}TH~w$O=lJ5sVS9V{5E8(p zui77W!|J|UTn&G3*2D6<*WvQ*`G4zu_g6=KY)^;}FE8e|v(H$4Rp34sD@0*^e~O%! z%`f})w$mOVg(DIhQ;HVu&oN1rOoR4JQV2cw#7Scb(wiu=dPAi!OIUZ(7ZD_D7$viC z2P|c&SsEs#MMmXZE0CmBkwP$2&L|W6!uX565r;w05inywvE_0MEfV*UG)DqRJzLbI zq4r!!sWdDSGlFX$8K~FN8l@ITV&r5%xaN#xh~rd?4*-nBN^H4uEE&0+XR*o^pk~Qn z)gcT(0;qa-1=&bhVhV&DMakb$;B(K!tvC0f>3=cc2JuhJk>N z`3eXmfNbzx)4OlDXmMw-*(54LZpsiKz(^I&bU9#;5#8h(6URv+uv&7xGDpQSbnS{X zscvqrDPrr;bz30Jt#nCgQWbg@SP`iWr-thX2w9s=rJ|SzLcpMM<`g9&I5lo;HBQ?) za9XorUE>rSz{)W}hJc(?_zR)#Z#aF*2Z6aK(K2M0qWVdfsA30akl?J;8wtm=rH%^h zlM`6?i4AehND3WK)MV9yEyJg(#&AdDreLckcftKMm4^`%j)&$=97E(%XW1b!NBHmld;r|{0z)SlA|&(P`Z zA=7f$j~{pIVITY^TprV1j@!eqMMb{`AHeI?4p_jE-=MS0(xLehUHq`QL94+hScZo> zm*Ypo(trBj13Al-A7%0Z_jAG1;Fqhz>TbM!9QtWDyzgQt-88=c4%j^$;DC=0e9+#) zc81`R#}H*48c!i;>Y}F5WVlO;Ke|4$ci)MkV7Ca(AZ8;v_)OHx1a}A-Z(03Z7XdGx4%AbNPRgj zkf3{zsJHX^W`?7)>&v&VsLx$T&b#sVKRV}Lzjm{$$LZb8w3vqWi|f~N;d~$1<8kNb zkbL&cuIO7N&(BSNSl^n%j-Pn`aai9?A6)=AH>vHk85R%S>~i>fwH)T}-nfgK(_hPN z_g80r@3tNF78|1T`@^fl>Es@7EsQVf?Ia6&oRPS#VBxg_rOAmCB=%xR5iVDDg{W80tmLe zPf5*IW<*}9Eo6d-fGKN?rP?ygt0V3`i9`>OdR8w%TIk>X>? zhImgo1L2Yqj6si)r6mB2qGn%hCAfes<2h>K0#H&E@y;M5KoL+;&LzPb2}d6s!A6eC z+{|$`2QWw~Io2fLqlh>HHA1Xh!Ki^j4!}f3C4&?T7~C5aS)`x}c%@XOBB^G96O=}M zNnGuL%@AM@Ie`?DG|xIm@v z8Uf`*2;-|GKo(~KOCUJJtl$zl+M{%eY6~iotmOd}Uj(cJLdRel37Pp2#Z)6nfYh=x zP=(OOW-oBCIRGeVqm%(6?WC}04OI%p1QkYWfha(TK%iRZCgNSQm%$?eM7waQZEtmt zl5Id(bL%aFZA$1vfSw9sw5~v@p$=q6vg00?Q#4Ot4B5sHM|Lgk=x3Kt%nDp$5Dr&SJ|mZXpar#L#o zMEjtivT-Wbf(9xkvf0DYMiU{!eemLN;RV9`BZ%3h&?`k;2~W|8DN3B`6v0qv^CUzi zR=Sc%F1TZmofzAVn~YMmAw#8nbB}op3B|0kc7RR9j8SfuI7{J%l^D=Uh^Tl)Jhnv4bC#%z-in!cS zIeQMa*kcU$(6m{s?uSih|6;zs1RXE7xDq_OS^Ya8`ubmE Rv6*T*SO%#(JG=V%?PpdK-)_5E1_H< z94T6#?4F%Db7m(O*H>>|$XJF!oOkPAe;b^8`O>|5HB75p`#R`jeD?jcy4!tl0r19l zZFie#{xHn0raza9>E@f)?(*&9Z;S2l#~`EcALoyY7pK*uzoq@AG=@~7s#`pqPoB1& z7Td*Y5c?)EG$&(@nk75VoIpR;QbX}h{1~;dfe17fFTu4jHy@J+krZRBZZWhm)+#X> z7PXY@?uR-SZB0wZT9oMOC_aT&L5jf9Kwu0-h$1HL6S%;<3~yTtTv zrKaknjiOC8I4~g%?N}p7a`R*GvHIc^?$}B$qyi&L7y;SjbL+XFXcKXYLSA#-i`8SAT6nZ1 zg+gP1wt^SYN+?gTHV9>`(ulWXq)6Wd+(eOaC#P{FEkWL(oDJB{IBQPLDiN9)t6^GD zX!vnpW~>w`evp8gMoAcgXx2}aFb<3f>nLE#)|N(SHXtCLBks$duIiuu7nf^1;^ssS2XQf#iiz8-c@f;i#juLF1$)|Q6Bqk%npdFk z9&{Nq*6dwN=!l2?T^coq8vA>(2bGR0W7ZnwA7~vz(~SBYhcpK@v3O5#wXK4P)y@Qz zoah&>Z(|}|-OH+EX`GZoiwdub?d6@p2Vj*Ruf2Cu65a*FTCG+-BC1jX0ppkcQuB=V zIC(buYj;2qhm=s37UrUVvFo`{Y!V&D%yA~!_}R}YL4nh#qVDGrHv4L|TJN^*-7kzZ zKC_?y`h9Y<>-pVu0DhQmm-E-_Pwt)Bs(;Rlc!JKWBh$^aU4PsxrmeG~d2^tMGR2Fx*1I`n9+8g!bsv+tI-gz7AV8+W|ta{ZCB@tp{I}(VIJD&v`dQh@KcDs z{sOA!gS*Xqc|UCi{sEbNdxad&w(E!KxqG&}eZCiEcC%jZoF&`^p1ocEJrRBXM`XU) P^>mckI9yzO{o|X@lGWD5 literal 2381 zcmbW3Pj4GH5XJBNDR?oE972)9|3F}%v7Ml3i@I`eK@Y0Daa`C^D=CHBPoLj#Wm$%M z2)YM*^~4#@ym^n*$>sU`H{O`Yuh2k`1YZ;LP&=De|!F@UmU52Ctud49$3(qa;a& zm;>l+EmaENIvb@$$y6XQHxbipZZ0OR$)^}nHp`*eSgVgoF&5Wk?uKfMv?irvEl4oc zdY3}0AjQ{~Dv0wT6i*c~h3p!+z?{!fv7Fek^<0YiY$RG2JP)AQl)Wp?_-a$7%Fsb4 zF={kYPnfL_SuV53Hh41?8v8lHx^XmoAuIs@CMA& zhIFh^B&?t#7pp5q$Q@fQY04OzW&|vRl-hfN(MAUXLrzkg$Z9#q7K}72EQ}GJsk%mj zfok%+F%7=Ufo0X!P)cRi11Ll^MhFUFwPm)^CZLCMb#upzqoOnsi>EbE?`y4O?O3UB z#v2LfqDM=d7)mv`qGJZ7;7&$oxB6s!fnHX2+R$N$L}v($sdmcYSKA>v+ZbF|RVO%y zUgsn_GU;r6tfgq32Pq!(c{h9>J`Jnkmu?UFU-rn*7{0+CK}$vDP~xC8m8AXvZ6y`# zNO=-O-4KeczV+dNVHyYzh{|CS(P-rk?JoyVVi{V`S#xqakkC|LEkLAtr$rYGnCgpx zH5<^JtD2HBJdRXbF*YSsGbSfl08@@kTNA1pD2U^a{Svt+&;>!CfEb|qE;Kwyw945z z#D}}s#d#@_Wq1g0vI}4gxm7hXvuCa(L_IOO%3QT8eAb7CWK*o-s2hpl2UQeMAfvWN zry;8HAh)VB59G3qgIq{~@`+r}O0VR$0A&-o90nqgTTrP+E4iqMXbyuSh_mO!&)vL~ zjv|$pWF?2fp+)HvsHuw0QaJ~nqZN}$%H3on%|XLfBqcJ>~e84 z?t!1i>)XY<^@I7Svg*&%OFTfxx$#ixW2X7IU4Pju$F0#zdcIG0wqEVV6+7zv`3PPv zHeiM$yF*8}tU^!s`wzF*lxnnt`C*^4^%r9Oi~6@9N13?XjxuS3+rx(ZBG@;6w%9H1 z);G^XA2#Es!I?5l?dxBYQ^L-``v>{;H?w+#Jd%12d6fJDN{mULLm&dD*5{DdUk7_= zw^`iYjhjJ#@O*y`9j~_Q`|-7Tb$k7KB4svTuXjcp?gY=SZvP#rzW*#)Y<9gJLEgj3 J$;Hp_zXAXp?Hd39 From a12517d0b3ae2ae39ec903d45e3976de35832734 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 14 Nov 2023 17:54:19 +0300 Subject: [PATCH 177/204] init --- .../Swaps/Setup/SwapSetupPresenter.swift | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 3d80019b93..36bceb104d 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -26,6 +26,7 @@ final class SwapSetupPresenter: SwapBasePresenter { private var feeIdentifier: SwapSetupFeeIdentifier? private var slippage: BigRational + private var isManualFeeSet: Bool = false init( initState: SwapSetupInitState, @@ -202,6 +203,7 @@ final class SwapSetupPresenter: SwapBasePresenter { provideButtonState() provideIssues() provideNotification() + switchFeeChainAssetIfNecessary() } override func handleNewPrice(_: PriceData?, chainAssetId: ChainAssetId) { @@ -232,6 +234,7 @@ final class SwapSetupPresenter: SwapBasePresenter { } provideIssues() + switchFeeChainAssetIfNecessary() } override func handleNewBalanceExistense(_: AssetBalanceExistence, chainAssetId _: ChainAssetId) { @@ -601,6 +604,22 @@ extension SwapSetupPresenter { provideIssues() provideNotification() } + + private func switchFeeChainAssetIfNecessary() { + guard + !isManualFeeSet, + let payChainAsset = getPayChainAsset(), + let feeChainAsset = getFeeChainAsset(), + feeChainAsset.chainAssetId == payChainAsset.chain.utilityChainAssetId(), + let feeAssetBalance = feeAssetBalance, + let fee = fee?.totalFee.nativeAmount else { + return + } + + if feeAssetBalance.transferable < fee { + updateFeeChainAsset(payChainAsset) + } + } } extension SwapSetupPresenter: SwapSetupPresenterProtocol { @@ -642,6 +661,7 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { self?.interactor.update(payChainAsset: chainAsset) self?.interactor.update(feeChainAsset: feeChainAsset) + self?.isManualFeeSet = false if let direction = self?.quoteArgs?.direction { self?.refreshQuote(direction: direction, forceUpdate: false) @@ -759,6 +779,9 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { }, action: { [weak self] in let chainAsset = FeeSelectionViewModel(rawValue: $0) == .utilityAsset ? utilityAsset : payChainAsset + if chainAsset.chainAssetId != self?.feeChainAsset?.chainAssetId { + self?.isManualFeeSet = true + } self?.updateFeeChainAsset(chainAsset) }, selectedIndex: payAssetSelected ? FeeSelectionViewModel.payAsset.rawValue : From 91f8878b9101791ae0e46c8ca49fe4be847649b0 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 15 Nov 2023 17:03:53 +0300 Subject: [PATCH 178/204] fix error frame --- .../Setup/View/SwapAmountInputView.swift | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift index ce3f08e620..665e52e271 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapAmountInputView.swift @@ -3,6 +3,7 @@ import SoraUI final class SwapAmountInputView: RoundedView { let assetControl = SwapAssetControl() let textInputView = SwapAmountInput() + private var style: Style = .normal var contentInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 16) { didSet { @@ -116,7 +117,12 @@ final class SwapAmountInputView: RoundedView { } private func updateFocusState() { - strokeWidth = textInputView.textField.isFirstResponder ? 0.5 : 0.0 + switch style { + case .error: + strokeWidth = 0.5 + case .normal: + strokeWidth = textInputView.textField.isFirstResponder ? 0.5 : 0.0 + } } @objc private func actionEditingDidBeginEnd() { @@ -165,29 +171,24 @@ extension SwapAmountInputView { } extension SwapAmountInputView { - struct Style { - let contentStyle: RoundedView.Style - let textColor: UIColor? + enum Style { + case normal + case error } func applyInput(style: Style) { - apply(style: style.contentStyle) - - textInputView.textField.textColor = style.textColor - textInputView.textField.tintColor = style.textColor + switch style { + case .error: + apply(style: .strokeOnError) + textInputView.textField.textColor = R.color.colorTextNegative() + textInputView.textField.tintColor = R.color.colorTextNegative() + case .normal: + apply(style: .strokeOnEditing) + textInputView.textField.textColor = R.color.colorTextPrimary() + textInputView.textField.tintColor = R.color.colorTextPrimary() + } + self.style = style updateFocusState() } } - -extension SwapAmountInputView.Style { - static let normal = SwapAmountInputView.Style( - contentStyle: .strokeOnEditing, - textColor: R.color.colorTextPrimary() - ) - - static let error = SwapAmountInputView.Style( - contentStyle: .strokeOnError, - textColor: R.color.colorTextNegative() - ) -} From 029e2eebb17e006ee649fd29015f9c681fc0e449 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 15 Nov 2023 17:12:37 +0300 Subject: [PATCH 179/204] fix price difference --- .../Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift b/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift index a84880748b..b0307193e5 100644 --- a/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Base/Model/SwapBaseViewModelFactory.swift @@ -138,7 +138,7 @@ extension SwapBaseViewModelFactory: SwapBaseViewModelFactoryProtocol { let amountPriceIn = amountInDecimal * priceIn let amountPriceOut = amountOutDecimal * priceOut - guard amountPriceIn != 0, amountPriceIn > amountPriceOut else { + guard amountPriceIn > 0, amountPriceOut > 0, amountPriceIn > amountPriceOut else { return nil } From b8f869aca66ccd1e4f6661640d6b555d7589bb14 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 15 Nov 2023 21:27:06 +0300 Subject: [PATCH 180/204] fix sorting --- novawallet.xcodeproj/project.pbxproj | 12 +++++++-- novawallet/Common/Model/AmountPair.swift | 6 +++++ .../Models/AssetListGroupModel.swift | 5 +++- .../Models/AssetListGroupModelCompator.swift | 26 +++++++++++++++++++ .../Models/AssetListModelHelpers.swift | 19 +++++++------- 5 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 novawallet/Common/Model/AmountPair.swift create mode 100644 novawallet/Modules/AssetList/Models/AssetListGroupModelCompator.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index a521498f67..e0ada42d3e 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -810,8 +810,8 @@ 77A0B2F32A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A0B2F22A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift */; }; 77A0B2F52A3CA39D00CBF653 /* StakingMoreOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A0B2F42A3CA39D00CBF653 /* StakingMoreOptionCollectionViewCell.swift */; }; 77A0B2F92A3CA40E00CBF653 /* StakingMoreOptionsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A0B2F82A3CA40E00CBF653 /* StakingMoreOptionsSection.swift */; }; - 77A4F4032B036615006294BC /* Optional+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A4F4022B036615006294BC /* Optional+Result.swift */; }; 77A4F4012B035027006294BC /* AssetOperationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A4F4002B035027006294BC /* AssetOperationState.swift */; }; + 77A4F4032B036615006294BC /* Optional+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A4F4022B036615006294BC /* Optional+Result.swift */; }; 77A6F5AB2A2E0B31004AFD1A /* ReceiveAssetOperationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5AA2A2E0B31004AFD1A /* ReceiveAssetOperationPresenter.swift */; }; 77A6F5AE2A2E0C7C004AFD1A /* ReceiveAssetOperationWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5AD2A2E0C7C004AFD1A /* ReceiveAssetOperationWireframe.swift */; }; 77A6F5B92A2E2AAD004AFD1A /* BuyAssetOperationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5B72A2E2AAD004AFD1A /* BuyAssetOperationPresenter.swift */; }; @@ -856,6 +856,8 @@ 77CC82A32A984BC3002D022F /* StartStakingSelectedValidatorsListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CC82A22A984BC3002D022F /* StartStakingSelectedValidatorsListWireframe.swift */; }; 77CC82A52A984EDA002D022F /* UINavigaionController+Pop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CC82A42A984EDA002D022F /* UINavigaionController+Pop.swift */; }; 77CC82A72A986CF1002D022F /* StakingSelectValidatorsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CC82A62A986CF1002D022F /* StakingSelectValidatorsDelegate.swift */; }; + 77D2E2712B05416E0098F188 /* AssetListGroupModelCompator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77D2E2702B05416E0098F188 /* AssetListGroupModelCompator.swift */; }; + 77D2E2732B0542A50098F188 /* AmountPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77D2E2722B0542A50098F188 /* AmountPair.swift */; }; 77E0DC9E2A6940C400D03724 /* Calendar+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E0DC9D2A6940C400D03724 /* Calendar+Helpers.swift */; }; 77E255672A16145500B644C3 /* StakingRewardsFilterMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E255662A16145500B644C3 /* StakingRewardsFilterMapper.swift */; }; 77E255692A16148000B644C3 /* StakingRewardsFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E255682A16148000B644C3 /* StakingRewardsFilter.swift */; }; @@ -4904,8 +4906,8 @@ 77A0B2F22A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionsViewModelFactory.swift; sourceTree = ""; }; 77A0B2F42A3CA39D00CBF653 /* StakingMoreOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionCollectionViewCell.swift; sourceTree = ""; }; 77A0B2F82A3CA40E00CBF653 /* StakingMoreOptionsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionsSection.swift; sourceTree = ""; }; - 77A4F4022B036615006294BC /* Optional+Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Result.swift"; sourceTree = ""; }; 77A4F4002B035027006294BC /* AssetOperationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetOperationState.swift; sourceTree = ""; }; + 77A4F4022B036615006294BC /* Optional+Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Result.swift"; sourceTree = ""; }; 77A6F5AA2A2E0B31004AFD1A /* ReceiveAssetOperationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveAssetOperationPresenter.swift; sourceTree = ""; }; 77A6F5AD2A2E0C7C004AFD1A /* ReceiveAssetOperationWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveAssetOperationWireframe.swift; sourceTree = ""; }; 77A6F5B72A2E2AAD004AFD1A /* BuyAssetOperationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuyAssetOperationPresenter.swift; sourceTree = ""; }; @@ -4951,6 +4953,8 @@ 77CC82A22A984BC3002D022F /* StartStakingSelectedValidatorsListWireframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartStakingSelectedValidatorsListWireframe.swift; sourceTree = ""; }; 77CC82A42A984EDA002D022F /* UINavigaionController+Pop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigaionController+Pop.swift"; sourceTree = ""; }; 77CC82A62A986CF1002D022F /* StakingSelectValidatorsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingSelectValidatorsDelegate.swift; sourceTree = ""; }; + 77D2E2702B05416E0098F188 /* AssetListGroupModelCompator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListGroupModelCompator.swift; sourceTree = ""; }; + 77D2E2722B0542A50098F188 /* AmountPair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountPair.swift; sourceTree = ""; }; 77E0DC9D2A6940C400D03724 /* Calendar+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Helpers.swift"; sourceTree = ""; }; 77E255652A16059A00B644C3 /* MultiassetUserDataModel9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel9.xcdatamodel; sourceTree = ""; }; 77E255662A16145500B644C3 /* StakingRewardsFilterMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardsFilterMapper.swift; sourceTree = ""; }; @@ -12879,6 +12883,7 @@ 0C83775C2A4EEB380072102D /* AssetListState.swift */, 0CAC01542A52E0CC0069413E /* AssetListModelHelpers.swift */, 77CBD39B2ABBA98900D646D6 /* AssetListModel.swift */, + 77D2E2702B05416E0098F188 /* AssetListGroupModelCompator.swift */, ); path = Models; sourceTree = ""; @@ -13471,6 +13476,7 @@ 0C3205EB2A8A122D002EB914 /* FeeOutputModel.swift */, 0C59E8D02AA5FAC5001E11F3 /* PooledAssetBalance.swift */, 0CD352942ACAF59900B3E446 /* BigRational.swift */, + 77D2E2722B0542A50098F188 /* AmountPair.swift */, ); path = Model; sourceTree = ""; @@ -20222,6 +20228,7 @@ 848DAF042822B7FE00D56F55 /* ParachainStakingCollatorService+Fetch.swift in Sources */, 8846F72529D6BA4400B8B776 /* Data+base8.swift in Sources */, 840689FC26321F2700A017B1 /* StorageQuery.swift in Sources */, + 77D2E2732B0542A50098F188 /* AmountPair.swift in Sources */, 8423ADD026B2C38600057EDD /* ImportantFlowViewFactory.swift in Sources */, 887AFC8E28BCB314002A0422 /* SelectableIconSubtitleView.swift in Sources */, 849A4EF8279ABBDD00AB6709 /* AssetBalance.swift in Sources */, @@ -21408,6 +21415,7 @@ 847999A9288862A500D1BAD2 /* MetaAccountOperationFactory+Secrets.swift in Sources */, 8496ADD7276AFEED00306B24 /* DAppBrowserModel.swift in Sources */, 8473B4782A207FE0003DE213 /* StakingDashboardOffchainMapper.swift in Sources */, + 77D2E2712B05416E0098F188 /* AssetListGroupModelCompator.swift in Sources */, 84F4386625D9B8C600AEDA56 /* TypeRegistryPrepared.swift in Sources */, 84282298289BC50A00163031 /* SwitchAccount+ParitySignerAddConfirmWireframe.swift in Sources */, AE805FC526B3DF8B00007CE9 /* ValidatorInfoInteractorBase.swift in Sources */, diff --git a/novawallet/Common/Model/AmountPair.swift b/novawallet/Common/Model/AmountPair.swift new file mode 100644 index 0000000000..09cb183fbc --- /dev/null +++ b/novawallet/Common/Model/AmountPair.swift @@ -0,0 +1,6 @@ +import Foundation + +struct AmountPair { + let amount: TAmount + let value: TValue +} diff --git a/novawallet/Modules/AssetList/Models/AssetListGroupModel.swift b/novawallet/Modules/AssetList/Models/AssetListGroupModel.swift index 599c023c72..b5d7c7fa34 100644 --- a/novawallet/Modules/AssetList/Models/AssetListGroupModel.swift +++ b/novawallet/Modules/AssetList/Models/AssetListGroupModel.swift @@ -1,14 +1,17 @@ import Foundation import RobinHood +import BigInt struct AssetListGroupModel: Identifiable { var identifier: String { chain.chainId } let chain: ChainModel let chainValue: Decimal + let chainAmount: BigUInt - init(chain: ChainModel, chainValue: Decimal) { + init(chain: ChainModel, chainValue: Decimal, chainAmount: BigUInt) { self.chain = chain self.chainValue = chainValue + self.chainAmount = chainAmount } } diff --git a/novawallet/Modules/AssetList/Models/AssetListGroupModelCompator.swift b/novawallet/Modules/AssetList/Models/AssetListGroupModelCompator.swift new file mode 100644 index 0000000000..c97e03d42c --- /dev/null +++ b/novawallet/Modules/AssetList/Models/AssetListGroupModelCompator.swift @@ -0,0 +1,26 @@ +enum AssetListGroupModelComparator { + static var byValue: (AssetListGroupModel, AssetListGroupModel) -> Bool? = { + compare(model1: $0, model2: $1, by: \.chainValue, zeroValue: 0) + } + + static var byTotalAmount: (AssetListGroupModel, AssetListGroupModel) -> Bool? = { + compare(model1: $0, model2: $1, by: \.chainAmount, zeroValue: 0) + } + + static func compare( + model1: AssetListGroupModel, + model2: AssetListGroupModel, + by keypath: KeyPath, + zeroValue: T + ) -> Bool? where T: Comparable { + if model1[keyPath: keypath] > zeroValue, model2[keyPath: keypath] > zeroValue { + return model1[keyPath: keypath] > model2[keyPath: keypath] + } else if model1[keyPath: keypath] > zeroValue { + return true + } else if model2[keyPath: keypath] > zeroValue { + return false + } else { + return nil + } + } +} diff --git a/novawallet/Modules/AssetList/Models/AssetListModelHelpers.swift b/novawallet/Modules/AssetList/Models/AssetListModelHelpers.swift index 39d1f4d811..307404c25b 100644 --- a/novawallet/Modules/AssetList/Models/AssetListModelHelpers.swift +++ b/novawallet/Modules/AssetList/Models/AssetListModelHelpers.swift @@ -19,23 +19,24 @@ enum AssetListModelHelpers { from chain: ChainModel, assets: [AssetListAssetModel] ) -> AssetListGroupModel { - let value: Decimal = assets.reduce(0) { result, asset in - result + (asset.totalValue ?? 0) + let amountValue: AmountPair = assets.reduce(.init(amount: 0, value: 0)) { result, asset in + .init( + amount: result.amount + (asset.totalAmount ?? 0), + value: result.value + (asset.totalValue ?? 0) + ) } - return AssetListGroupModel(chain: chain, chainValue: value) + return AssetListGroupModel(chain: chain, chainValue: amountValue.value, chainAmount: amountValue.amount) } static func createGroupsDiffCalculator( from groups: [AssetListGroupModel] ) -> ListDifferenceCalculator { let sortingBlock: (AssetListGroupModel, AssetListGroupModel) -> Bool = { model1, model2 in - if model1.chainValue > 0, model2.chainValue > 0 { - return model1.chainValue > model2.chainValue - } else if model1.chainValue > 0 { - return true - } else if model2.chainValue > 0 { - return false + if let result = AssetListGroupModelComparator.byValue(model1, model2) { + return result + } else if let result = AssetListGroupModelComparator.byTotalAmount(model1, model2) { + return result } else { return ChainModelCompator.defaultComparator(chain1: model1.chain, chain2: model2.chain) } From 8bad469ed1656a8e4fa08b325e774ea78690452f Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 16 Nov 2023 12:58:07 +0300 Subject: [PATCH 181/204] update info icon --- novawallet.xcodeproj/project.pbxproj | 4 - .../iconInfo.imageset/Contents.json | 12 - .../iconInfo.imageset/iconInfo.pdf | Bin 2477 -> 0 bytes .../iconInfoFilled.imageset/Contents.json | 4 +- .../iconInfoFilled.pdf | Bin 2567 -> 0 bytes .../new-info-icon.pdf | Bin .../Contents.json | 12 - novawallet/Common/View/HintView.swift | 2 +- novawallet/Common/View/LinkView.swift | 2 +- .../View/StackTable/StackAddressCell.swift | 4 +- .../View/StackTable/StackInfoTableCell.swift | 15 +- .../StackTable/StackTitleMultiValueCell.swift | 4 +- .../View/AssetListTotalBalanceCell.swift | 4 +- .../View/OperationDetailsSwapView.swift | 2 - .../Selection/View/StakingPoolView.swift | 2 +- .../View/CollatorSelectionCell.swift | 2 +- .../View/CustomValidatorCell.swift | 2 +- .../View/SelectedValidatorCell.swift | 2 +- .../View/YourValidatorTableCell.swift | 2 +- .../View/RewardEstimationView.swift | 337 ------------------ .../Swaps/Base/View/SwapInfoView.swift | 4 +- .../Swaps/Base/View/SwapNetworkFeeView.swift | 2 +- .../Confirm/View/SwapConfirmViewLayout.swift | 4 - .../Swaps/Setup/View/SwapDetailsView.swift | 1 - .../Slippage/SwapSlippageViewLayout.swift | 4 +- .../View/Web3NameReceipientView.swift | 2 +- .../AcalaContributionSetupViewLayout.swift | 2 +- .../CommonVotes/VotesContentView.swift | 2 +- .../View/DelegateInfoView.swift | 2 +- .../View/GovernanceDelegateStackCell.swift | 4 +- .../View/ReferendumDetailsTitleView.swift | 2 +- .../ReferendumVotingStatusDetailsView.swift | 4 +- 32 files changed, 27 insertions(+), 418 deletions(-) delete mode 100644 novawallet/Assets.xcassets/iconInfo.imageset/Contents.json delete mode 100644 novawallet/Assets.xcassets/iconInfo.imageset/iconInfo.pdf delete mode 100644 novawallet/Assets.xcassets/iconInfoFilled.imageset/iconInfoFilled.pdf rename novawallet/Assets.xcassets/{iconInfoFilledAccent.imageset => iconInfoFilled.imageset}/new-info-icon.pdf (100%) delete mode 100644 novawallet/Assets.xcassets/iconInfoFilledAccent.imageset/Contents.json delete mode 100644 novawallet/Modules/Staking/StakingMain/View/RewardEstimationView.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index e0ada42d3e..ce638a87fd 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -2228,7 +2228,6 @@ 84948C36287DD1C800E6DD3E /* NftListRMRKV2ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84948C35287DD1C800E6DD3E /* NftListRMRKV2ViewModel.swift */; }; 84948C38287E0B4F00E6DD3E /* FilterImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84948C37287E0B4F00E6DD3E /* FilterImageProcessor.swift */; }; 8494D86B25247F9600614D8F /* Decimal+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8494D86A25247F9600614D8F /* Decimal+String.swift */; }; - 849528E326036997009DC845 /* RewardEstimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849528E226036997009DC845 /* RewardEstimationView.swift */; }; 84953F662934C7D90033F47D /* EtherscanERC20HistoryResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84953F652934C7D90033F47D /* EtherscanERC20HistoryResponse.swift */; }; 84953F682934C8A70033F47D /* EtherscanERC20HistoryInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84953F672934C8A70033F47D /* EtherscanERC20HistoryInfo.swift */; }; 84953F6A2934C9E20033F47D /* EtherscanERC20OperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84953F692934C9E20033F47D /* EtherscanERC20OperationFactory.swift */; }; @@ -6353,7 +6352,6 @@ 84948C35287DD1C800E6DD3E /* NftListRMRKV2ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NftListRMRKV2ViewModel.swift; sourceTree = ""; }; 84948C37287E0B4F00E6DD3E /* FilterImageProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterImageProcessor.swift; sourceTree = ""; }; 8494D86A25247F9600614D8F /* Decimal+String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+String.swift"; sourceTree = ""; }; - 849528E226036997009DC845 /* RewardEstimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RewardEstimationView.swift; sourceTree = ""; }; 84953F652934C7D90033F47D /* EtherscanERC20HistoryResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EtherscanERC20HistoryResponse.swift; sourceTree = ""; }; 84953F672934C8A70033F47D /* EtherscanERC20HistoryInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EtherscanERC20HistoryInfo.swift; sourceTree = ""; }; 84953F692934C9E20033F47D /* EtherscanERC20OperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EtherscanERC20OperationFactory.swift; sourceTree = ""; }; @@ -18180,7 +18178,6 @@ AEFC6D602600A754000BD310 /* View */ = { isa = PBXGroup; children = ( - 849528E226036997009DC845 /* RewardEstimationView.swift */, 84FFE504261290830054EA63 /* NetworkInfoView.swift */, F418E890264D318C00699085 /* AlertsView.swift */, 84B018AB26E01A4100C75E28 /* StakingStateView.swift */, @@ -21641,7 +21638,6 @@ 84DC3CE32795E0340038E2ED /* SubqueryHistoryOperationFactory+RemoteHistory.swift in Sources */, 5678BAE4B652C5C5E4284F28 /* AccountManagementViewFactory.swift in Sources */, 84CFE441292B8CDA00CDDD7C /* EvmOnChainTransferSetupInteractor.swift in Sources */, - 849528E326036997009DC845 /* RewardEstimationView.swift in Sources */, 77864F4C2A6AC5A100FA7BA7 /* TitleHorizontalMultiValueView+Bind.swift in Sources */, 88784E9D29BF966B004489D5 /* Web3NamesOperationFactory.swift in Sources */, 847999B628894FE200D1BAD2 /* AccountInputViewDelegate.swift in Sources */, diff --git a/novawallet/Assets.xcassets/iconInfo.imageset/Contents.json b/novawallet/Assets.xcassets/iconInfo.imageset/Contents.json deleted file mode 100644 index 282eb33b72..0000000000 --- a/novawallet/Assets.xcassets/iconInfo.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "iconInfo.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/novawallet/Assets.xcassets/iconInfo.imageset/iconInfo.pdf b/novawallet/Assets.xcassets/iconInfo.imageset/iconInfo.pdf deleted file mode 100644 index a986d0f8928a670186602f78dbb60b94efc02e86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2477 zcmZuzO>Y}F5WVwP@M0h-5Q@WZ0zrVrZi=EU>MFejJ*exAqe7NiNh#9&`o5udhikh! z81&=Jn>QaM_v-fM-77L#DbiLS{`gaA{ra_j^G1#PJN{R>CBFJ;+~1!+X%BGCR;TlE z+&rk&&G^rD7}wvw)z|Nr{|zVgw<2TPPuqF%N>;MxOmejpuX$mKvK}6~rS(M>E<{b)xVGCJIs`T1(b2`~e|9AmIWxaS)k<>h`mhY(C!-0?xk}n>Zm&WI~$-3_vz16j#y$%*NAfN0u^q-63mI;px!> zTxUCLT=uSH*iCHJ)Jp^jD#!pKbcu9Z`p)r@rgHP;4ZfBF6dQ^en zYG=Rg3VC0HXQ(!&c6>rD&1!-MTt}%E=iI^$8GzW~HscsMmw;%PEuZiYV=+`7)mDm- zqW8GB&;e(&3v3xNbfGa?ve{$OJLb7Mp-$ejr8gm@5FibfP?NypjU!vA#rC*lS%o)X z^wnQtyBLB)Y-ftCHMcl61zX^a8#!VUDzYwtJQvKN%oHiW8Cg?FrTK! z;+#|KQBJWE?7$n?70pbkB3I<_bguZa`uL67t6vqrtkbhOzp?zBUfy0p^78Zsf$=1- zzTNK+=ac^MJDzC#R=@rG*Qi&woBMGBeje|(o41EA`a^%+NlJAGrkJ~#))ttdv&vC8~pW#c-^q+t% zn9?IAPVgiFd*aud^JaIrf9(3@IDS-CXVqTC_D5ri&=bJZkF36I>;>}B$v-_qR2@`2 zfpDZALymCPpF%i}Gju*~w!87DxO+d`pd2qwhllZ{ezCoKc`3?jeK?#oC)^6WdcXZT WLVf$N+8oa<9p_?HS6AQt^6o#^+WLZ1G42JLe6}lKm4$w!51m?XS;EjfOLJ z_F%}*kQ85%8qL-1&AV65kWx-tef;w;rSYMLLKUag0f zubHxkl`WJ)Vn63$uM;b_Co~wMEAiVbMzq+heQU1C3hy*$_3&) zi>K21fPK+t))!)e_|+uz!y<(H{+vBvvS>bvnBI0MF}|i zeuTDSpxCcx6;LK0JV)=DgG>rVpO9A7>7lR(uJpvR;TH+I#;m9Kgmodp(j=A#8z++G zV^1y#fe^I?=h_Na!3qu=N{L+O9c%5i>4cWa5i3GVdu@6oGNGV0JcE5dGW>rEZE{)* z_~0f&ZTKAY%xP9A`h@W0bj`vZzngK&!=HLYyQ00 z_Hft7!(}{RNB*{$;qC4Orc+0&FAzrZWwQBce}_~xE7Wm|lG;sgnR%k1I5EPIn{{(ryWuGHlm(P&L zoBKJ!xX$Qu+U+04lj83Ec!P4hI3FL!m-@y2?&VaJ&GvY_XcpWGym`O>e?<1}lWljp QG&vqOgu1%={?~V30ampQ4FCWD diff --git a/novawallet/Assets.xcassets/iconInfoFilledAccent.imageset/new-info-icon.pdf b/novawallet/Assets.xcassets/iconInfoFilled.imageset/new-info-icon.pdf similarity index 100% rename from novawallet/Assets.xcassets/iconInfoFilledAccent.imageset/new-info-icon.pdf rename to novawallet/Assets.xcassets/iconInfoFilled.imageset/new-info-icon.pdf diff --git a/novawallet/Assets.xcassets/iconInfoFilledAccent.imageset/Contents.json b/novawallet/Assets.xcassets/iconInfoFilledAccent.imageset/Contents.json deleted file mode 100644 index 837aee4580..0000000000 --- a/novawallet/Assets.xcassets/iconInfoFilledAccent.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "new-info-icon.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/novawallet/Common/View/HintView.swift b/novawallet/Common/View/HintView.swift index 9b1b8d9d31..4ab392422c 100644 --- a/novawallet/Common/View/HintView.swift +++ b/novawallet/Common/View/HintView.swift @@ -10,7 +10,7 @@ final class HintView: UIView { }() let iconView: UIImageView = { - let view = UIImageView(image: R.image.iconInfoFilled()?.withRenderingMode(.alwaysTemplate)) + let view = UIImageView(image: R.image.iconInfoFilled()) view.tintColor = R.color.colorIconInactive() return view }() diff --git a/novawallet/Common/View/LinkView.swift b/novawallet/Common/View/LinkView.swift index d071ef91b3..56ba830b9f 100644 --- a/novawallet/Common/View/LinkView.swift +++ b/novawallet/Common/View/LinkView.swift @@ -18,7 +18,7 @@ final class LinkView: IconDetailsGenericView { let blueColor = R.color.colorButtonTextAccent()! mode = .iconDetails - imageView.image = R.image.iconInfoFilled()?.tinted(with: blueColor) + imageView.image = R.image.iconInfoAccent() spacing = 5.0 actionButton.applyIconStyle() diff --git a/novawallet/Common/View/StackTable/StackAddressCell.swift b/novawallet/Common/View/StackTable/StackAddressCell.swift index 73fdf25be8..7c267e472e 100644 --- a/novawallet/Common/View/StackTable/StackAddressCell.swift +++ b/novawallet/Common/View/StackTable/StackAddressCell.swift @@ -56,9 +56,7 @@ final class StackAddressCell: RowView = LocalizableResource { locale in - R.string.localizable.stakingStartTitle(preferredLanguages: locale.rLanguages) - } { - didSet { - applyActionTitle() - } - } - - weak var delegate: RewardEstimationViewDelegate? - - var locale = Locale.current { - didSet { - applyLocalization() - - if widgetViewModel != nil { - applyWidgetViewModel() - } - } - } - - private var widgetViewModel: StakingEstimationViewModel? - - override init(frame: CGRect) { - super.init(frame: frame) - - setupLayout() - - mainButton.addTarget( - self, - action: #selector(actionMainTouchUpInside), - for: .touchUpInside - ) - - infoButton.addTarget( - self, - action: #selector(actionInfoTouchUpInside), - for: .touchUpInside - ) - } - - override var intrinsicContentSize: CGSize { - CGSize(width: UIView.noIntrinsicMetric, height: 202.0) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - - if skeletonView != nil { - setupSkeleton() - } - } - - func bind(viewModel: StakingEstimationViewModel) { - widgetViewModel = viewModel - applyWidgetViewModel() - } - - private func setupLayout() { - addSubview(backgroundView) - backgroundView.snp.makeConstraints { make in - make.edges.equalToSuperview() - } - - addSubview(infoButton) - infoButton.snp.makeConstraints { make in - make.trailing.equalToSuperview() - make.top.equalToSuperview() - make.width.equalTo(56.0) - make.height.equalTo(48.0) - } - - addSubview(titleLabel) - titleLabel.snp.makeConstraints { make in - make.leading.equalToSuperview().inset(16.0) - make.top.equalToSuperview().inset(14.0) - } - - addSubview(mainButton) - mainButton.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) - make.bottom.equalToSuperview().inset(24.0) - make.height.equalTo(UIConstants.actionHeight) - } - - addSubview(monthlyTitleLabel) - monthlyTitleLabel.snp.makeConstraints { make in - make.leading.greaterThanOrEqualToSuperview().inset(16) - make.trailing.lessThanOrEqualTo(self.snp.centerX).offset(-16.0) - make.centerX.equalToSuperview().multipliedBy(0.5) - make.bottom.equalTo(mainButton.snp.top).offset(-24.0) - } - - addSubview(yearlyTitleLabel) - yearlyTitleLabel.snp.makeConstraints { make in - make.leading.greaterThanOrEqualTo(self.snp.centerX).offset(16.0) - make.trailing.lessThanOrEqualToSuperview().offset(-16.0) - make.centerX.equalToSuperview().multipliedBy(1.5) - make.bottom.equalTo(mainButton.snp.top).offset(-24.0) - } - - addSubview(monthlyValueLabel) - monthlyValueLabel.snp.makeConstraints { make in - make.leading.greaterThanOrEqualToSuperview().inset(16) - make.trailing.lessThanOrEqualTo(self.snp.centerX).offset(-16.0) - make.centerX.equalToSuperview().multipliedBy(0.5) - make.bottom.equalTo(monthlyTitleLabel.snp.top).offset(-2.0) - } - - addSubview(yearlyValueLabel) - yearlyValueLabel.snp.makeConstraints { make in - make.leading.greaterThanOrEqualTo(self.snp.centerX).offset(16.0) - make.trailing.lessThanOrEqualToSuperview().offset(-16.0) - make.centerX.equalToSuperview().multipliedBy(1.5) - make.bottom.equalTo(yearlyTitleLabel.snp.top).offset(-2.0) - } - } - - private func applyWidgetViewModel() { - let tokenSymbol = widgetViewModel?.tokenSymbol ?? "" - titleLabel.text = R.string.localizable.stakingEstimateEarningTitle_v190( - tokenSymbol.uppercased(), - preferredLanguages: locale.rLanguages - ) - - if let viewModel = widgetViewModel?.reward?.value(for: locale) { - stopLoadingIfNeeded() - - monthlyValueLabel.text = viewModel.monthly - yearlyValueLabel.text = viewModel.yearly - } else { - startLoadingIfNeeded() - } - } - - private func applyLocalization() { - let languages = locale.rLanguages - - monthlyTitleLabel.text = R.string.localizable - .stakingMonthPeriodTitle(preferredLanguages: languages) - - yearlyTitleLabel.text = R.string.localizable - .stakingYearPeriodTitle(preferredLanguages: languages) - - applyActionTitle() - } - - private func applyActionTitle() { - let title = actionTitle.value(for: locale) - mainButton.imageWithTitleView?.title = title - mainButton.invalidateLayout() - } - - func startLoadingIfNeeded() { - guard skeletonView == nil else { - return - } - - monthlyValueLabel.alpha = 0.0 - yearlyValueLabel.alpha = 0.0 - - setupSkeleton() - } - - func stopLoadingIfNeeded() { - guard skeletonView != nil else { - return - } - - skeletonView?.stopSkrulling() - skeletonView?.removeFromSuperview() - skeletonView = nil - - monthlyValueLabel.alpha = 1.0 - yearlyValueLabel.alpha = 1.0 - } - - private func setupSkeleton() { - let spaceSize = frame.size - - guard spaceSize.width > 0, spaceSize.height > 0 else { - return - } - - let builder = Skrull( - size: spaceSize, - decorations: [], - skeletons: createSkeletons(for: spaceSize) - ) - - let currentSkeletonView: SkrullableView? - - if let skeletonView = skeletonView { - currentSkeletonView = skeletonView - builder.updateSkeletons(in: skeletonView) - } else { - let view = builder - .fillSkeletonStart(R.color.colorSkeletonStart()!) - .fillSkeletonEnd(color: R.color.colorSkeletonEnd()!) - .build() - view.autoresizingMask = [] - insertSubview(view, aboveSubview: backgroundView) - - skeletonView = view - - view.startSkrulling() - - currentSkeletonView = view - } - - currentSkeletonView?.frame = CGRect(origin: .zero, size: spaceSize) - } - - private func createSkeletons(for spaceSize: CGSize) -> [Skeletonable] { - let bigRowSize = CGSize(width: 72.0, height: 12.0) - - let offsetY = 8.0 - let monthlyOffsetX = monthlyTitleLabel.intrinsicContentSize.width / 2.0 - bigRowSize.width / 2.0 - let yearlyOffsetX = yearlyTitleLabel.intrinsicContentSize.width / 2.0 - bigRowSize.width / 2.0 - - return [ - SingleSkeleton.createRow( - above: monthlyTitleLabel, - containerView: self, - spaceSize: spaceSize, - offset: CGPoint(x: monthlyOffsetX, y: offsetY), - size: bigRowSize - ), - - SingleSkeleton.createRow( - above: yearlyTitleLabel, - containerView: self, - spaceSize: spaceSize, - offset: CGPoint(x: yearlyOffsetX, y: offsetY), - size: bigRowSize - ) - ] - } - - @objc private func actionMainTouchUpInside() { - delegate?.rewardEstimationDidStartAction(self) - } - - @objc private func actionInfoTouchUpInside() { - delegate?.rewardEstimationDidRequestInfo(self) - } -} - -extension RewardEstimationView: SkeletonLoadable { - func didDisappearSkeleton() { - skeletonView?.stopSkrulling() - } - - func didAppearSkeleton() { - skeletonView?.stopSkrulling() - skeletonView?.startSkrulling() - } - - func didUpdateSkeletonLayout() { - guard skeletonView != nil else { - return - } - - setupSkeleton() - } -} diff --git a/novawallet/Modules/Swaps/Base/View/SwapInfoView.swift b/novawallet/Modules/Swaps/Base/View/SwapInfoView.swift index 56a9a28346..2bf82caa1a 100644 --- a/novawallet/Modules/Swaps/Base/View/SwapInfoView.swift +++ b/novawallet/Modules/Swaps/Base/View/SwapInfoView.swift @@ -32,9 +32,7 @@ final class SwapInfoView: GenericTitleValueView, Skeleto private func configure() { titleButton.applyIconStyle() - titleButton.imageWithTitleView?.iconImage = R.image.iconInfoFilled()?.tinted( - with: R.color.colorIconSecondary()! - ) + titleButton.imageWithTitleView?.iconImage = R.image.iconInfoFilled() titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() titleButton.imageWithTitleView?.titleFont = .regularFootnote titleButton.imageWithTitleView?.spacingBetweenLabelAndIcon = 4 diff --git a/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeView.swift b/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeView.swift index 425d5a89c9..2212f759f1 100644 --- a/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeView.swift +++ b/novawallet/Modules/Swaps/Base/View/SwapNetworkFeeView.swift @@ -38,7 +38,7 @@ final class SwapNetworkFeeView: GenericTitleValueView Date: Thu, 16 Nov 2023 13:19:50 +0300 Subject: [PATCH 182/204] pr fix --- novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 36bceb104d..48d3bd91b6 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -612,6 +612,8 @@ extension SwapSetupPresenter { let feeChainAsset = getFeeChainAsset(), feeChainAsset.chainAssetId == payChainAsset.chain.utilityChainAssetId(), let feeAssetBalance = feeAssetBalance, + let payAssetBalance = payAssetBalance, + payAssetBalance.transferable > 0, let fee = fee?.totalFee.nativeAmount else { return } From 77282104c9ee0f6f227781b735f72c5f0e359c26 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 16 Nov 2023 13:56:28 +0300 Subject: [PATCH 183/204] pr fix --- .../AssetList/Models/AssetListBuilderResult.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift b/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift index 47f1aed902..6d9c62a161 100644 --- a/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift +++ b/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift @@ -50,14 +50,17 @@ struct AssetListBuilderResult { } func hasSwaps() -> Bool { - balanceResults.contains { - guard let chain = allChains[$0.key.chainId] else { + allChains.values.contains { chain in + guard chain.hasSwaps else { return false } - if case .success = $0.value { - return chain.hasSwaps - } else { - return false + return chain.assets.contains { asset in + let chainAssetId = ChainAssetId(chainId: chain.chainId, assetId: asset.assetId) + if case .success = balanceResults[chainAssetId] { + return true + } else { + return false + } } } } From 8b71af73dc90ae47511dad284055d0e6096603e2 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 16 Nov 2023 14:20:40 +0300 Subject: [PATCH 184/204] add wiki --- .../iconWiki.imageset/Contents.json | 12 ++++++++++++ .../iconWiki.imageset/nova-wiki.pdf | Bin 0 -> 2872 bytes .../Common/Configs/ApplicationConfigs.swift | 5 +++++ .../Modules/Settings/SettingsPresenter.swift | 2 ++ .../Modules/Settings/ViewModel/SettingsRow.swift | 5 +++++ .../ViewModel/SettingsViewModelFactory.swift | 1 + novawallet/en.lproj/Localizable.strings | 1 + novawallet/ru.lproj/Localizable.strings | 1 + 8 files changed, 27 insertions(+) create mode 100644 novawallet/Assets.xcassets/iconWiki.imageset/Contents.json create mode 100644 novawallet/Assets.xcassets/iconWiki.imageset/nova-wiki.pdf diff --git a/novawallet/Assets.xcassets/iconWiki.imageset/Contents.json b/novawallet/Assets.xcassets/iconWiki.imageset/Contents.json new file mode 100644 index 0000000000..72f6443dee --- /dev/null +++ b/novawallet/Assets.xcassets/iconWiki.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "nova-wiki.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconWiki.imageset/nova-wiki.pdf b/novawallet/Assets.xcassets/iconWiki.imageset/nova-wiki.pdf new file mode 100644 index 0000000000000000000000000000000000000000..71c02bf7a5ee17b7ca4b00b16d4d7f10798bddd9 GIT binary patch literal 2872 zcmZvePmdc#5XJBFDf$vAK}x&-SGN>JBD(}3K!gpqh=cJ?HjA=0*bWN$^!%!4Ja!U% zSk1oa?x}k9>Qz5_b^Y?Gw=xXgIJ5iX&%u~y&&>1Z!??ZGe}{01uYMZ0cgJ@o0^aDV z!*M^ZABNTS_|N@jy!rlxxq5y1-{vs0H+rJBvd;H(cm zm&VQ{F4t<7^@q)tgVVGp*$}I%#XOubUu^NkyJFrC`o2`oRM5pB&J-Jb3NwPLcB;;VOt!*UGKlVkGHwcL@EC3BXhDn3-jN~@wwEXn&;B$rEMreia_ z8@#hk^Abq(rKA*1u)QFSa(PcEPSViMm6!S4r=#Dj%sbp#$5JDz$;fX@A2eno&iKj}N z`h+5T5h0d!ia7~Uchf^c9s75XqSW3AuuVjIVu@&-c!QRV_EpxZXt(KZPeYL`5fvMy zI(S!|K>^C7$w)OhFj`(Y=bQw8()fzD6fBd)#lnVwgVVjsc@>(aCK9;#aG|?HY;0X3 zOxl+sy)ZoyUeZ;BLjC8Ay~d1JR7tFxs6mk$an2L3GlP`5U?iP_dZPM&z_a&Jx{TyA z?bX@k^OI@8$p-W5=GL<)KD(p>UBJciF0g28%^7c?>Zph?qs^Eb#)vjDt`$K7rOjFj zhZoYdbeu(>B5DR`Rtj2L`uOBB3$hssoyzH`>$D1!#Feya7VSGqMWOf{)l=ul#j9ww zc;4eHVA)AZx(azs{u1dBHPWW_?P~(7iqfx1<2g(n+_Shwb$~pU%haO$PCY`Qm?U>g z6BXmM6#*8C!rM~meO*U^p~xs})~qtubxZ&XtC|cV%tVDGl5m%) z+SUT~fJ>1|M|Tis&~-Ib!5M+*6IciO(wNn+`xZ{0w9w10gRpC1Hmli|aEp#!sAd*J zH|0f27Ygb6nx({O@Q}w(eVa~DgJU!-;5^GhLwBH*EUP{Aq30{HFbSGk^*kFz%?x|a zOzh8xD0@V?>`gyUTS7r;o>FU2I+WS7OpVJ%IHQZJ6HrFTXkeO~-Rg;l5SXSqBa~}6 zT#{KbRF2<%Gi--n2fgIXH<;6PM{hRMH7GEOms=2`I^T{~-)^_N)o=g) zHJa6{_1!oFKaaQf>leF^=FOx4E$33#z_gs|c78gYbTb}yANHH^VDvtAGaq-g+aAX) zU)0Ok34FEQgBdi|p^GbpiFCeVr3y2%R zSCG2Hyz#hS-@hOC19#ii57*G~_^^8zpP0w@w@)TgRyVud(P+Y*;MME Date: Thu, 16 Nov 2023 14:45:15 +0300 Subject: [PATCH 185/204] enable scroll for dapp networks --- .../Common/ViewController/ModalPicker/ModalNetworksFactory.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/novawallet/Common/ViewController/ModalPicker/ModalNetworksFactory.swift b/novawallet/Common/ViewController/ModalPicker/ModalNetworksFactory.swift index 2b39696a51..2c57de252a 100644 --- a/novawallet/Common/ViewController/ModalPicker/ModalNetworksFactory.swift +++ b/novawallet/Common/ViewController/ModalPicker/ModalNetworksFactory.swift @@ -31,6 +31,7 @@ enum ModalNetworksFactory { viewController.footerHeight = 0.0 viewController.sectionHeaderHeight = 32 viewController.sectionFooterHeight = 32 + viewController.isScrollEnabled = true return viewController } From 6c0147f9bd4a3352df2d5681c042aa7f401cb932 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 16 Nov 2023 17:43:25 +0300 Subject: [PATCH 186/204] fix selection --- novawallet/Common/View/CollapsableContainerView.swift | 7 ++----- .../Modules/Swaps/Setup/View/SwapDetailsView.swift | 11 +++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/novawallet/Common/View/CollapsableContainerView.swift b/novawallet/Common/View/CollapsableContainerView.swift index 0e4f1e3bae..d5df0c1892 100644 --- a/novawallet/Common/View/CollapsableContainerView.swift +++ b/novawallet/Common/View/CollapsableContainerView.swift @@ -10,7 +10,6 @@ class CollapsableContainerView: UIView { private enum Constants { static let headerHeight: CGFloat = 32 static let rowHeight: CGFloat = 44 - static let stackViewBottomInset: CGFloat = 4 } let backgroundView = BlockBackgroundView() @@ -115,9 +114,7 @@ class CollapsableContainerView: UIView { contentView.addSubview(stackView) stackView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.top.equalToSuperview() - make.bottom.equalToSuperview().inset(Constants.stackViewBottomInset) + make.edges.equalToSuperview() } rows.forEach { view in @@ -164,7 +161,7 @@ class CollapsableContainerView: UIView { } else { contentView.snp.updateConstraints { make in make.top.equalToSuperview().offset( - -CGFloat(stackView.arrangedSubviews.count) * Constants.rowHeight - Constants.stackViewBottomInset + -CGFloat(stackView.arrangedSubviews.count) * Constants.rowHeight ) } layoutIfNeeded() diff --git a/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift b/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift index e599e1e565..f0b171312a 100644 --- a/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift +++ b/novawallet/Modules/Swaps/Setup/View/SwapDetailsView.swift @@ -5,11 +5,22 @@ final class SwapDetailsView: CollapsableContainerView { $0.titleButton.imageWithTitleView?.titleColor = R.color.colorTextSecondary() $0.titleButton.imageWithTitleView?.titleFont = .regularFootnote $0.contentInsets = .init(top: 8, left: 16, bottom: 8, right: 16) + $0.borderView.borderType = .bottom + $0.roundedBackgroundView.cornerRadius = 12 + $0.roundedBackgroundView.roundingCorners = [.topLeft, .topRight] } let networkFeeCell: SwapNetworkFeeViewCell = .create { $0.contentInsets = .init(top: 8, left: 16, bottom: 8, right: 16) $0.borderView.borderType = .none + $0.roundedBackgroundView.cornerRadius = 12 + $0.roundedBackgroundView.roundingCorners = [.bottomLeft, .bottomRight] + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundView.sideLength = 12 } override var rows: [UIView] { From 348b7b6c816fe5887950e83d956eb58fff842ee2 Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 17 Nov 2023 09:36:21 +0100 Subject: [PATCH 187/204] add parsing swap extrinsics --- novawallet.xcodeproj/project.pbxproj | 50 ++- .../Subquery/SubqueryHistory+Wallet.swift | 2 +- .../ExtrinsicProcessing.swift | 21 +- .../ExtrinsicProcessingResult.swift | 24 ++ .../ExtrinsicProcessor+CustomFee.swift | 25 ++ .../ExtrinsicProcessor+Events.swift | 0 .../ExtrinsicProcessor+Fee.swift | 0 .../ExtrinsicProcessor+Matching.swift | 0 .../ExtrinsicProcessor+SwapMatching.swift | 289 ++++++++++++++++++ .../TransactionSubscription.swift | 0 .../AssetConversionPallet+Call.swift | 15 + .../AssetConversionPallet+Event.swift | 17 ++ .../AssetTxPaymentPallet.swift | 16 + .../Substrate/Types/CallCodingPath.swift | 9 - .../AssetHub/AssetHubTokensConverter.swift | 61 ++++ 15 files changed, 500 insertions(+), 29 deletions(-) rename novawallet/Common/Services/{WebSocketService/StorageSubscription => TransactionSubscription}/ExtrinsicProcessing.swift (89%) create mode 100644 novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessingResult.swift create mode 100644 novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+CustomFee.swift rename novawallet/Common/Services/{WebSocketService/StorageSubscription => TransactionSubscription}/ExtrinsicProcessor+Events.swift (100%) rename novawallet/Common/Services/{WebSocketService/StorageSubscription => TransactionSubscription}/ExtrinsicProcessor+Fee.swift (100%) rename novawallet/Common/Services/{WebSocketService/StorageSubscription => TransactionSubscription}/ExtrinsicProcessor+Matching.swift (100%) create mode 100644 novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+SwapMatching.swift rename novawallet/Common/Services/{WebSocketService/StorageSubscription => TransactionSubscription}/TransactionSubscription.swift (100%) create mode 100644 novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Event.swift create mode 100644 novawallet/Common/Substrate/Types/AssetTxPaymentPallet/AssetTxPaymentPallet.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index a521498f67..9bcf19944f 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -138,6 +138,10 @@ 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 */; }; + 0C500B1F2B04EA9100ABEE70 /* AssetConversionPallet+Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C500B1E2B04EA9100ABEE70 /* AssetConversionPallet+Event.swift */; }; + 0C500B222B05102900ABEE70 /* AssetTxPaymentPallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C500B212B05102900ABEE70 /* AssetTxPaymentPallet.swift */; }; + 0C500B242B0511F400ABEE70 /* ExtrinsicProcessor+CustomFee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C500B232B0511F400ABEE70 /* ExtrinsicProcessor+CustomFee.swift */; }; + 0C500B272B07314000ABEE70 /* ExtrinsicProcessingResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C500B262B07314000ABEE70 /* ExtrinsicProcessingResult.swift */; }; 0C5364A02A4D6EB700990478 /* AssetListBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C53649F2A4D6EB700990478 /* AssetListBuilder.swift */; }; 0C543E972AAB1B350035F45F /* ElectedAndPrefValidators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C543E962AAB1B350035F45F /* ElectedAndPrefValidators.swift */; }; 0C56B29DBA5245728AF7EDA4 /* GovernanceEditDelegationTracksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B18E8361691E548ABAB33EA4 /* GovernanceEditDelegationTracksViewController.swift */; }; @@ -233,6 +237,7 @@ 0CA50CB32AFE6F23005668CD /* GetTokenOptionsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA50CB22AFE6F23005668CD /* GetTokenOptionsPresenter.swift */; }; 0CA50CB52AFE6F31005668CD /* GetTokenOptionsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA50CB42AFE6F31005668CD /* GetTokenOptionsInteractor.swift */; }; 0CA50CB72AFE6F40005668CD /* GetTokenOptionsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA50CB62AFE6F40005668CD /* GetTokenOptionsViewFactory.swift */; }; + 0CA7821C2B03D0A9003F562A /* ExtrinsicProcessor+SwapMatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7821B2B03D0A9003F562A /* ExtrinsicProcessor+SwapMatching.swift */; }; 0CAC01552A52E0CC0069413E /* AssetListModelHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAC01542A52E0CC0069413E /* AssetListModelHelpers.swift */; }; 0CAC01572A52E1960069413E /* AssetListPresenterHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAC01562A52E1960069413E /* AssetListPresenterHelpers.swift */; }; 0CAC44AA2A7A79C2001EDE61 /* StartStakingInfoParachainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAC44A92A7A79C2001EDE61 /* StartStakingInfoParachainWireframe.swift */; }; @@ -810,8 +815,8 @@ 77A0B2F32A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A0B2F22A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift */; }; 77A0B2F52A3CA39D00CBF653 /* StakingMoreOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A0B2F42A3CA39D00CBF653 /* StakingMoreOptionCollectionViewCell.swift */; }; 77A0B2F92A3CA40E00CBF653 /* StakingMoreOptionsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A0B2F82A3CA40E00CBF653 /* StakingMoreOptionsSection.swift */; }; - 77A4F4032B036615006294BC /* Optional+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A4F4022B036615006294BC /* Optional+Result.swift */; }; 77A4F4012B035027006294BC /* AssetOperationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A4F4002B035027006294BC /* AssetOperationState.swift */; }; + 77A4F4032B036615006294BC /* Optional+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A4F4022B036615006294BC /* Optional+Result.swift */; }; 77A6F5AB2A2E0B31004AFD1A /* ReceiveAssetOperationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5AA2A2E0B31004AFD1A /* ReceiveAssetOperationPresenter.swift */; }; 77A6F5AE2A2E0C7C004AFD1A /* ReceiveAssetOperationWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5AD2A2E0C7C004AFD1A /* ReceiveAssetOperationWireframe.swift */; }; 77A6F5B92A2E2AAD004AFD1A /* BuyAssetOperationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5B72A2E2AAD004AFD1A /* BuyAssetOperationPresenter.swift */; }; @@ -4233,6 +4238,10 @@ 0C432D57ACFA53F42E574CBD /* TokensManageViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManageViewController.swift; sourceTree = ""; }; 0C463FC72A58126A003E71C9 /* UIView+MotionEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+MotionEffect.swift"; sourceTree = ""; }; 0C463FCF2A592ACD003E71C9 /* PartialInterpolatingMotionEffect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PartialInterpolatingMotionEffect.swift; sourceTree = ""; }; + 0C500B1E2B04EA9100ABEE70 /* AssetConversionPallet+Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetConversionPallet+Event.swift"; sourceTree = ""; }; + 0C500B212B05102900ABEE70 /* AssetTxPaymentPallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetTxPaymentPallet.swift; sourceTree = ""; }; + 0C500B232B0511F400ABEE70 /* ExtrinsicProcessor+CustomFee.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ExtrinsicProcessor+CustomFee.swift"; sourceTree = ""; }; + 0C500B262B07314000ABEE70 /* ExtrinsicProcessingResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicProcessingResult.swift; sourceTree = ""; }; 0C53649F2A4D6EB700990478 /* AssetListBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListBuilder.swift; sourceTree = ""; }; 0C543E962AAB1B350035F45F /* ElectedAndPrefValidators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElectedAndPrefValidators.swift; sourceTree = ""; }; 0C56B4FA2A4B0C320030F9C9 /* AssetListBaseBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListBaseBuilder.swift; sourceTree = ""; }; @@ -4328,6 +4337,7 @@ 0CA50CB22AFE6F23005668CD /* GetTokenOptionsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsPresenter.swift; sourceTree = ""; }; 0CA50CB42AFE6F31005668CD /* GetTokenOptionsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsInteractor.swift; sourceTree = ""; }; 0CA50CB62AFE6F40005668CD /* GetTokenOptionsViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTokenOptionsViewFactory.swift; sourceTree = ""; }; + 0CA7821B2B03D0A9003F562A /* ExtrinsicProcessor+SwapMatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ExtrinsicProcessor+SwapMatching.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 = ""; }; 0CAC44A92A7A79C2001EDE61 /* StartStakingInfoParachainWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoParachainWireframe.swift; sourceTree = ""; }; @@ -4904,8 +4914,8 @@ 77A0B2F22A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionsViewModelFactory.swift; sourceTree = ""; }; 77A0B2F42A3CA39D00CBF653 /* StakingMoreOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionCollectionViewCell.swift; sourceTree = ""; }; 77A0B2F82A3CA40E00CBF653 /* StakingMoreOptionsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionsSection.swift; sourceTree = ""; }; - 77A4F4022B036615006294BC /* Optional+Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Result.swift"; sourceTree = ""; }; 77A4F4002B035027006294BC /* AssetOperationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetOperationState.swift; sourceTree = ""; }; + 77A4F4022B036615006294BC /* Optional+Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Result.swift"; sourceTree = ""; }; 77A6F5AA2A2E0B31004AFD1A /* ReceiveAssetOperationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReceiveAssetOperationPresenter.swift; sourceTree = ""; }; 77A6F5AD2A2E0C7C004AFD1A /* ReceiveAssetOperationWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveAssetOperationWireframe.swift; sourceTree = ""; }; 77A6F5B72A2E2AAD004AFD1A /* BuyAssetOperationPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuyAssetOperationPresenter.swift; sourceTree = ""; }; @@ -8507,6 +8517,7 @@ isa = PBXGroup; children = ( 0C22006D2ACAAC2F0067BA61 /* AssetConversionPallet+Call.swift */, + 0C500B1E2B04EA9100ABEE70 /* AssetConversionPallet+Event.swift */, ); path = AssetConversionPallet; sourceTree = ""; @@ -8616,6 +8627,29 @@ path = Effects; sourceTree = ""; }; + 0C500B202B05101100ABEE70 /* AssetTxPaymentPallet */ = { + isa = PBXGroup; + children = ( + 0C500B212B05102900ABEE70 /* AssetTxPaymentPallet.swift */, + ); + path = AssetTxPaymentPallet; + sourceTree = ""; + }; + 0C500B252B0730FC00ABEE70 /* TransactionSubscription */ = { + isa = PBXGroup; + children = ( + 8454C26E2632BBAA00657DAD /* ExtrinsicProcessing.swift */, + 849B563227A70D71007D5528 /* ExtrinsicProcessor+Fee.swift */, + 849B563427A70DDE007D5528 /* ExtrinsicProcessor+Matching.swift */, + 847A25BA28D7BB92006AC9F5 /* ExtrinsicProcessor+Events.swift */, + 84FD3DB62540EF0700A234E3 /* TransactionSubscription.swift */, + 0CA7821B2B03D0A9003F562A /* ExtrinsicProcessor+SwapMatching.swift */, + 0C500B232B0511F400ABEE70 /* ExtrinsicProcessor+CustomFee.swift */, + 0C500B262B07314000ABEE70 /* ExtrinsicProcessingResult.swift */, + ); + path = TransactionSubscription; + sourceTree = ""; + }; 0C59E8CA2AA5D621001E11F3 /* ExternalBalanceUpdater */ = { isa = PBXGroup; children = ( @@ -10551,6 +10585,7 @@ 84155DE8253980D700A27058 /* Services */ = { isa = PBXGroup; children = ( + 0C500B252B0730FC00ABEE70 /* TransactionSubscription */, 0C59E8CA2AA5D621001E11F3 /* ExternalBalanceUpdater */, 8455F1912A1DC631003F072D /* Multistaking */, 8490111229E68FAD005D688B /* WalletConnect */, @@ -11324,6 +11359,7 @@ 8438E1DC24C18F11001BDB13 /* Types */ = { isa = PBXGroup; children = ( + 0C500B202B05101100ABEE70 /* AssetTxPaymentPallet */, 0C0CB3862AC5686C00EAC516 /* AssetConversionPallet */, 0C7945B92ABB223D001C07CA /* XTokens */, 0C893E6B2A65629E00781503 /* NominationPools */, @@ -12592,14 +12628,9 @@ 84F30EA025FD3EE700039D09 /* ChildSubscriptionFactory.swift */, 84F30E9B25FD3DBC00039D09 /* EmptyHandlingStorageSubscription.swift */, 84F30E9625FD3C5300039D09 /* EventEmittingStorageSubscription.swift */, - 8454C26E2632BBAA00657DAD /* ExtrinsicProcessing.swift */, - 849B563227A70D71007D5528 /* ExtrinsicProcessor+Fee.swift */, - 849B563427A70DDE007D5528 /* ExtrinsicProcessor+Matching.swift */, - 847A25BA28D7BB92006AC9F5 /* ExtrinsicProcessor+Events.swift */, 8470D6D1253E3382009E9A5D /* StorageSubscriptionContainer.swift */, 8470D6CF253E321C009E9A5D /* StorageSubscriptionProtocols.swift */, 8470D6D3253E35F0009E9A5D /* StorageUpdate.swift */, - 84FD3DB62540EF0700A234E3 /* TransactionSubscription.swift */, 84B73AD1279B47700071AE16 /* AssetsBalanceUpdater.swift */, 84B73AD7279C2EDA0071AE16 /* AssetAccountSubscription.swift */, 84F18D4D27A18C1400CA7554 /* OrmlAccountSubscription.swift */, @@ -20922,6 +20953,7 @@ 8490145824A9406D008F705E /* LegalData.swift in Sources */, 8472C601265D7A1F00E2481B /* WebSocketProviderSource.swift in Sources */, 8425EA9025EA7E5800C307C9 /* ElectedValidatorInfo.swift in Sources */, + 0C500B222B05102900ABEE70 /* AssetTxPaymentPallet.swift in Sources */, 84216FCF28264A1E00479375 /* ParaStakingRewardCalculatorEngine.swift in Sources */, 84DA03D62759341200E8B326 /* ChainAccountControl.swift in Sources */, 84329ED02832461D0020BC1C /* TimeInterval+Localization.swift in Sources */, @@ -21877,6 +21909,7 @@ 848DAEF7282274E700D56F55 /* ParachainStakingRemoteSubscriptionService.swift in Sources */, 845B821F26EF8E8900D25C72 /* ManagedMetaAccountModel.swift in Sources */, 88F34FD428FFE64400712BDE /* ReferendumDAppCellView.swift in Sources */, + 0CA7821C2B03D0A9003F562A /* ExtrinsicProcessor+SwapMatching.swift in Sources */, 8430AB1226023C9F005B1066 /* PendingBondedState.swift in Sources */, 88421059289BBA8D00306F2C /* CurrencyViewFactory.swift in Sources */, 84F76ED829006BC400D7206C /* DiscreteGradientSlider+Style.swift in Sources */, @@ -21888,6 +21921,7 @@ 848F5FE52989130B0058CD74 /* GovernanceOffchainDelegations.swift in Sources */, 84F1CB3327CE575E0095D523 /* NftListUniquesViewModel.swift in Sources */, AEBE173B262F3E6600DF257C /* StakingPayoutConfirmViewModelFactory.swift in Sources */, + 0C500B272B07314000ABEE70 /* ExtrinsicProcessingResult.swift in Sources */, 84B64E3F2704567700914E88 /* StakingLocalStorageSubscriber.swift in Sources */, 847449512891F3B00042FD80 /* WalletSwitchControl.swift in Sources */, AE3983A2272C08AE00BC8A85 /* BaseAccountImportPresenter.swift in Sources */, @@ -21915,6 +21949,7 @@ F4F22976260DBF3F00ACFDB8 /* StakingPayoutRewardTableCell.swift in Sources */, 8428229A289BC8E400163031 /* AddAccount+ParitySignerWelcomeWireframe.swift in Sources */, 8471577D2910F18300D7D003 /* GovernanceUnlockProtocols.swift in Sources */, + 0C500B1F2B04EA9100ABEE70 /* AssetConversionPallet+Event.swift in Sources */, 2CF2F93AF862CF54FC46B560 /* PurchaseInteractor.swift in Sources */, 0C9951D32AE2DB0200B65615 /* PromotionViewModelFactory.swift in Sources */, 77EFFC912A7276F1009E28F8 /* StakingTypeAccountView.swift in Sources */, @@ -23116,6 +23151,7 @@ 5B652F1E0040F68F835A2F1D /* AssetDetailsViewLayout.swift in Sources */, E0710E487509797C12110D83 /* AssetDetailsViewFactory.swift in Sources */, 58F385F41D42CC96373EDA42 /* TokensManageProtocols.swift in Sources */, + 0C500B242B0511F400ABEE70 /* ExtrinsicProcessor+CustomFee.swift in Sources */, CA3C4729115D875D0C80A3E8 /* TokensManageWireframe.swift in Sources */, 844C3E6B2A08C05A00C4305F /* DAppWalletAuthViewModelFactory.swift in Sources */, 88E5E2A7295D8FA1001B1D41 /* TitleIconViewModel+Hashable.swift in Sources */, diff --git a/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift b/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift index 7a8f1c807f..159ef165e1 100644 --- a/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift +++ b/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift @@ -135,7 +135,7 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { feeAssetId: feeAsset?.assetId, blockNumber: blockNumber, txIndex: nil, - callPath: CallCodingPath.swap(direction: .sell), + callPath: AssetConversionPallet.swapExactTokenForTokensPath, call: nil, swap: .init( amountIn: swap.amountIn, diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessing.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessing.swift similarity index 89% rename from novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessing.swift rename to novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessing.swift index 7fa2865359..3a063da2ad 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessing.swift +++ b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessing.swift @@ -2,18 +2,6 @@ import Foundation import SubstrateSdk import BigInt -struct ExtrinsicProcessingResult { - let sender: AccountId - let callPath: CallCodingPath - let call: JSON - let extrinsicHash: Data? - let fee: BigUInt? - let peerId: AccountId? - let amount: BigUInt? - let isSuccess: Bool - let assetId: UInt32 -} - protocol ExtrinsicProcessing { func process( extrinsicIndex: UInt32, @@ -84,6 +72,15 @@ extension ExtrinsicProcessor: ExtrinsicProcessing { return processingResult } + if let processingResult = matchAssetHubSwap( + extrinsicIndex: extrinsicIndex, + extrinsic: extrinsic, + eventRecords: eventRecords, + codingFactory: coderFactory + ) { + return processingResult + } + return matchExtrinsic( extrinsicIndex: extrinsicIndex, extrinsic: extrinsic, diff --git a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessingResult.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessingResult.swift new file mode 100644 index 0000000000..28ee3d1d03 --- /dev/null +++ b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessingResult.swift @@ -0,0 +1,24 @@ +import Foundation +import BigInt +import SubstrateSdk + +struct ExtrinsicProcessingResult { + struct Swap { + let assetIdIn: AssetModel.Id + let assetIdOut: AssetModel.Id + let amountIn: BigUInt + let amountOut: BigUInt + } + + let sender: AccountId + let callPath: CallCodingPath + let call: JSON + let extrinsicHash: Data? + let fee: BigUInt? + let feeAssetId: AssetModel.Id? + let peerId: AccountId? + let amount: BigUInt? + let isSuccess: Bool + let assetId: AssetModel.Id? + let swap: Swap? +} diff --git a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+CustomFee.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+CustomFee.swift new file mode 100644 index 0000000000..85e0424c8e --- /dev/null +++ b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+CustomFee.swift @@ -0,0 +1,25 @@ +import Foundation +import SubstrateSdk + +extension ExtrinsicProcessor { + func findFeeInCustomAsset( + in events: [EventRecord], + codingFactory: RuntimeCoderFactoryProtocol + ) throws -> AssetTxPaymentPallet.AssetTxFeePaid? { + let context = codingFactory.createRuntimeJsonContext() + let metadata = codingFactory.metadata + + let optFeeRecord = events.first { record in + guard let eventPath = metadata.createEventCodingPath(from: record.event) else { + return false + } + + return eventPath == AssetTxPaymentPallet.assetTxFeePaidEvent + } + + return try optFeeRecord?.event.params.map( + to: AssetTxPaymentPallet.AssetTxFeePaid.self, + with: context.toRawContext() + ) + } +} diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Events.swift similarity index 100% rename from novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift rename to novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Events.swift diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Fee.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Fee.swift similarity index 100% rename from novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Fee.swift rename to novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Fee.swift diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Matching.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Matching.swift similarity index 100% rename from novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Matching.swift rename to novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Matching.swift diff --git a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+SwapMatching.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+SwapMatching.swift new file mode 100644 index 0000000000..580de94a80 --- /dev/null +++ b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+SwapMatching.swift @@ -0,0 +1,289 @@ +import Foundation +import BigInt +import SubstrateSdk + +private struct SwapExtrinsicCallArgs { + let receiver: AccountId + let amountIn: BigUInt + let amountOut: BigUInt + let path: [AssetConversionPallet.AssetId] +} + +private struct SwapExtrinsicParsingResult { + let receiver: AccountId + let assetIdIn: UInt32 + let amountIn: BigUInt + let assetIdOut: UInt32 + let amountOut: BigUInt + let callPath: CallCodingPath + let call: JSON + let customFee: AssetTxPaymentPallet.AssetTxFeePaid? + let isSuccess: Bool +} + +extension ExtrinsicProcessor { + func matchAssetHubSwap( + extrinsicIndex: UInt32, + extrinsic: Extrinsic, + eventRecords: [EventRecord], + codingFactory: RuntimeCoderFactoryProtocol + ) -> ExtrinsicProcessingResult? { + do { + guard let swapResult = try parseAssetHubSwapExtrinsic( + extrinsic, + extrinsicIndex: extrinsicIndex, + eventRecords: eventRecords, + codingFactory: codingFactory + ) else { + return nil + } + + let fee: BigUInt + let feeAssetId: AssetModel.Id? + + let context = codingFactory.createRuntimeJsonContext() + + let maybeSender: AccountId? = try extrinsic.signature?.address.map( + to: MultiAddress.self, + with: context.toRawContext() + ).accountId + + guard let sender = maybeSender else { + return nil + } + + if let customFee = swapResult.customFee { + fee = customFee.actualFee + feeAssetId = customFee.assetId + } else { + let optNativeFee = findFee( + for: extrinsicIndex, + sender: sender, + eventRecords: eventRecords, + metadata: codingFactory.metadata, + runtimeJsonContext: context + ) + + guard let nativeFee = optNativeFee else { + return nil + } + + fee = nativeFee + feeAssetId = chain.utilityAsset()?.assetId + } + + return .init( + sender: sender, + callPath: swapResult.callPath, + call: swapResult.call, + extrinsicHash: nil, + fee: fee, + feeAssetId: feeAssetId, + peerId: swapResult.receiver, + amount: nil, + isSuccess: swapResult.isSuccess, + assetId: swapResult.assetIdIn, + swap: .init( + assetIdIn: swapResult.assetIdIn, + assetIdOut: swapResult.assetIdOut, + amountIn: swapResult.amountIn, + amountOut: swapResult.amountOut + ) + ) + + } catch { + return nil + } + } + + private func parseAssetHubSwapExtrinsic( + _ extrinsic: Extrinsic, + extrinsicIndex: UInt32, + eventRecords: [EventRecord], + codingFactory: RuntimeCoderFactoryProtocol + ) throws -> SwapExtrinsicParsingResult? { + let context = codingFactory.createRuntimeJsonContext() + + guard + let call = try? extrinsic.call.map(to: RuntimeCall.self, with: context.toRawContext()), + AssetConversionPallet.isSwap(.init(moduleName: call.moduleName, callName: call.callName)) else { + return nil + } + + let customFee = try findFeeInCustomAsset( + in: eventRecords, + codingFactory: codingFactory + ) + + guard + let isSuccess = matchStatus( + for: extrinsicIndex, + eventRecords: eventRecords, + metadata: codingFactory.metadata + ) else { + return nil + } + + if isSuccess { + return try findSuccessAssetHubSwapResult( + from: call, + eventRecords: eventRecords, + customFee: customFee, + codingFactory: codingFactory + ) + } else { + return try findFailedAssetHubSwapResult( + from: call, + customFee: customFee, + codingFactory: codingFactory + ) + } + } + + private func findSuccessAssetHubSwapResult( + from call: RuntimeCall, + eventRecords: [EventRecord], + customFee: AssetTxPaymentPallet.AssetTxFeePaid?, + codingFactory: RuntimeCoderFactoryProtocol + ) throws -> SwapExtrinsicParsingResult? { + let callPath = CallCodingPath(moduleName: call.moduleName, callName: call.callName) + + let context = codingFactory.createRuntimeJsonContext() + let metadata = codingFactory.metadata + + let swapEvents: [AssetConversionPallet.SwapExecutedEvent] = eventRecords.compactMap { record in + guard + let eventPath = metadata.createEventCodingPath(from: record.event), + AssetConversionPallet.swapExecutedEvent == eventPath else { + return nil + } + + return try? record.event.params.map( + to: AssetConversionPallet.SwapExecutedEvent.self, + with: context.toRawContext() + ) + } + + guard + let swap = try findSwap(swapEvents, customFee: customFee), + let remoteAssetIn = swap.path.first, + let remoteAssetOut = swap.path.last + else { + return nil + } + + let conversionClosure = AssetHubTokensConverter.createPoolAssetToLocalClosure( + for: chain, + codingFactory: codingFactory + ) + + guard + let assetIn = AssetHubTokensConverter.convertFromMultilocationToLocal( + remoteAssetIn, + chain: chain, + conversionClosure: conversionClosure + ), + let assetOut = AssetHubTokensConverter.convertFromMultilocationToLocal( + remoteAssetOut, + chain: chain, + conversionClosure: conversionClosure + ) else { + return nil + } + + return .init( + receiver: swap.sendTo, + assetIdIn: assetIn.chainAssetId.assetId, + amountIn: swap.amountIn, + assetIdOut: assetOut.chainAssetId.assetId, + amountOut: swap.amountOut, + callPath: callPath, + call: call.args, + customFee: customFee, + isSuccess: true + ) + } + + private func findFailedAssetHubSwapResult( + from call: RuntimeCall, + customFee: AssetTxPaymentPallet.AssetTxFeePaid?, + codingFactory: RuntimeCoderFactoryProtocol + ) throws -> SwapExtrinsicParsingResult? { + let callPath = CallCodingPath(moduleName: call.moduleName, callName: call.callName) + + let conversionClosure = AssetHubTokensConverter.createPoolAssetToLocalClosure( + for: chain, + codingFactory: codingFactory + ) + + let context = codingFactory.createRuntimeJsonContext() + let args: SwapExtrinsicCallArgs + + switch callPath { + case AssetConversionPallet.swapExactTokenForTokensPath: + let type = AssetConversionPallet.SwapExactTokensForTokensCall.self + let call = try call.args.map(to: type, with: context.toRawContext()) + + args = .init(receiver: call.sendTo, amountIn: call.amountIn, amountOut: call.amountOutMin, path: call.path) + + case AssetConversionPallet.swapTokenForExactTokens: + let type = AssetConversionPallet.SwapTokensForExactTokensCall.self + let call = try call.args.map(to: type, with: context.toRawContext()) + + args = .init(receiver: call.sendTo, amountIn: call.amountInMax, amountOut: call.amountOut, path: call.path) + default: + return nil + } + + guard + let remoteAssetIn = args.path.first, + let remoteAssetOut = args.path.last, + let assetIn = AssetHubTokensConverter.convertFromMultilocationToLocal( + remoteAssetIn, + chain: chain, + conversionClosure: conversionClosure + ), + let assetOut = AssetHubTokensConverter.convertFromMultilocationToLocal( + remoteAssetOut, + chain: chain, + conversionClosure: conversionClosure + ) else { + return nil + } + + return .init( + receiver: args.receiver, + assetIdIn: assetIn.asset.assetId, + amountIn: args.amountIn, + assetIdOut: assetOut.asset.assetId, + amountOut: args.amountOut, + callPath: callPath, + call: call.args, + customFee: customFee, + isSuccess: false + ) + } + + private func findSwap( + _ swapEvents: [AssetConversionPallet.SwapExecutedEvent], + customFee: AssetTxPaymentPallet.AssetTxFeePaid? + ) throws -> AssetConversionPallet.SwapExecutedEvent? { + guard let fee = customFee else { + return swapEvents.first + } + + let optFeeSwap = swapEvents.first + let swapsAfterFee = swapEvents.dropFirst() + + guard + let feeSwap = optFeeSwap, + let targetSwap = swapsAfterFee.first, + let feeAssetOut = feeSwap.path.last, + case .native = AssetHubTokensConverter.convertFromMultilocation(feeAssetOut, chain: chain), + feeSwap.amountIn <= fee.actualFee else { + return nil + } + + return targetSwap + } +} diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/TransactionSubscription.swift b/novawallet/Common/Services/TransactionSubscription/TransactionSubscription.swift similarity index 100% rename from novawallet/Common/Services/WebSocketService/StorageSubscription/TransactionSubscription.swift rename to novawallet/Common/Services/TransactionSubscription/TransactionSubscription.swift diff --git a/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Call.swift b/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Call.swift index 58ce2ad652..91751e6926 100644 --- a/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Call.swift +++ b/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Call.swift @@ -3,6 +3,21 @@ import SubstrateSdk import BigInt extension AssetConversionPallet { + static var swapExactTokenForTokensPath: CallCodingPath { + CallCodingPath(moduleName: AssetConversionPallet.name, callName: "swap_exact_tokens_for_tokens") + } + + static var swapTokenForExactTokens: CallCodingPath { + CallCodingPath(moduleName: AssetConversionPallet.name, callName: "swap_tokens_for_exact_tokens") + } + + static func isSwap(_ callPath: CallCodingPath) -> Bool { + [ + AssetConversionPallet.swapExactTokenForTokensPath, + AssetConversionPallet.swapTokenForExactTokens + ].contains(callPath) + } + struct SwapExactTokensForTokensCall: Codable { enum CodingKeys: String, CodingKey { case path diff --git a/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Event.swift b/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Event.swift new file mode 100644 index 0000000000..a920f7b4cd --- /dev/null +++ b/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Event.swift @@ -0,0 +1,17 @@ +import Foundation +import BigInt +import SubstrateSdk + +extension AssetConversionPallet { + static var swapExecutedEvent: EventCodingPath { + .init(moduleName: AssetConversionPallet.name, eventName: "SwapExecuted") + } + + struct SwapExecutedEvent: Codable { + @BytesCodable var who: AccountId + @BytesCodable var sendTo: AccountId + let path: [AssetId] + @StringCodable var amountIn: BigUInt + @StringCodable var amountOut: BigUInt + } +} diff --git a/novawallet/Common/Substrate/Types/AssetTxPaymentPallet/AssetTxPaymentPallet.swift b/novawallet/Common/Substrate/Types/AssetTxPaymentPallet/AssetTxPaymentPallet.swift new file mode 100644 index 0000000000..1a6cf9f1de --- /dev/null +++ b/novawallet/Common/Substrate/Types/AssetTxPaymentPallet/AssetTxPaymentPallet.swift @@ -0,0 +1,16 @@ +import Foundation +import BigInt +import SubstrateSdk + +enum AssetTxPaymentPallet { + static let name = "AssetTxPayment" + + static var assetTxFeePaidEvent: EventCodingPath { + .init(moduleName: Self.name, eventName: "AssetTxFeePaid") + } + + struct AssetTxFeePaid: Codable { + @StringCodable var actualFee: BigUInt + let assetId: JSON + } +} diff --git a/novawallet/Common/Substrate/Types/CallCodingPath.swift b/novawallet/Common/Substrate/Types/CallCodingPath.swift index d812aba86c..23728787e2 100644 --- a/novawallet/Common/Substrate/Types/CallCodingPath.swift +++ b/novawallet/Common/Substrate/Types/CallCodingPath.swift @@ -98,15 +98,6 @@ extension CallCodingPath { static var ethereumTransact: CallCodingPath { CallCodingPath(moduleName: "Ethereum", callName: "transact") } - - static func swap(direction: AssetConversion.Direction) -> CallCodingPath { - switch direction { - case .sell: - return CallCodingPath(moduleName: AssetConversionPallet.name, callName: "swap_exact_tokens_for_tokens") - case .buy: - return CallCodingPath(moduleName: AssetConversionPallet.name, callName: "swap_tokens_for_exact_tokens") - } - } } // MARK: Syntetic keys diff --git a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift index 9cda1f9ac4..4ddd9f99e8 100644 --- a/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift +++ b/novawallet/Modules/AssetConversion/Service/AssetHub/AssetHubTokensConverter.swift @@ -55,6 +55,16 @@ enum AssetHubTokensConverter { } } + static func convertFromMultilocationToLocal( + _ assetId: AssetConversionPallet.AssetId, + chain: ChainModel, + conversionClosure: (AssetConversionPallet.PoolAsset) -> ChainAsset? + ) -> ChainAsset? { + let poolAsset = convertFromMultilocation(assetId, chain: chain) + + return conversionClosure(poolAsset) + } + static func convertToMultilocation( chainAsset: ChainAsset, codingFactory: RuntimeCoderFactoryProtocol @@ -100,4 +110,55 @@ enum AssetHubTokensConverter { return nil } } + + static func createPoolAssetToLocalClosure( + for chain: ChainModel, + codingFactory: RuntimeCoderFactoryProtocol + ) -> (AssetConversionPallet.PoolAsset) -> ChainAsset? { + let initAssetsStore = [JSON: (AssetModel, AssetsPalletStorageInfo)]() + let assetsPalletTokens = chain.assets.reduce(into: initAssetsStore) { store, asset in + let optStorageInfo = try? AssetStorageInfo.extract(from: asset, codingFactory: codingFactory) + guard case let .statemine(info) = optStorageInfo else { + return + } + + store[info.assetId] = (asset, info) + } + + return { remoteAsset in + switch remoteAsset { + case .native: + return chain.utilityChainAsset() + case let .assets(pallet, index): + guard let localToken = assetsPalletTokens[.stringValue(String(index))] else { + return nil + } + + let palletName = localToken.1.palletName ?? PalletAssets.name + + guard + let moduleIndex = codingFactory.metadata.getModuleIndex(palletName), + moduleIndex == pallet else { + // only Assets pallet currently supported + return nil + } + + return chain.asset(for: localToken.0.assetId).map { asset in + ChainAsset(chain: chain, asset: asset) + } + case let .foreign(remoteId): + guard + let json = try? remoteId.toScaleCompatibleJSON(), + let localToken = assetsPalletTokens[json] else { + return nil + } + + return chain.asset(for: localToken.0.assetId).map { asset in + ChainAsset(chain: chain, asset: asset) + } + default: + return nil + } + } + } } From fc9ee26810a5a342eab0248970dae3fbac07c08c Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 17 Nov 2023 12:49:19 +0100 Subject: [PATCH 188/204] complete subscription data parsing --- .../TransactionHistoryItem+Subscription.swift | 17 +++++++++++-- .../ExtrinsicProcessor+Matching.swift | 24 ++++++++++++++----- .../ExtrinsicProcessor+SwapMatching.swift | 17 +++++++++++-- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/novawallet/Common/Extension/Model/TransactionHistoryItem+Subscription.swift b/novawallet/Common/Extension/Model/TransactionHistoryItem+Subscription.swift index 36797d6a85..f43fb73d81 100644 --- a/novawallet/Common/Extension/Model/TransactionHistoryItem+Subscription.swift +++ b/novawallet/Common/Extension/Model/TransactionHistoryItem+Subscription.swift @@ -55,16 +55,29 @@ extension TransactionHistoryItem { txHash: txHash, timestamp: timestamp, fee: maybeFee, - feeAssetId: nil, + feeAssetId: extrinsic.feeAssetId, blockNumber: result.blockNumber, txIndex: result.txIndex, callPath: result.processingResult.callPath, call: encodedCall, - swap: nil + swap: createSwapIfNeeded(from: extrinsic) ) } catch { return nil } } + + private static func createSwapIfNeeded(from subscription: ExtrinsicProcessingResult) -> SwapHistoryData? { + guard let remoteSwap = subscription.swap else { + return nil + } + + return .init( + amountIn: String(remoteSwap.amountIn), + assetIdIn: remoteSwap.assetIdIn, + amountOut: String(remoteSwap.amountOut), + assetIdOut: remoteSwap.assetIdOut + ) + } } diff --git a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Matching.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Matching.swift index d3ff1b3613..554a8e2464 100644 --- a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Matching.swift +++ b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+Matching.swift @@ -100,10 +100,12 @@ extension ExtrinsicProcessor { call: extrinsic.call, extrinsicHash: nil, fee: fee, + feeAssetId: nil, peerId: peerId, amount: result.callAmount, isSuccess: status, - assetId: asset.assetId + assetId: asset.assetId, + swap: nil ) } catch { @@ -225,10 +227,12 @@ extension ExtrinsicProcessor { call: extrinsic.call, extrinsicHash: executedValue.transactionHash, fee: fee, + feeAssetId: nil, peerId: executedValue.to, amount: nil, isSuccess: executedValue.isSuccess, - assetId: assetId + assetId: assetId, + swap: nil ) } catch { return nil @@ -281,10 +285,12 @@ extension ExtrinsicProcessor { call: extrinsic.call, extrinsicHash: nil, fee: fee, + feeAssetId: nil, peerId: nil, amount: nil, isSuccess: isSuccess, - assetId: assetId + assetId: assetId, + swap: nil ) } catch { @@ -359,10 +365,12 @@ extension ExtrinsicProcessor { call: extrinsic.call, extrinsicHash: nil, fee: fee, + feeAssetId: nil, peerId: peerId, amount: result.callAmount, isSuccess: isSuccess, - assetId: asset.assetId + assetId: asset.assetId, + swap: nil ) } catch { @@ -441,10 +449,12 @@ extension ExtrinsicProcessor { call: extrinsic.call, extrinsicHash: nil, fee: fee, + feeAssetId: nil, peerId: peerId, amount: result.callAmount, isSuccess: isSuccess, - assetId: assetId + assetId: assetId, + swap: nil ) } catch { @@ -580,10 +590,12 @@ extension ExtrinsicProcessor { call: extrinsic.call, extrinsicHash: nil, fee: fee, + feeAssetId: nil, peerId: peerId, amount: call.args.value, isSuccess: status, - assetId: assetId + assetId: assetId, + swap: nil ) } catch { diff --git a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+SwapMatching.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+SwapMatching.swift index 580de94a80..83ceb5e622 100644 --- a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+SwapMatching.swift +++ b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+SwapMatching.swift @@ -52,9 +52,22 @@ extension ExtrinsicProcessor { return nil } - if let customFee = swapResult.customFee { + if + let customFee = swapResult.customFee, + let remoteAssetId = try? customFee.assetId.map( + to: AssetConversionPallet.AssetId.self, + with: context.toRawContext() + ), + let localAsset = AssetHubTokensConverter.convertFromMultilocationToLocal( + remoteAssetId, + chain: chain, + conversionClosure: AssetHubTokensConverter.createPoolAssetToLocalClosure( + for: chain, + codingFactory: codingFactory + ) + ) { fee = customFee.actualFee - feeAssetId = customFee.assetId + feeAssetId = localAsset.asset.assetId } else { let optNativeFee = findFee( for: extrinsicIndex, From 139d76efa4aab1b973cce04566b1028580d8da85 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sat, 18 Nov 2023 07:23:14 +0100 Subject: [PATCH 189/204] persist swap after transaction --- .../PersistExtrinsicFactory.swift | 49 +++++++++++++ .../PersistTransferDetails.swift | 13 ++++ .../PersistentExtrinsicService.swift | 35 ++++++++++ .../AssetConversionPallet+Call.swift | 9 +++ .../Swaps/Confirm/SwapConfirmInteractor.swift | 69 +++++++++++++++++-- .../Swaps/Confirm/SwapConfirmPresenter.swift | 2 +- .../Swaps/Confirm/SwapConfirmProtocols.swift | 3 +- .../Confirm/SwapConfirmViewFactory.swift | 8 +++ 8 files changed, 180 insertions(+), 8 deletions(-) diff --git a/novawallet/Common/Services/PersistExtrinsicService/PersistExtrinsicFactory.swift b/novawallet/Common/Services/PersistExtrinsicService/PersistExtrinsicFactory.swift index f6f84d0ac0..3de08e0255 100644 --- a/novawallet/Common/Services/PersistExtrinsicService/PersistExtrinsicFactory.swift +++ b/novawallet/Common/Services/PersistExtrinsicService/PersistExtrinsicFactory.swift @@ -14,6 +14,12 @@ protocol PersistExtrinsicFactoryProtocol { chainAssetId: ChainAssetId, details: PersistExtrinsicDetails ) -> CompoundOperationWrapper + + func createSwapSaveOperation( + source: TransactionHistoryItemSource, + chainAssetId: ChainAssetId, + details: PersistSwapDetails + ) -> CompoundOperationWrapper } final class PersistExtrinsicFactory: PersistExtrinsicFactoryProtocol { @@ -94,4 +100,47 @@ final class PersistExtrinsicFactory: PersistExtrinsicFactoryProtocol { return CompoundOperationWrapper(targetOperation: operation) } + + func createSwapSaveOperation( + source: TransactionHistoryItemSource, + chainAssetId: ChainAssetId, + details: PersistSwapDetails + ) -> CompoundOperationWrapper { + let timestamp = Int64(Date().timeIntervalSince1970) + let feeString = details.fee.map { String($0) } + + let txHash = details.txHash.toHex(includePrefix: true) + let identifier = TransactionHistoryItem.createIdentifier(from: txHash, source: source) + + let swap = SwapHistoryData( + amountIn: String(details.amountIn), + assetIdIn: details.assetIdIn.assetId, + amountOut: String(details.amountOut), + assetIdOut: details.assetIdOut.assetId + ) + + let item = TransactionHistoryItem( + identifier: identifier, + source: source, + chainId: chainAssetId.chainId, + assetId: chainAssetId.assetId, + sender: details.sender, + receiver: details.receive, + amountInPlank: nil, + status: .pending, + txHash: txHash, + timestamp: timestamp, + fee: feeString, + feeAssetId: details.feeAssetId, + blockNumber: nil, + txIndex: nil, + callPath: details.callPath, + call: nil, + swap: swap + ) + + let operation = repository.saveOperation({ [item] }, { [] }) + + return CompoundOperationWrapper(targetOperation: operation) + } } diff --git a/novawallet/Common/Services/PersistExtrinsicService/PersistTransferDetails.swift b/novawallet/Common/Services/PersistExtrinsicService/PersistTransferDetails.swift index f0177103f1..0673d955e3 100644 --- a/novawallet/Common/Services/PersistExtrinsicService/PersistTransferDetails.swift +++ b/novawallet/Common/Services/PersistExtrinsicService/PersistTransferDetails.swift @@ -16,3 +16,16 @@ struct PersistExtrinsicDetails { let callPath: CallCodingPath let fee: BigUInt? } + +struct PersistSwapDetails { + let txHash: Data + let sender: AccountAddress + let receive: AccountAddress + let assetIdIn: ChainAssetId + let amountIn: BigUInt + let assetIdOut: ChainAssetId + let amountOut: BigUInt + let fee: BigUInt? + let feeAssetId: AssetModel.Id + let callPath: CallCodingPath +} diff --git a/novawallet/Common/Services/PersistExtrinsicService/PersistentExtrinsicService.swift b/novawallet/Common/Services/PersistExtrinsicService/PersistentExtrinsicService.swift index 6f65428210..2571387177 100644 --- a/novawallet/Common/Services/PersistExtrinsicService/PersistentExtrinsicService.swift +++ b/novawallet/Common/Services/PersistExtrinsicService/PersistentExtrinsicService.swift @@ -17,6 +17,14 @@ protocol PersistentExtrinsicServiceProtocol { runningIn queue: DispatchQueue, completion closure: @escaping (Result) -> Void ) + + func saveSwap( + source: TransactionHistoryItemSource, + chainAssetId: ChainAssetId, + details: PersistSwapDetails, + runningIn queue: DispatchQueue, + completion closure: @escaping (Result) -> Void + ) } final class PersistentExtrinsicService { @@ -86,4 +94,31 @@ extension PersistentExtrinsicService: PersistentExtrinsicServiceProtocol { operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) } + + func saveSwap( + source: TransactionHistoryItemSource, + chainAssetId: ChainAssetId, + details: PersistSwapDetails, + runningIn queue: DispatchQueue, + completion closure: @escaping (Result) -> Void + ) { + let wrapper = factory.createSwapSaveOperation( + source: source, + chainAssetId: chainAssetId, + details: details + ) + + wrapper.targetOperation.completionBlock = { + queue.async { + do { + try wrapper.targetOperation.extractNoCancellableResultData() + closure(.success(())) + } catch { + closure(.failure(error)) + } + } + } + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } } diff --git a/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Call.swift b/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Call.swift index 91751e6926..821e901360 100644 --- a/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Call.swift +++ b/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Call.swift @@ -18,6 +18,15 @@ extension AssetConversionPallet { ].contains(callPath) } + static func callPath(for direction: AssetConversion.Direction) -> CallCodingPath { + switch direction { + case .sell: + return AssetConversionPallet.swapExactTokenForTokensPath + case .buy: + return AssetConversionPallet.swapTokenForExactTokens + } + } + struct SwapExactTokensForTokensCall: Codable { enum CodingKeys: String, CodingKey { case path diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift index 1c82b582e7..afe7579698 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift @@ -1,11 +1,15 @@ import UIKit import RobinHood +import IrohaCrypto +import BigInt final class SwapConfirmInteractor: SwapBaseInteractor { var presenter: SwapConfirmInteractorOutputProtocol? { basePresenter as? SwapConfirmInteractorOutputProtocol } + let persistExtrinsicService: PersistentExtrinsicServiceProtocol + let eventCenter: EventCenterProtocol let initState: SwapConfirmInitState let runtimeService: RuntimeProviderProtocol let extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol @@ -24,6 +28,8 @@ final class SwapConfirmInteractor: SwapBaseInteractor { priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, generalLocalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol, + persistExtrinsicService: PersistentExtrinsicServiceProtocol, + eventCenter: EventCenterProtocol, currencyManager: CurrencyManagerProtocol, selectedWallet: MetaAccountModel, operationQueue: OperationQueue, @@ -34,6 +40,8 @@ final class SwapConfirmInteractor: SwapBaseInteractor { self.runtimeService = runtimeService self.extrinsicServiceFactory = extrinsicServiceFactory self.assetConversionExtrinsicService = assetConversionExtrinsicService + self.persistExtrinsicService = persistExtrinsicService + self.eventCenter = eventCenter super.init( assetConversionAggregator: assetConversionAggregator, @@ -49,6 +57,44 @@ final class SwapConfirmInteractor: SwapBaseInteractor { ) } + private func persistSwapAndComplete(txHash: String, args: AssetConversion.CallArgs, lastFee: BigUInt?) { + do { + let chainIn = initState.chainAssetIn.chain + + guard let sender = selectedWallet.fetch(for: chainIn.accountRequest())?.toAddress() else { + throw ChainAccountFetchingError.accountNotExists + } + + let receiver = try args.receiver.toAddress(using: initState.chainAssetOut.chain.chainFormat) + + let details = PersistSwapDetails( + txHash: try Data(hexString: txHash), + sender: sender, + receive: receiver, + assetIdIn: args.assetIn, + amountIn: args.amountIn, + assetIdOut: args.assetOut, + amountOut: args.amountOut, + fee: lastFee, + feeAssetId: initState.feeChainAsset.asset.assetId, + callPath: AssetConversionPallet.callPath(for: args.direction) + ) + + persistExtrinsicService.saveSwap( + source: .substrate, + chainAssetId: details.assetIdIn, + details: details, + runningIn: .main + ) { [weak self] _ in + self?.eventCenter.notify(with: WalletTransactionListUpdated()) + self?.presenter?.didReceiveConfirmation(hash: txHash) + } + } catch { + // complete successfully as we don't want a user to think tx is failed + presenter?.didReceiveConfirmation(hash: txHash) + } + } + override func setup() { super.setup() @@ -57,7 +103,7 @@ final class SwapConfirmInteractor: SwapBaseInteractor { set(feeChainAsset: initState.feeChainAsset) } - func submitExtrinsic(args: AssetConversion.CallArgs) { + func submitExtrinsic(args: AssetConversion.CallArgs, lastFee: BigUInt?) { let runtimeCoderFactoryOperation = runtimeService.fetchCoderFactoryOperation() runtimeCoderFactoryOperation.completionBlock = { [weak self] in @@ -71,7 +117,12 @@ final class SwapConfirmInteractor: SwapBaseInteractor { for: args, codingFactory: runtimeCoderFactory ) - try self.submitClosure(builder: builder, runtimeCoderFactory: runtimeCoderFactory) + try self.submitClosure( + builder: builder, + runtimeCoderFactory: runtimeCoderFactory, + args: args, + lastFee: lastFee + ) } catch { self.presenter?.didReceive(error: .submit(error)) } @@ -83,7 +134,9 @@ final class SwapConfirmInteractor: SwapBaseInteractor { private func submitClosure( builder: @escaping ExtrinsicBuilderClosure, - runtimeCoderFactory: RuntimeCoderFactoryProtocol + runtimeCoderFactory: RuntimeCoderFactoryProtocol, + args: AssetConversion.CallArgs, + lastFee: BigUInt? ) throws { let extrinsicService: ExtrinsicServiceProtocol @@ -118,7 +171,11 @@ final class SwapConfirmInteractor: SwapBaseInteractor { ) { [weak self] result in switch result { case let .success(hash): - self?.presenter?.didReceiveConfirmation(hash: hash) + self?.persistSwapAndComplete( + txHash: hash, + args: args, + lastFee: lastFee + ) case let .failure(error): self?.presenter?.didReceive(error: .submit(error)) } @@ -127,7 +184,7 @@ final class SwapConfirmInteractor: SwapBaseInteractor { } extension SwapConfirmInteractor: SwapConfirmInteractorInputProtocol { - func submit(args: AssetConversion.CallArgs) { - submitExtrinsic(args: args) + func submit(args: AssetConversion.CallArgs, lastFee: BigUInt?) { + submitExtrinsic(args: args, lastFee: lastFee) } } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 9705e0e96d..e359e0115c 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -328,7 +328,7 @@ extension SwapConfirmPresenter { view?.didReceiveStartLoading() - interactor.submit(args: args) + interactor.submit(args: args, lastFee: fee?.networkFee.targetAmount) } } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift index 00fdf43bcc..566a38e840 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift @@ -1,4 +1,5 @@ import Foundation +import BigInt protocol SwapConfirmViewProtocol: ControllerBackedProtocol { func didReceiveAssetIn(viewModel: SwapAssetAmountViewModel) @@ -25,7 +26,7 @@ protocol SwapConfirmPresenterProtocol: AnyObject { } protocol SwapConfirmInteractorInputProtocol: SwapBaseInteractorInputProtocol { - func submit(args: AssetConversion.CallArgs) + func submit(args: AssetConversion.CallArgs, lastFee: BigUInt?) } protocol SwapConfirmInteractorOutputProtocol: SwapBaseInteractorOutputProtocol { diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift index a457bb6df7..af8af10aa8 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewFactory.swift @@ -108,6 +108,12 @@ struct SwapConfirmViewFactory { operationQueue: operationQueue ) + let transactionStorage = SubstrateRepositoryFactory().createTxRepository() + let persistExtrinsicService = PersistentExtrinsicService( + repository: transactionStorage, + operationQueue: OperationManagerFacade.sharedDefaultQueue + ) + let interactor = SwapConfirmInteractor( initState: initState, assetConversionFeeService: feeService, @@ -120,6 +126,8 @@ struct SwapConfirmViewFactory { priceLocalSubscriptionFactory: PriceProviderFactory.shared, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, generalLocalSubscriptionFactory: generalSubscriptonFactory, + persistExtrinsicService: persistExtrinsicService, + eventCenter: EventCenter.shared, currencyManager: currencyManager, selectedWallet: wallet, operationQueue: operationQueue, From 5d688d4f286be6e9e2e067c6d395d944c4d2bd65 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sat, 18 Nov 2023 10:26:40 +0100 Subject: [PATCH 190/204] fix fee parsing --- .../ExtrinsicProcessor+SwapMatching.swift | 7 +++---- .../AssetConversionPallet+Event.swift | 18 ++++++++++++++---- .../AssetTxPaymentPallet.swift | 13 ++++++++++++- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+SwapMatching.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+SwapMatching.swift index 83ceb5e622..124fe09773 100644 --- a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+SwapMatching.swift +++ b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+SwapMatching.swift @@ -280,8 +280,8 @@ extension ExtrinsicProcessor { private func findSwap( _ swapEvents: [AssetConversionPallet.SwapExecutedEvent], customFee: AssetTxPaymentPallet.AssetTxFeePaid? - ) throws -> AssetConversionPallet.SwapExecutedEvent? { - guard let fee = customFee else { + ) -> AssetConversionPallet.SwapExecutedEvent? { + guard customFee != nil else { return swapEvents.first } @@ -292,8 +292,7 @@ extension ExtrinsicProcessor { let feeSwap = optFeeSwap, let targetSwap = swapsAfterFee.first, let feeAssetOut = feeSwap.path.last, - case .native = AssetHubTokensConverter.convertFromMultilocation(feeAssetOut, chain: chain), - feeSwap.amountIn <= fee.actualFee else { + case .native = AssetHubTokensConverter.convertFromMultilocation(feeAssetOut, chain: chain) else { return nil } diff --git a/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Event.swift b/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Event.swift index a920f7b4cd..c915c35e94 100644 --- a/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Event.swift +++ b/novawallet/Common/Substrate/Calls/AssetConversionPallet/AssetConversionPallet+Event.swift @@ -8,10 +8,20 @@ extension AssetConversionPallet { } struct SwapExecutedEvent: Codable { - @BytesCodable var who: AccountId - @BytesCodable var sendTo: AccountId + let who: AccountId + let sendTo: AccountId let path: [AssetId] - @StringCodable var amountIn: BigUInt - @StringCodable var amountOut: BigUInt + let amountIn: BigUInt + let amountOut: BigUInt + + init(from decoder: Decoder) throws { + var unkeyedContainer = try decoder.unkeyedContainer() + + who = try unkeyedContainer.decode(BytesCodable.self).wrappedValue + sendTo = try unkeyedContainer.decode(BytesCodable.self).wrappedValue + path = try unkeyedContainer.decode([AssetId].self) + amountIn = try unkeyedContainer.decode(StringScaleMapper.self).value + amountOut = try unkeyedContainer.decode(StringScaleMapper.self).value + } } } diff --git a/novawallet/Common/Substrate/Types/AssetTxPaymentPallet/AssetTxPaymentPallet.swift b/novawallet/Common/Substrate/Types/AssetTxPaymentPallet/AssetTxPaymentPallet.swift index 1a6cf9f1de..ba639e5213 100644 --- a/novawallet/Common/Substrate/Types/AssetTxPaymentPallet/AssetTxPaymentPallet.swift +++ b/novawallet/Common/Substrate/Types/AssetTxPaymentPallet/AssetTxPaymentPallet.swift @@ -10,7 +10,18 @@ enum AssetTxPaymentPallet { } struct AssetTxFeePaid: Codable { - @StringCodable var actualFee: BigUInt + let who: AccountId + let tip: BigUInt + let actualFee: BigUInt let assetId: JSON + + init(from decoder: Decoder) throws { + var unkeyedContainer = try decoder.unkeyedContainer() + + who = try unkeyedContainer.decode(BytesCodable.self).wrappedValue + actualFee = try unkeyedContainer.decode(StringScaleMapper.self).value + tip = try unkeyedContainer.decode(StringScaleMapper.self).value + assetId = try unkeyedContainer.decode(JSON.self) + } } } From b1195dc2fde8385ced2d23acef5d8a1df7859581 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sun, 19 Nov 2023 07:00:10 +0100 Subject: [PATCH 191/204] refactoring --- .../ExtrinsicProcessor+SwapMatching.swift | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+SwapMatching.swift b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+SwapMatching.swift index 124fe09773..3ba1bb97fd 100644 --- a/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+SwapMatching.swift +++ b/novawallet/Common/Services/TransactionSubscription/ExtrinsicProcessor+SwapMatching.swift @@ -171,14 +171,12 @@ extension ExtrinsicProcessor { return nil } - return try? record.event.params.map( - to: AssetConversionPallet.SwapExecutedEvent.self, - with: context.toRawContext() - ) + let type = AssetConversionPallet.SwapExecutedEvent.self + return try? record.event.params.map(to: type, with: context.toRawContext()) } guard - let swap = try findSwap(swapEvents, customFee: customFee), + let swap = findSwap(swapEvents, customFee: customFee), let remoteAssetIn = swap.path.first, let remoteAssetOut = swap.path.last else { From b908fdc903dbe6388a0ab373e9034611d6532def Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 20 Nov 2023 06:18:19 +0100 Subject: [PATCH 192/204] take into account ed --- novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 48d3bd91b6..493d6613a6 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -609,16 +609,18 @@ extension SwapSetupPresenter { guard !isManualFeeSet, let payChainAsset = getPayChainAsset(), + !payChainAsset.isUtilityAsset, let feeChainAsset = getFeeChainAsset(), - feeChainAsset.chainAssetId == payChainAsset.chain.utilityChainAssetId(), + feeChainAsset.isUtilityAsset, let feeAssetBalance = feeAssetBalance, let payAssetBalance = payAssetBalance, payAssetBalance.transferable > 0, - let fee = fee?.totalFee.nativeAmount else { + let fee = fee?.totalFee.nativeAmount, + let nativeMinBalance = utilityAssetBalanceExistense?.minBalance else { return } - if feeAssetBalance.transferable < fee { + if feeAssetBalance.freeInPlank < fee + nativeMinBalance { updateFeeChainAsset(payChainAsset) } } From 26ae0896753281bb3e3f866c5c626b8327156832 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 20 Nov 2023 07:23:38 +0100 Subject: [PATCH 193/204] fix fee calculation on flip --- novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 91d6c7acfa..25bed7a2bf 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -726,10 +726,12 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { let receiveAmount = receiveAmountInput.map { AmountInputResult.absolute($0) } Swift.swap(&payChainAsset, &receiveChainAsset) + feeChainAsset = payChainAsset?.chain.utilityChainAsset() canPayFeeInPayAsset = false interactor.update(payChainAsset: payChainAsset) interactor.update(receiveChainAsset: receiveChainAsset) + interactor.update(feeChainAsset: feeChainAsset) let newFocus: TextFieldFocus? switch currentFocus { From 0725c1296890d555106185814f0dad91be820851 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 20 Nov 2023 07:30:16 +0100 Subject: [PATCH 194/204] fix review comments --- .../PersistExtrinsicFactory.swift | 2 +- .../PersistTransferDetails.swift | 2 +- .../Swaps/Confirm/SwapConfirmInteractor.swift | 2 +- novawalletTests/Mocks/ModuleMocks.swift | 80 +++++++++---------- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/novawallet/Common/Services/PersistExtrinsicService/PersistExtrinsicFactory.swift b/novawallet/Common/Services/PersistExtrinsicService/PersistExtrinsicFactory.swift index 3de08e0255..a845c1d80c 100644 --- a/novawallet/Common/Services/PersistExtrinsicService/PersistExtrinsicFactory.swift +++ b/novawallet/Common/Services/PersistExtrinsicService/PersistExtrinsicFactory.swift @@ -125,7 +125,7 @@ final class PersistExtrinsicFactory: PersistExtrinsicFactoryProtocol { chainId: chainAssetId.chainId, assetId: chainAssetId.assetId, sender: details.sender, - receiver: details.receive, + receiver: details.receiver, amountInPlank: nil, status: .pending, txHash: txHash, diff --git a/novawallet/Common/Services/PersistExtrinsicService/PersistTransferDetails.swift b/novawallet/Common/Services/PersistExtrinsicService/PersistTransferDetails.swift index 0673d955e3..558fd1a0b5 100644 --- a/novawallet/Common/Services/PersistExtrinsicService/PersistTransferDetails.swift +++ b/novawallet/Common/Services/PersistExtrinsicService/PersistTransferDetails.swift @@ -20,7 +20,7 @@ struct PersistExtrinsicDetails { struct PersistSwapDetails { let txHash: Data let sender: AccountAddress - let receive: AccountAddress + let receiver: AccountAddress let assetIdIn: ChainAssetId let amountIn: BigUInt let assetIdOut: ChainAssetId diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift index afe7579698..47c1f1c98e 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift @@ -70,7 +70,7 @@ final class SwapConfirmInteractor: SwapBaseInteractor { let details = PersistSwapDetails( txHash: try Data(hexString: txHash), sender: sender, - receive: receiver, + receiver: receiver, assetIdIn: args.assetIn, amountIn: args.amountIn, assetIdOut: args.assetOut, diff --git a/novawalletTests/Mocks/ModuleMocks.swift b/novawalletTests/Mocks/ModuleMocks.swift index 494ddeb062..4f02b59996 100644 --- a/novawalletTests/Mocks/ModuleMocks.swift +++ b/novawalletTests/Mocks/ModuleMocks.swift @@ -17472,16 +17472,16 @@ import Cuckoo - func send() { + func repeatOperation() { - return cuckoo_manager.call("send()", + return cuckoo_manager.call("repeatOperation()", parameters: (), escapingParameters: (), superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall() , - defaultCall: __defaultImplStub!.send()) + defaultCall: __defaultImplStub!.repeatOperation()) } @@ -17515,21 +17515,6 @@ import Cuckoo } - - - func repeatOperation() { - - return cuckoo_manager.call("repeatOperation()", - parameters: (), - escapingParameters: (), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.repeatOperation()) - - } - struct __StubbingProxy_OperationDetailsPresenterProtocol: Cuckoo.StubbingProxy { private let cuckoo_manager: Cuckoo.MockManager @@ -17559,9 +17544,9 @@ import Cuckoo return .init(stub: cuckoo_manager.createStub(for: MockOperationDetailsPresenterProtocol.self, method: "showOperationActions()", parameterMatchers: matchers)) } - func send() -> Cuckoo.ProtocolStubNoReturnFunction<()> { + func repeatOperation() -> Cuckoo.ProtocolStubNoReturnFunction<()> { let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockOperationDetailsPresenterProtocol.self, method: "send()", parameterMatchers: matchers)) + return .init(stub: cuckoo_manager.createStub(for: MockOperationDetailsPresenterProtocol.self, method: "repeatOperation()", parameterMatchers: matchers)) } func showRateInfo() -> Cuckoo.ProtocolStubNoReturnFunction<()> { @@ -17574,11 +17559,6 @@ import Cuckoo return .init(stub: cuckoo_manager.createStub(for: MockOperationDetailsPresenterProtocol.self, method: "showNetworkFeeInfo()", parameterMatchers: matchers)) } - func repeatOperation() -> Cuckoo.ProtocolStubNoReturnFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockOperationDetailsPresenterProtocol.self, method: "repeatOperation()", parameterMatchers: matchers)) - } - } struct __VerificationProxy_OperationDetailsPresenterProtocol: Cuckoo.VerificationProxy { @@ -17620,9 +17600,9 @@ import Cuckoo } @discardableResult - func send() -> Cuckoo.__DoNotUse<(), Void> { + func repeatOperation() -> Cuckoo.__DoNotUse<(), Void> { let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify("send()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + return cuckoo_manager.verify("repeatOperation()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -17637,12 +17617,6 @@ import Cuckoo return cuckoo_manager.verify("showNetworkFeeInfo()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } - @discardableResult - func repeatOperation() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify("repeatOperation()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - } } @@ -17678,7 +17652,7 @@ import Cuckoo - func send() { + func repeatOperation() { return DefaultValueRegistry.defaultValue(for: (Void).self) } @@ -17694,12 +17668,6 @@ import Cuckoo return DefaultValueRegistry.defaultValue(for: (Void).self) } - - - func repeatOperation() { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - } @@ -17930,6 +17898,21 @@ import Cuckoo + func showSwapSetup(from: OperationDetailsViewProtocol?, state: SwapSetupInitState) { + + return cuckoo_manager.call("showSwapSetup(from: OperationDetailsViewProtocol?, state: SwapSetupInitState)", + parameters: (from, state), + escapingParameters: (from, state), + superclassCall: + + Cuckoo.MockManager.crashOnProtocolSuperclassCall() + , + defaultCall: __defaultImplStub!.showSwapSetup(from: from, state: state)) + + } + + + func present(message: String?, title: String?, closeAction: String?, from view: ControllerBackedProtocol?) { return cuckoo_manager.call("present(message: String?, title: String?, closeAction: String?, from: ControllerBackedProtocol?)", @@ -17972,6 +17955,11 @@ import Cuckoo return .init(stub: cuckoo_manager.createStub(for: MockOperationDetailsWireframeProtocol.self, method: "showSend(from: OperationDetailsViewProtocol?, displayAddress: DisplayAddress, chainAsset: ChainAsset)", parameterMatchers: matchers)) } + func showSwapSetup(from: M1, state: M2) -> Cuckoo.ProtocolStubNoReturnFunction<(OperationDetailsViewProtocol?, SwapSetupInitState)> where M1.OptionalMatchedType == OperationDetailsViewProtocol, M2.MatchedType == SwapSetupInitState { + let matchers: [Cuckoo.ParameterMatcher<(OperationDetailsViewProtocol?, SwapSetupInitState)>] = [wrap(matchable: from) { $0.0 }, wrap(matchable: state) { $0.1 }] + return .init(stub: cuckoo_manager.createStub(for: MockOperationDetailsWireframeProtocol.self, method: "showSwapSetup(from: OperationDetailsViewProtocol?, state: SwapSetupInitState)", parameterMatchers: matchers)) + } + func present(message: M1, title: M2, closeAction: M3, from view: M4) -> Cuckoo.ProtocolStubNoReturnFunction<(String?, String?, String?, ControllerBackedProtocol?)> where M1.OptionalMatchedType == String, M2.OptionalMatchedType == String, M3.OptionalMatchedType == String, M4.OptionalMatchedType == ControllerBackedProtocol { let matchers: [Cuckoo.ParameterMatcher<(String?, String?, String?, ControllerBackedProtocol?)>] = [wrap(matchable: message) { $0.0 }, wrap(matchable: title) { $0.1 }, wrap(matchable: closeAction) { $0.2 }, wrap(matchable: view) { $0.3 }] return .init(stub: cuckoo_manager.createStub(for: MockOperationDetailsWireframeProtocol.self, method: "present(message: String?, title: String?, closeAction: String?, from: ControllerBackedProtocol?)", parameterMatchers: matchers)) @@ -18004,6 +17992,12 @@ import Cuckoo return cuckoo_manager.verify("showSend(from: OperationDetailsViewProtocol?, displayAddress: DisplayAddress, chainAsset: ChainAsset)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } + @discardableResult + func showSwapSetup(from: M1, state: M2) -> Cuckoo.__DoNotUse<(OperationDetailsViewProtocol?, SwapSetupInitState), Void> where M1.OptionalMatchedType == OperationDetailsViewProtocol, M2.MatchedType == SwapSetupInitState { + let matchers: [Cuckoo.ParameterMatcher<(OperationDetailsViewProtocol?, SwapSetupInitState)>] = [wrap(matchable: from) { $0.0 }, wrap(matchable: state) { $0.1 }] + return cuckoo_manager.verify("showSwapSetup(from: OperationDetailsViewProtocol?, state: SwapSetupInitState)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + } + @discardableResult func present(message: M1, title: M2, closeAction: M3, from view: M4) -> Cuckoo.__DoNotUse<(String?, String?, String?, ControllerBackedProtocol?), Void> where M1.OptionalMatchedType == String, M2.OptionalMatchedType == String, M3.OptionalMatchedType == String, M4.OptionalMatchedType == ControllerBackedProtocol { let matchers: [Cuckoo.ParameterMatcher<(String?, String?, String?, ControllerBackedProtocol?)>] = [wrap(matchable: message) { $0.0 }, wrap(matchable: title) { $0.1 }, wrap(matchable: closeAction) { $0.2 }, wrap(matchable: view) { $0.3 }] @@ -18033,6 +18027,12 @@ import Cuckoo + func showSwapSetup(from: OperationDetailsViewProtocol?, state: SwapSetupInitState) { + return DefaultValueRegistry.defaultValue(for: (Void).self) + } + + + func present(message: String?, title: String?, closeAction: String?, from view: ControllerBackedProtocol?) { return DefaultValueRegistry.defaultValue(for: (Void).self) } From c0f1d2ad0dd58cae5de640a918086542a56ea177 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Mon, 20 Nov 2023 13:00:43 +0300 Subject: [PATCH 195/204] pr fixes --- .../Common/Extension/Foundation/BigInt+Decimal.swift | 9 +++++++++ .../Modules/AssetList/Models/AssetListGroupModel.swift | 4 ++-- .../AssetList/Models/AssetListModelHelpers.swift | 10 +++++++--- novawallet/ru.lproj/Localizable.strings | 2 +- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/novawallet/Common/Extension/Foundation/BigInt+Decimal.swift b/novawallet/Common/Extension/Foundation/BigInt+Decimal.swift index 34df8de764..1837469cc2 100644 --- a/novawallet/Common/Extension/Foundation/BigInt+Decimal.swift +++ b/novawallet/Common/Extension/Foundation/BigInt+Decimal.swift @@ -9,3 +9,12 @@ extension BigUInt { ) ?? 0 } } + +extension Optional where Wrapped == BigUInt { + func decimalOrZero(precision: UInt16) -> Decimal { + guard let self = self, self != 0 else { + return 0 + } + return self.decimal(precision: precision) + } +} diff --git a/novawallet/Modules/AssetList/Models/AssetListGroupModel.swift b/novawallet/Modules/AssetList/Models/AssetListGroupModel.swift index b5d7c7fa34..ed2433fcb4 100644 --- a/novawallet/Modules/AssetList/Models/AssetListGroupModel.swift +++ b/novawallet/Modules/AssetList/Models/AssetListGroupModel.swift @@ -7,9 +7,9 @@ struct AssetListGroupModel: Identifiable { let chain: ChainModel let chainValue: Decimal - let chainAmount: BigUInt + let chainAmount: Decimal - init(chain: ChainModel, chainValue: Decimal, chainAmount: BigUInt) { + init(chain: ChainModel, chainValue: Decimal, chainAmount: Decimal) { self.chain = chain self.chainValue = chainValue self.chainAmount = chainAmount diff --git a/novawallet/Modules/AssetList/Models/AssetListModelHelpers.swift b/novawallet/Modules/AssetList/Models/AssetListModelHelpers.swift index 307404c25b..784af8215d 100644 --- a/novawallet/Modules/AssetList/Models/AssetListModelHelpers.swift +++ b/novawallet/Modules/AssetList/Models/AssetListModelHelpers.swift @@ -19,14 +19,18 @@ enum AssetListModelHelpers { from chain: ChainModel, assets: [AssetListAssetModel] ) -> AssetListGroupModel { - let amountValue: AmountPair = assets.reduce(.init(amount: 0, value: 0)) { result, asset in + let amountValue: AmountPair = assets.reduce(.init(amount: 0, value: 0)) { result, asset in .init( - amount: result.amount + (asset.totalAmount ?? 0), + amount: result.amount + asset.totalAmount.decimalOrZero(precision: asset.assetModel.precision), value: result.value + (asset.totalValue ?? 0) ) } - return AssetListGroupModel(chain: chain, chainValue: amountValue.value, chainAmount: amountValue.amount) + return AssetListGroupModel( + chain: chain, + chainValue: amountValue.value, + chainAmount: amountValue.amount + ) } static func createGroupsDiffCalculator( diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 173b0ecef5..67d246e760 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -114,7 +114,7 @@ "settings.preferences" = "Предпочтения"; "settings.security" = "Безопасность"; "settings.community" = "Сообщество"; -"settings.wiki" = "Википедия"; +"settings.wiki" = "Вики"; "confirmation.skip.action" = "Пропустить"; "account.info.title" = "Аккаунт"; "account.info.name.title" = "Имя"; From 3f2c6e0d639e683313dcd562d79a251d07916f41 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 20 Nov 2023 15:42:49 +0100 Subject: [PATCH 196/204] prioritise json for staking max electing voters --- .../Model/ChainRegistry/LocalChain/ChainModel.swift | 8 ++++++++ .../RelaychainConsensusStateDepending.swift | 7 +++++-- .../StakingSharedState/RelaychainStakingSharedState.swift | 3 ++- .../StakingSharedState/RelaychainStartStakingState.swift | 3 ++- .../Staking/Operations/VotersInfoOperationFactory.swift | 8 +++++++- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift index 5bb9a02f5b..560c1eeb15 100644 --- a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift +++ b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift @@ -239,6 +239,14 @@ struct ChainModel: Equatable, Codable, Hashable { var isUtilityTokenOnRelaychain: Bool { additional?.relaychainAsNative?.boolValue ?? false } + + var stakingMaxElectingVoters: UInt32? { + guard let value = additional?.stakingMaxElectingVoters?.unsignedIntValue else { + return nil + } + + return UInt32(value) + } } extension ChainModel: Identifiable { diff --git a/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainConsensusStateDepending.swift b/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainConsensusStateDepending.swift index 64b18d3b39..c5df8bbe6a 100644 --- a/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainConsensusStateDepending.swift +++ b/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainConsensusStateDepending.swift @@ -4,7 +4,8 @@ import SubstrateSdk protocol RelaychainConsensusStateDepending { func createNetworkInfoOperationFactory( - for durationFactory: StakingDurationOperationFactoryProtocol, + for chain: ChainModel, + durationFactory: StakingDurationOperationFactoryProtocol, operationQueue: OperationQueue ) -> NetworkStakingInfoOperationFactoryProtocol @@ -22,10 +23,12 @@ protocol RelaychainConsensusStateDepending { final class RelaychainConsensusStateDependingFactory: RelaychainConsensusStateDepending { func createNetworkInfoOperationFactory( - for durationFactory: StakingDurationOperationFactoryProtocol, + for chain: ChainModel, + durationFactory: StakingDurationOperationFactoryProtocol, operationQueue: OperationQueue ) -> NetworkStakingInfoOperationFactoryProtocol { let votersInfoOperationFactory = VotersInfoOperationFactory( + chain: chain, operationManager: OperationManager(operationQueue: operationQueue) ) diff --git a/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainStakingSharedState.swift b/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainStakingSharedState.swift index c870e70fb9..548f85a656 100644 --- a/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainStakingSharedState.swift +++ b/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainStakingSharedState.swift @@ -123,7 +123,8 @@ final class RelaychainStakingSharedState: RelaychainStakingSharedStateProtocol { ) return consensusDependingFactory.createNetworkInfoOperationFactory( - for: durationFactory, + for: stakingOption.chainAsset.chain, + durationFactory: durationFactory, operationQueue: operationQueue ) } diff --git a/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainStartStakingState.swift b/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainStartStakingState.swift index bb0c17ba75..848535598f 100644 --- a/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainStartStakingState.swift +++ b/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainStartStakingState.swift @@ -211,7 +211,8 @@ final class RelaychainStartStakingState: RelaychainStartStakingStateProtocol { ) return consensusDependingFactory.createNetworkInfoOperationFactory( - for: durationFactory, + for: chainAsset.chain, + durationFactory: durationFactory, operationQueue: operationQueue ) } diff --git a/novawallet/Modules/Staking/Operations/VotersInfoOperationFactory.swift b/novawallet/Modules/Staking/Operations/VotersInfoOperationFactory.swift index a3921f3f92..69891b6c13 100644 --- a/novawallet/Modules/Staking/Operations/VotersInfoOperationFactory.swift +++ b/novawallet/Modules/Staking/Operations/VotersInfoOperationFactory.swift @@ -11,9 +11,11 @@ protocol VotersInfoOperationFactoryProtocol { final class VotersInfoOperationFactory { let operationManager: OperationManagerProtocol + let chain: ChainModel - init(operationManager: OperationManagerProtocol) { + init(chain: ChainModel, operationManager: OperationManagerProtocol) { self.operationManager = operationManager + self.chain = chain } private func createBagsListResolutionOperation( @@ -54,6 +56,10 @@ final class VotersInfoOperationFactory { private func createMaxElectingVotersOperation( dependingOn codingFactoryOperation: BaseOperation ) -> BaseOperation { + if let maxElectingVoter = chain.stakingMaxElectingVoters { + return BaseOperation.createWithResult(maxElectingVoter) + } + let valueOperation = PrimitiveConstantOperation( path: ElectionProviderMultiPhase.maxElectingVoters, fallbackValue: UInt32.max From 54a2ba72cd6fdda15dd7122a13989d9470215a5a Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 21 Nov 2023 05:55:25 +0100 Subject: [PATCH 197/204] fix get tokens receiver --- .../Transfer/TransferSetup/TransferSetupViewFactory.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift index 6fabbf39b0..a7a781ab46 100644 --- a/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupViewFactory.swift @@ -36,11 +36,11 @@ enum TransferSetupViewFactory { assetListObservable: AssetListModelObservable, transferCompletion: TransferCompletionClosure? = nil ) -> TransferSetupViewProtocol? { - guard let originChainAsset = origins.first, let wallet = SelectedWalletSettings.shared.value else { + guard let wallet = SelectedWalletSettings.shared.value else { return nil } - let recepient = try? wallet.fetch(for: originChainAsset.chain.accountRequest())?.toDisplayAddress() + let recepient = try? wallet.fetch(for: destination.chain.accountRequest())?.toDisplayAddress() return createView( from: .init( From 8d0edec013b0e24c3d38a5cd86d304966ad3b09f Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 21 Nov 2023 06:21:42 +0100 Subject: [PATCH 198/204] fix approx sign --- .../ViewModel/OperationDetailsViewModelFactory.swift | 3 ++- .../Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift | 8 +++----- .../Swaps/Confirm/Model/SwapConfirmViewModels.swift | 3 ++- .../Modules/Swaps/Confirm/View/SwapElementView.swift | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift index ddd1ec9848..9bf604e32a 100644 --- a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift +++ b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift @@ -298,7 +298,8 @@ final class OperationDetailsViewModelFactory { return .init( imageViewModel: assetIcon, hub: networkViewModel, - balance: balanceViewModel + amount: balanceViewModel.amount, + price: balanceViewModel.price.map { $0.approximately() } ) } diff --git a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift index 495dd685e9..5a4b925c97 100644 --- a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift +++ b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModelFactory.swift @@ -61,14 +61,12 @@ extension SwapConfirmViewModelFactory: SwapConfirmViewModelFactoryProtocol { amount: amountDecimal, priceData: priceData ).value(for: locale) - let price = balanceViewModel.price.map { $0.approximately() } + return .init( imageViewModel: assetIcon, hub: networkViewModel, - balance: BalanceViewModel( - amount: balanceViewModel.amount, - price: price - ) + amount: balanceViewModel.amount, + price: balanceViewModel.price.map { $0.approximately() } ) } diff --git a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModels.swift b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModels.swift index b5361defd8..1ee05a4848 100644 --- a/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModels.swift +++ b/novawallet/Modules/Swaps/Confirm/Model/SwapConfirmViewModels.swift @@ -1,7 +1,8 @@ struct SwapAssetAmountViewModel { let imageViewModel: ImageViewModelProtocol? let hub: NetworkViewModel - let balance: BalanceViewModelProtocol + let amount: String + let price: String? } struct DifferenceViewModel { diff --git a/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift b/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift index 41d11541c9..af94688bea 100644 --- a/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift +++ b/novawallet/Modules/Swaps/Confirm/View/SwapElementView.swift @@ -118,7 +118,7 @@ extension SwapElementView { animated: true ) hubIconNameView.detailsLabel.text = viewModel.hub.name - valueLabel.text = viewModel.balance.amount - priceLabel.text = viewModel.balance.price ?? " " + valueLabel.text = viewModel.amount + priceLabel.text = viewModel.price ?? " " } } From 8eab30412817a6dd1575d1e3685baae9b06a3405 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 21 Nov 2023 07:07:58 +0100 Subject: [PATCH 199/204] replace icons in tx history and details --- .../iconIncomingTransfer.pdf | Bin 1392 -> 1390 bytes .../iconOutgoingTransfer.pdf | Bin 1391 -> 1390 bytes .../iconRewardOperation.pdf | Bin 4992 -> 5007 bytes .../Contents.json | 0 .../iconSwapOnDetails.pdf | Bin .../iconSwapHistory.imageset/Contents.json | 12 ------------ .../iconSwapHistory.pdf | Bin 2226 -> 0 bytes .../OperationDetailsViewModelFactory.swift | 2 +- .../TransactionHistoryViewModelFactory.swift | 2 +- 9 files changed, 2 insertions(+), 14 deletions(-) rename novawallet/Assets.xcassets/{iconSwapOnDetails.imageset => iconSwap.imageset}/Contents.json (100%) rename novawallet/Assets.xcassets/{iconSwapOnDetails.imageset => iconSwap.imageset}/iconSwapOnDetails.pdf (100%) delete mode 100644 novawallet/Assets.xcassets/iconSwapHistory.imageset/Contents.json delete mode 100644 novawallet/Assets.xcassets/iconSwapHistory.imageset/iconSwapHistory.pdf diff --git a/novawallet/Assets.xcassets/iconIncomingTransfer.imageset/iconIncomingTransfer.pdf b/novawallet/Assets.xcassets/iconIncomingTransfer.imageset/iconIncomingTransfer.pdf index 06bbafad70d07b97fdac99e996b34f922f268c2c..42864579b320b8f5fa90827b2e2bb712a4678684 100644 GIT binary patch literal 1390 zcmZux%Wm5+5WMp%_7WgDgyQloAPCSn4-{=t*Xb?jL6sTDg)Oy`Ql$O*E@g?5(+~zs zjX1kIqg|dXuW!$hJH`+M)Zc$FfQt*rZeF^(`>yE1;R*Y{vH-!o7qsB=^6+D!T1Og_ zNBobW+H_B#2wc%Y)^<%O_AI*&zqe&r-Ce=re*RRp>=(;!5H>AC9y3r5F>cP&F1@qd zTI;k2%{^)%EtLC-nU1ARTUJbQY0xSONF-~~<*;Xx8{wR@fQDNXLTNy*1jg7CB)Boo z=W}Yws4Vm*x5UsKrBqVG43j)ZD|1acMzZASq*fkEhRAboHIW5(1X&L|hLSra6y}3= zL_o!JWtE;onp-cVm{Ch6El1&%<;<{D_Kcmpp_qm%t8+=RlR1(& z#`jiCnBp3ZP;TZdDKuOvFLDF)v525WRG2{Nxg__TIYx7?l*@JRsq#jVN_Fr#N>lVU zt{Fy&Bt=3pX@-)$n!-B?a*8-%DR=)NhLf{0-)NN`dt#5QVqaM7`Jti5?vIUrm>!2B z%>;d**-X8ws=908;Tz2ut?cWcp8>LEu?Yk4DXh1}Rs8}FQ|qVB*Kh2`NUPA+&rKOx zh*P{8!Y%5m3l(LIqd9`hq9F{-0MTfU5+XER03Wt%DixxF6^wZn^)qqtzxrE3W|8bs zq$sdW2`Ax;qAPaw=9v1@ghwX8vBMS6Ujx<{Wq{yeA>9o9iz-3X8m}Nv-S!%?WW*5> z1qFTup`b|{PIXPO-Gzq58}a=$*>T#|`)~%Q+x6MOlx$Vk9Yn#s!P))xZ=mY?8?R`( OUJj!5?BwL;^X)&RZ7t9M literal 1392 zcmZXUT~FIE6o&8pE6xoPOv3T;XDlJ4(E^&bX{_uH?IKG}>C`4$5+=s4-*K8aDT^xA zd2&8q`}pK;d2^23F@_+Z{_&FmTwFkQ{nFjucSRQtZ`l8p1qg1npa(854?h;Fb#!C$ z$p2HQHr*2_f-5p)ZP$ci&$4Csvn|8w_6ipF^S82Pzgc#TuxT0cn1OPLadDnj>6_!m zN<&7LE9rBkq1;cN$t*3}vSMgCdBn)lbK$H;3*?lj5?rD&LIWCZoHcn43OTCWlO<}d zm5V7SRx(ta(rc76ijwz+WI07lY|2gEi8?N|l1jmjDQ=|jA_uBWc&DT$D7izU4MymN zbUCfK7T#z-L$p1Dpq#mujMS3?mF?S-9&_tp#uWFt@$_|aRvdF-sDi#i?|cj?II#$K zv1ymumg1yxX3UVrec3bbqz%P% zTw4;SZ+j3&T8@U)=~Wz=;mVLaB*e^n+W*-eOmNz+9eZMr?^wmYvUuu;BR!t~c+?N` z<4~rlAXR5m^}ed=u7!v1G-dQ;-~RpzklhuVFo4fty)CZl7kC(CoKJz)<2+2OG14lu z^>b5(7UCSQhH#6z>Ow^s<8Y4fu4ssX84wNU*g{0p74UJprcxnxu!1qqqJAb{{IC9& z$SjgQiWD1cQ^Kk7MbQ5nA(DoJRo4{TU1(Um6F)3Tj?=c@hch_cuFnplWUIRFAU50!oZW9<16AMO Sc}3Ip?I1eAPEM}B-24NFB`tyg diff --git a/novawallet/Assets.xcassets/iconOutgoingTransfer.imageset/iconOutgoingTransfer.pdf b/novawallet/Assets.xcassets/iconOutgoingTransfer.imageset/iconOutgoingTransfer.pdf index 3c90142fc4ac6e7bb5bbe9b803c60649f654e9d0..029e46de3d41c916a2ae5df1aebe522fbcd7dcb7 100644 GIT binary patch literal 1390 zcmZux$!^;)5WVv&<`N(|gywK35CmwP1w~ubb$SbWP-Vt(VN0!~6luS{Ls_QeCWHY~ zPqVyvnwgWu_3b%w#~6Zu`uh(CaB%^%o0snHzAL)8f5QH+EI@GY1s%A&-2YgtR>_UY zBma-FT6a&N2wajOYr7^EJ2tzHzc*!E-d(}`e)?3l>=&EeAgo)4JY}F9;&3_7xBSj> zYpoF)H20{5MksfqXEw{1wyc;`T%!@n02*$DaYn4kN$WvF;i zR_OpraOE&aiY~cRLg5jHJb0_^)VpMY8{<5M$01P)NeZGjflP5vi3FQqnfmCQ6(W{g z33S>`#UTA$uSM2OacR&hl5FaVTuX191QOqRX@f}xjwlyI%lHY1Kp#!15a=Sg^r$TL zR!`pqAZ=8CMyQujc|-DyAd!&Di$DeEXc>}+tmHa?CKFV|<*iQHvM2V)D)xn?o*x?e z(EX{=_tWE0-DkgOA{ZN0LS(#pkD*l7#dlEhl8{k`WID(9PvDc zynbcnQy(2!VcG@%K`26ox~AA{W5d$ddw)%GoVN8Yp26v6b+#8}wyf(8Qo+5zv-{28 VNbLKKS2SH;4x;t!N>pzJ*YC{xNxLaQi`-+-=!^)GC~+O zHRkNj4tI63yuLk0?ifQ5(Ej+z04^?|xOpA!?uTj!hbP?s)D;MBwV(x;mxm7v%{tPU zJmP-}&1QH4Mc|4KvVQ17^~{Rv@Ml|x)!h{=?&nW+&wjJw24T}P5>W~O6l)7K3{!%J_xC_xEslrE(PA{j2c&;~&vS6avt0Tp+pHrmfI z$-VQ!%Ne_7WQfoa#FC@&G31VsUMWyn4d}QsT3HJ-%yOYiO;{!yF0~S*tfnF;UQCUe zYvqhlAh|;m6=q0rBT04%BtKInCqlGNYAVVJ?@0}2luN5T+9MD-N?j@uizX*E(oIJB zn$C^+_IEKtB+zim-wbrcI=5gzGDsh%3{-x9X+*w z?DWI-ZxD<^ziVVhKyG6?eDJu#j@Ij5%?U|+v=))g@@exsr5BEcJMgw zrbw&Mw=Z2CdWd7Z8pAExW(W;sjI%j`%c>&`%m~qJP9;PrzX3jO*CZ8U1uK~HEZP_1 z;v4-vA+t!-*|SKoz&<6+g)ge1+O?Zw>T4GsnE=NQwZ?C-Gy?_*9v9N9AuebVA~{Lod~UFcZ6y${!9$7$a_hch_cuFnpp6sxu!AQrqgxVYc` W3#5Mk%B#9zUk;-6?BwL;%k6(Y9W6cp diff --git a/novawallet/Assets.xcassets/iconRewardOperation.imageset/iconRewardOperation.pdf b/novawallet/Assets.xcassets/iconRewardOperation.imageset/iconRewardOperation.pdf index 8253361fa359fa80eb5b934e0834d64a4c98fead..c4786e39e634ab990bd24e636594a28ac0a71eb4 100644 GIT binary patch literal 5007 zcmZvgS&tjn5ryCTSM-YjJAixcz5s@SSdxt(38IYhX83`oXn6u@m<;Ja@vqN!>NcAk zIRgW39p0suQ>Uu?#hX{(edU)rO@4Os@uz=I^ZfPK^WE!D=kMR1cjv?Sm$(1zce7hs zbNtQUd^7%=4v+7&Z3@r!|K;%b{`|`v0qPc- z=^%CayfUyS`0DHoYPxz-Oc++D_1lwHTBxV z{NXtodhU(W`x1OiDa;|TC;7^mZOJL{EX=u-bJm%4shOqh@k{ijHY_ent|ez~cT0)Q zcb@X%Q#N}EP)aG9MV!=oNOAUUskQqY^b|tNG3&{N6av4dg&&JAH`spi+?Z48{RXW| zYO&+49ax9~QBSg=$QqY?aS^*CE|k)6MkeN1xNd3Dx0Z(|v130eNiO!)WnTlSo7@r$ zu``g^v{tX#4{-^-6fWCn0JO$hTe&qs7o|nnbtC`~OA?t>{z?xRzi?HptW%mp!@9&n zZ;Ir`hy8@NK80TAC`&7On!V%XmVy^n_zMJW3*QwZd?Xa}eYjakYJK6Ba&zMUqvC%0 zg%IZANb{T_C7TKaElaB|z!rku3!#`vk}Iu|hq#mqPs#AroC`Fp@hL&`Z5_r`A3{pk z(+H6ZzLj>p)Hnz?8ZMV5l53@~LAP2UV})V0i^*;2rFs{DY9P*al!Ont*9_QF(XUn! z1i^_okv+VRTedZc}?ea7dnuYSPVFk3~WwS*_T*rq-M3XMVZo<6jCEh zx*+vj#@2*g*{;YwS6i1o3vHdJ`rt4%oRn&|LXbgbfiNgnfgx9H5=5;RZAEh2dfF&| zAj+F~-D*NvY7Fi(Ngz|H_DFT(qz{!q1;Pk7c|f8nu^^$`T0GUsc_NVM4Ru9yd{QM! zLMXdc8=q0&iopSKw|jziMMd$FfVT6n$wkHK=31y;ktAwp>D5ui*ZSLB*eY`KbQGlN z_ZE^5)8+1R4INvdld)utFGva%sl7=9SalUKBda4@Zt>p179k_0Amk-0eOu-o{UP^Y z92^7z$%I>!>CRY$=y+O)$gz=4LWET!;L1ryu;>(a(jYDfOd7l${Fc+jwLB;kT`a{! zNEo7qKVTU6ffj|fWGBqizY^@@{Mu@?Ld#P9-;02xVJ%aL>#;YZ_EFu->RH9+=@1R& zN3I|TWlyd7yi~dvrmp%Bq0WYx+Dl02h*xyTjZ|hka2P~Tt~bL<><$u7u1oX;#}1rx zt@6+p8Wc?k%Af=iVxSf!!@tJwZvo;3t=)6rsMci7XgyEsHCr8T{7IjJZoC(N(2@TvPTi< z&9RwexI#N8$n?*I^pP@#n8GR&sXohf0yWqWv(b$^>(Y|pE3MbMHYzpyNCbjp#`+xu z&=o=Luz0SpA)snB@M@pZ-WvjKm~|Kp78)@&6iR9UDeQrq4w2eMm5epInkLt|dL3#+ zENm=}vXm%l_@Nt_D;0w-V^T5qbTaMBWL5(eq_E&H1Ez~aQ)*Ckjo~WW16zY_yCvpA zx4dkFn`Rv)kWMuQKg|+YMD~dVCAPK<=1*aaWegbOOvrhm4*F&-O)aQ;tz-zGWVD-J z#i(gmriI$rH=3HtmX$4{Us|c&hmHyBL4z(BO%l?Kbxeip-Rh=HU$&bPv4*9lD_LhZ zS0hX8R(I~^H|5rbc%y#3WhwDT`9iI+O+}WjQS$=#W=d7ge@kS%*m>`}>lO$_! zrusJbr7%KrJwv*wQ>I!raEKWFN0SxJuL>($$s|q5P?QdPxrZ?(Q46**2f6^5xG^km4Vkh^ z0i}eq-P1Cc+e#)GL`J(CBR#UiZbq4`b|4Qc{bc23fu-gn6A2@htz_h49%dyZafpX@ zgQ-b?F@{~3bwNF^MZV#~rBZAyid4N`F4#{+X1#hSk0>h)Cf&3mG6n^C=V9)#G*aZ6 zH`u|CRN>Hq0-M;NzRq#>enTYHpfh9UIt+0v?@{zi#&{}EMC1_B2Stok^V^kq8)V%M z;~W9qK)u=!e@(e|m|Hax+0>nx_d(1xDV18vG&%zLlHM|_nElPk?nJSfMq%YlARw%fw}h3X*xt8H2obYs5#YMf3>21@VTb z3Ks{kP7DG>izT%p3oO^gOj67s2IZc}yY)yLFj>`t%1c|JZPLr5I9}HFia4sR1JM~N z0PDHW2FTpfgpj=kM z<5(sTn!6z1sAv*kjMW^{;_ww0lU_D7Wvx9a=Ac(Bn`&%CxDgneexj;xQ=*Y64Ktfy zUFo$!(z~DkFg;FxpY%;+{orx=I-(CDi`{b2?EdU*G+`}X*0{sF%=Ros3gNgH&1x}0=>I30g|+8<7{KJ(nK$Gtp0 zo)3@QsBbqH@XhWCY-U_Ch;KI+9|T>$5dHb#9j4Cm;C{Z`^YZvBd;Rb1KY`qw#Ef-w zk{mp37hWBIxjXMZ9N$0R`swNL^TfnH4fibs`Ye)X;PsFGYftmz1|j9op_DY~>2F(= zcq{uHqL4p_V9^GhpLP!)4o{Olnf>V%;rQZo{CN0s{^H@?mm?^5_s8RTmctg{ySER& Y9kAX$iS3@w<_@Dpo?g6o{kQM_2e(4&_5c6? literal 4992 zcmZvgTW=i45ryC9SM-YjJAj9MzW|1TSdxt(38GMWGxET!)-nT$OvrWM_}Aw<)w8oJ z#gt%=ie24ZRp*?lnHO(fefL#Z+p+}b-Qz$0xj6Ur*Y4)^FQ@O{p6*V^`LAIAJKQ(L-{;oDC@zk*rrdmxt>jQjbL^8lzfbP4Tv_WHV(z85 z57*YR)>tYRWo2NeK3vRe&$0HX6B9v1_RMSa#Sh!luO(KFa51f!Epya8V#?9u9oOc0 z;Hh!*VZV*5&wZ56jo2yq5o&i)k+o2~o_q0IrHRP-Z3potJJS=|A zzBW#^+@xHusvniYnh9fvLELjKSMQt8xKftUVpIVzwp5bkt>#ci<6PpZlw4)5F{b9q z%5OS#q&6EK4ohEEWJ;1UPci1=NQxh%jfDiBQX-&B`Ytw-Paru(fOD~`ARUkbuzknh zg5qKMi4;<~7K5v6E=jQYxHM4NlJ6vakeEOUfl@2YS4^JcK+@$pyUA{oYUz+;H*%TL z$?v%A7CZUPzSPU1xPp_`JzS~l0P2d}uY-lSwzYd~1R2&|Q%~J>%RTosN_YaMm~0Fp zMxBiCpadjitQE8xA+lFsm^#IW4+I7vg1L^Ac(DCoPyuYL16E;ZT>@7CQLa6fX#&@^ z*#WUoUUSW1sQRo+wFf&QR|+WCGMXnj?8eO~wM@CS31;ZdE@;a1H3EtZj_$=X4^&hD7_EKBh|A#C%RNw6C+KQtF`g$ zSx1>GNCYCY+w0iGX$(QTjtD@zjdKa4u)@5zmC_|9jXrl|j6p%SnM6S3Xxc3E5Z-fo zVY0G2Rwyg=W6F`lU~4t>jM&V12(7i42bjehkp*LbY9^q*lz@0GN@SIRO;e#QQLGH$;zEKanO$ z))XB$OGj4;VFAgo)6fI z29qlypL&pB+K}$?DzOPiB|-4HMC}X)Okp(NYA!-)fTe_p`ALImQ3F3Kme9AG=8H@p zbkB>?1Q4k{Z|0{?>V;2d>g`PT)muy^9lDy{!|Wx7DiEpypvVM>S{nZ?QeF-Aml$R? zO9`)IkjUgeFm}$7y@KXz)0X0`P1w4<8t1u5sygdN*OE8=jE<(F(-IJ5Oj(@Ua4=}! zwqb*_R&ttMW-?a?-V25E7D0=~C)V3VPzlfMq4_Cx z^$=m4!E5$=2Bc?YzyIO>a4xM#N3y6jDM@q3_DX z$Uu|Vq(zss7#KV{?+lchQH#L13Zf9D3N~x?A*M?^Xlt-d!xoW+3|fl^V@T zY3ii5o0k2SzDf6f{N3`f{B_YsknIb{`Ev*f+CHan?XZ6(VT``|%JTb%hsV>i`{D0= zb>ZjcZ~y(*(cQecdw<*ve?Gpuzx(#_7xx2^Yr44pDw27ro2JXtd8gas^W)D?hvTy& zXYO{}?&agd>G;5j`dD)|zPWqC<^ZOd_*ip}L8i+mp+DZgauD?7Te_WWNml>=6>re@r5$5d&z}b^~*iCxB z(a&x|M@hwJCTRPaNqYL3sR^>1GK~eD^;QF6Ku7 diff --git a/novawallet/Assets.xcassets/iconSwapOnDetails.imageset/Contents.json b/novawallet/Assets.xcassets/iconSwap.imageset/Contents.json similarity index 100% rename from novawallet/Assets.xcassets/iconSwapOnDetails.imageset/Contents.json rename to novawallet/Assets.xcassets/iconSwap.imageset/Contents.json diff --git a/novawallet/Assets.xcassets/iconSwapOnDetails.imageset/iconSwapOnDetails.pdf b/novawallet/Assets.xcassets/iconSwap.imageset/iconSwapOnDetails.pdf similarity index 100% rename from novawallet/Assets.xcassets/iconSwapOnDetails.imageset/iconSwapOnDetails.pdf rename to novawallet/Assets.xcassets/iconSwap.imageset/iconSwapOnDetails.pdf diff --git a/novawallet/Assets.xcassets/iconSwapHistory.imageset/Contents.json b/novawallet/Assets.xcassets/iconSwapHistory.imageset/Contents.json deleted file mode 100644 index b7858e5993..0000000000 --- a/novawallet/Assets.xcassets/iconSwapHistory.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "iconSwapHistory.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/novawallet/Assets.xcassets/iconSwapHistory.imageset/iconSwapHistory.pdf b/novawallet/Assets.xcassets/iconSwapHistory.imageset/iconSwapHistory.pdf deleted file mode 100644 index db39ff218e585db8ea5f61ba97672f1c60564d8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2226 zcmbVOO>Y}F5WVlO;Ke|4$ci(ZA%{Q^ps|~xXp6c^Z$S^Lym3_6+E!AEw7))YxbjMN z3k1j-u-8wVugCXDU0h$ic|m>YItnq2zyHyN@bYDN^{N}TH~w$O=lJ5sVS9V{5E8(p zui77W!|J|UTn&G3*2D6<*WvQ*`G4zu_g6=KY)^;}FE8e|v(H$4Rp34sD@0*^e~O%! z%`f})w$mOVg(DIhQ;HVu&oN1rOoR4JQV2cw#7Scb(wiu=dPAi!OIUZ(7ZD_D7$viC z2P|c&SsEs#MMmXZE0CmBkwP$2&L|W6!uX565r;w05inywvE_0MEfV*UG)DqRJzLbI zq4r!!sWdDSGlFX$8K~FN8l@ITV&r5%xaN#xh~rd?4*-nBN^H4uEE&0+XR*o^pk~Qn z)gcT(0;qa-1=&bhVhV&DMakb$;B(K!tvC0f>3=cc2JuhJk>N z`3eXmfNbzx)4OlDXmMw-*(54LZpsiKz(^I&bU9#;5#8h(6URv+uv&7xGDpQSbnS{X zscvqrDPrr;bz30Jt#nCgQWbg@SP`iWr-thX2w9s=rJ|SzLcpMM<`g9&I5lo;HBQ?) za9XorUE>rSz{)W}hJc(?_zR)#Z#aF*2Z6aK(K2M0qWVdfsA30akl?J;8wtm=rH%^h zlM`6?i4AehND3WK)MV9yEyJg(#&AdDreLckcftKMm4^`%j)&$=97E(%XW1b!NBHmld;r|{0z)SlA|&(P`Z zA=7f$j~{pIVITY^TprV1j@!eqMMb{`AHeI?4p_jE-=MS0(xLehUHq`QL94+hScZo> zm*Ypo(trBj13Al-A7%0Z_jAG1;Fqhz>TbM!9QtWDyzgQt-88=c4%j^$;DC=0e9+#) zc81`R#}H*48c!i;> Date: Tue, 21 Nov 2023 10:06:58 +0100 Subject: [PATCH 200/204] 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 67d246e760..d185e8a65c 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -114,7 +114,7 @@ "settings.preferences" = "Предпочтения"; "settings.security" = "Безопасность"; "settings.community" = "Сообщество"; -"settings.wiki" = "Вики"; +"settings.wiki" = "Руководство пользователя"; "confirmation.skip.action" = "Пропустить"; "account.info.title" = "Аккаунт"; "account.info.name.title" = "Имя"; From 2ac10c7b2a5a8498649f33a6609587ff38d2b398 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 21 Nov 2023 16:27:26 +0100 Subject: [PATCH 201/204] improve referendum call parsing --- novawallet.xcodeproj/project.pbxproj | 48 +++++++++++++ .../Calls/UtilityPallet/Utility+Calls.swift | 24 +++++++ .../Types/UtilityPallet/UtilityPallet.swift | 5 ++ .../Model/ReferendumActionLocal.swift | 14 +++- .../GovernanceActionOperationFactory.swift | 67 +++++-------------- .../Parser/GovSpentAmountBatchHandler.swift | 50 ++++++++++++++ .../Parser/GovSpentAmountExtractor.swift | 56 ++++++++++++++++ .../Parser/GovTreasuryApproveHandler.swift | 61 +++++++++++++++++ .../GovTreasurySpentAmountHandler.swift | 35 ++++++++++ .../ReferendumDetailsPresenter.swift | 4 +- .../ReferendumFullDetailsPresenter.swift | 6 +- 11 files changed, 313 insertions(+), 57 deletions(-) create mode 100644 novawallet/Common/Substrate/Calls/UtilityPallet/Utility+Calls.swift create mode 100644 novawallet/Common/Substrate/Types/UtilityPallet/UtilityPallet.swift create mode 100644 novawallet/Modules/Vote/Governance/Operation/Action/Parser/GovSpentAmountBatchHandler.swift create mode 100644 novawallet/Modules/Vote/Governance/Operation/Action/Parser/GovSpentAmountExtractor.swift create mode 100644 novawallet/Modules/Vote/Governance/Operation/Action/Parser/GovTreasuryApproveHandler.swift create mode 100644 novawallet/Modules/Vote/Governance/Operation/Action/Parser/GovTreasurySpentAmountHandler.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 177ef93f81..262e0db4dc 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -314,6 +314,12 @@ 0CF193D32A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D22A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift */; }; 0CF193D52A861926003F12F6 /* PredefinedTimeShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D42A861925003F12F6 /* PredefinedTimeShortcut.swift */; }; 0CF193D72A861D7E003F12F6 /* StartStakingInfoConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D62A861D7E003F12F6 /* StartStakingInfoConstants.swift */; }; + 0CFA16132B0CD8A0007AF885 /* GovSpentAmountExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFA16122B0CD8A0007AF885 /* GovSpentAmountExtractor.swift */; }; + 0CFA16162B0CE51E007AF885 /* GovSpentAmountBatchHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFA16152B0CE51E007AF885 /* GovSpentAmountBatchHandler.swift */; }; + 0CFA16192B0CE709007AF885 /* UtilityPallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFA16182B0CE709007AF885 /* UtilityPallet.swift */; }; + 0CFA161C2B0CE851007AF885 /* Utility+Calls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFA161B2B0CE851007AF885 /* Utility+Calls.swift */; }; + 0CFA161E2B0CED07007AF885 /* GovTreasurySpentAmountHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFA161D2B0CED07007AF885 /* GovTreasurySpentAmountHandler.swift */; }; + 0CFA16202B0CEF31007AF885 /* GovTreasuryApproveHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFA161F2B0CEF31007AF885 /* GovTreasuryApproveHandler.swift */; }; 0D5245ED354CC52A842C85A0 /* TransferConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B98AB03AAF06AA891695 /* TransferConfirmViewLayout.swift */; }; 0D8213272889988B78188D9A /* DAppWalletAuthInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 337EC62037D657258BCBC02F /* DAppWalletAuthInteractor.swift */; }; 0DACB56C0BDD4C984FE3C15C /* AssetReceiveWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1179A25C22AF0875A1ADCD /* AssetReceiveWireframe.swift */; }; @@ -4416,6 +4422,12 @@ 0CF193D22A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedStakingViewModelFactory.swift; sourceTree = ""; }; 0CF193D42A861925003F12F6 /* PredefinedTimeShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PredefinedTimeShortcut.swift; sourceTree = ""; }; 0CF193D62A861D7E003F12F6 /* StartStakingInfoConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoConstants.swift; sourceTree = ""; }; + 0CFA16122B0CD8A0007AF885 /* GovSpentAmountExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovSpentAmountExtractor.swift; sourceTree = ""; }; + 0CFA16152B0CE51E007AF885 /* GovSpentAmountBatchHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovSpentAmountBatchHandler.swift; sourceTree = ""; }; + 0CFA16182B0CE709007AF885 /* UtilityPallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilityPallet.swift; sourceTree = ""; }; + 0CFA161B2B0CE851007AF885 /* Utility+Calls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Utility+Calls.swift"; sourceTree = ""; }; + 0CFA161D2B0CED07007AF885 /* GovTreasurySpentAmountHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovTreasurySpentAmountHandler.swift; sourceTree = ""; }; + 0CFA161F2B0CEF31007AF885 /* GovTreasuryApproveHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovTreasuryApproveHandler.swift; sourceTree = ""; }; 0D3FE2CE7F9F2836755DBA63 /* GovernanceUnlockConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnlockConfirmProtocols.swift; sourceTree = ""; }; 0D65686560E2E6C18A5C34CB /* StartStakingInfoWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingInfoWireframe.swift; sourceTree = ""; }; 0D6E67AD564867E121601F18 /* WalletsListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsListPresenter.swift; sourceTree = ""; }; @@ -8955,6 +8967,33 @@ path = StakingUnbondSetup; sourceTree = ""; }; + 0CFA16142B0CD8A9007AF885 /* Parser */ = { + isa = PBXGroup; + children = ( + 0CFA16122B0CD8A0007AF885 /* GovSpentAmountExtractor.swift */, + 0CFA16152B0CE51E007AF885 /* GovSpentAmountBatchHandler.swift */, + 0CFA161D2B0CED07007AF885 /* GovTreasurySpentAmountHandler.swift */, + 0CFA161F2B0CEF31007AF885 /* GovTreasuryApproveHandler.swift */, + ); + path = Parser; + sourceTree = ""; + }; + 0CFA16172B0CE6F6007AF885 /* UtilityPallet */ = { + isa = PBXGroup; + children = ( + 0CFA16182B0CE709007AF885 /* UtilityPallet.swift */, + ); + path = UtilityPallet; + sourceTree = ""; + }; + 0CFA161A2B0CE83A007AF885 /* UtilityPallet */ = { + isa = PBXGroup; + children = ( + 0CFA161B2B0CE851007AF885 /* Utility+Calls.swift */, + ); + path = UtilityPallet; + sourceTree = ""; + }; 0D927CDE29F5C9F4CA537F8F /* AccountConfirm */ = { isa = PBXGroup; children = ( @@ -11361,6 +11400,7 @@ 8438E1DC24C18F11001BDB13 /* Types */ = { isa = PBXGroup; children = ( + 0CFA16172B0CE6F6007AF885 /* UtilityPallet */, 0C500B202B05101100ABEE70 /* AssetTxPaymentPallet */, 0C0CB3862AC5686C00EAC516 /* AssetConversionPallet */, 0C7945B92ABB223D001C07CA /* XTokens */, @@ -11991,6 +12031,7 @@ 845B08022918C2E5005785D3 /* Action */ = { isa = PBXGroup; children = ( + 0CFA16142B0CD8A9007AF885 /* Parser */, 845B811E28F451A40040CE84 /* GovernanceActionOperationFactory.swift */, 845B08002918406A005785D3 /* Gov2ActionOperationFactory.swift */, 845B08032918C308005785D3 /* Gov1ActionOperationFactory.swift */, @@ -12083,6 +12124,7 @@ 845BB8C725E45D0600E5FCDC /* Calls */ = { isa = PBXGroup; children = ( + 0CFA161A2B0CE83A007AF885 /* UtilityPallet */, 0CD352992ACD3E3500B3E446 /* Assets */, 0C22006C2ACAAC0F0067BA61 /* AssetConversionPallet */, 0C7945BC2ABB22AA001C07CA /* XTokens */, @@ -19833,6 +19875,7 @@ 88421062289BBD1900306F2C /* JsonFileRepository.swift in Sources */, AE8B8835267349C200AB0AA9 /* CustomVlidatorListFilter.swift in Sources */, F4E17FB827216C2F00FE36D3 /* MoonbeamBonusService.swift in Sources */, + 0CFA16202B0CEF31007AF885 /* GovTreasuryApproveHandler.swift in Sources */, 8490141324A92F6D008F705E /* AttributedStringDecorator+Terms.swift in Sources */, 849976CA27B2F9E000B14A6C /* DAppMetamaskAuthorizingState.swift in Sources */, F484FF5F264BA2720015320F /* ControllerAccountConfirmationLayout.swift in Sources */, @@ -19938,6 +19981,7 @@ 8860F3E2289D4FFD00C0BF86 /* SectionProtocol.swift in Sources */, 0C22006E2ACAAC2F0067BA61 /* AssetConversionPallet+Call.swift in Sources */, F4FDA0F826A57626003D753B /* BabeEraOperationFactory.swift in Sources */, + 0CFA16162B0CE51E007AF885 /* GovSpentAmountBatchHandler.swift in Sources */, 84AE7AB927D3F96300495267 /* RMRKV1Collection.swift in Sources */, 84AE7AAF27D38B1800495267 /* DrawableIconViewModel.swift in Sources */, 849976D027B3AC0100B14A6C /* MetamaskEvent.swift in Sources */, @@ -21109,6 +21153,7 @@ 84E25BF027E8EFB500290BF1 /* SubqueryAccumulateReward.swift in Sources */, 88AC5ADA2948A8CC0056DD40 /* TransactionSectionModel.swift in Sources */, 8499FEE227C0AF4700712589 /* ChainModel+Nft.swift in Sources */, + 0CFA161E2B0CED07007AF885 /* GovTreasurySpentAmountHandler.swift in Sources */, 0CB64E692B01F798008F268F /* TransferSetupOriginSelectionWireframe.swift in Sources */, 8428765424ADDE0200D91AD8 /* SettingsViewModelFactory.swift in Sources */, 843612C1278FE62900DC739E /* DAppOperationConfirmInteractorError.swift in Sources */, @@ -22743,6 +22788,7 @@ 8217DCBEB74527D57AC82070 /* ParaStkStakeConfirmViewLayout.swift in Sources */, 06FD6F5999D57B27B29C8738 /* ParaStkStakeConfirmViewFactory.swift in Sources */, 0C9525E32A7AAB2A00BD724D /* StakingTimeModel.swift in Sources */, + 0CFA16192B0CE709007AF885 /* UtilityPallet.swift in Sources */, 8846F73129D6BE5000B8B776 /* Data+base58.swift in Sources */, 2BBA744323AA0BF6FE53C212 /* ParaStkSelectCollatorsProtocols.swift in Sources */, 880059E128EF0A5C00E87B9B /* VotingProgressView.swift in Sources */, @@ -23157,6 +23203,7 @@ 58F385F41D42CC96373EDA42 /* TokensManageProtocols.swift in Sources */, 0C500B242B0511F400ABEE70 /* ExtrinsicProcessor+CustomFee.swift in Sources */, CA3C4729115D875D0C80A3E8 /* TokensManageWireframe.swift in Sources */, + 0CFA16132B0CD8A0007AF885 /* GovSpentAmountExtractor.swift in Sources */, 844C3E6B2A08C05A00C4305F /* DAppWalletAuthViewModelFactory.swift in Sources */, 88E5E2A7295D8FA1001B1D41 /* TitleIconViewModel+Hashable.swift in Sources */, 1772735F89EFA931DF7420AD /* TokensManagePresenter.swift in Sources */, @@ -23393,6 +23440,7 @@ 6DC454C4BA27C98987F5DC52 /* WalletConnectSessionsViewFactory.swift in Sources */, F332FA8C330A16C3894B6542 /* WalletConnectSessionDetailsProtocols.swift in Sources */, D0AD3C44BBFD6A9F9FDEC933 /* WalletConnectSessionDetailsWireframe.swift in Sources */, + 0CFA161C2B0CE851007AF885 /* Utility+Calls.swift in Sources */, 0A44D28DF4BCF56131752F35 /* WalletConnectSessionDetailsPresenter.swift in Sources */, F92E73C24AB577F37B35649E /* WalletConnectSessionDetailsInteractor.swift in Sources */, E06F3BD43E589BCE3904BBCB /* WalletConnectSessionDetailsViewController.swift in Sources */, diff --git a/novawallet/Common/Substrate/Calls/UtilityPallet/Utility+Calls.swift b/novawallet/Common/Substrate/Calls/UtilityPallet/Utility+Calls.swift new file mode 100644 index 0000000000..1b3f6379ca --- /dev/null +++ b/novawallet/Common/Substrate/Calls/UtilityPallet/Utility+Calls.swift @@ -0,0 +1,24 @@ +import Foundation +import SubstrateSdk + +extension UtilityPallet { + static var batchPath: CallCodingPath { + CallCodingPath(moduleName: name, callName: "batch") + } + + static var batchAllPath: CallCodingPath { + CallCodingPath(moduleName: name, callName: "batch_all") + } + + static var forceBatchPath: CallCodingPath { + CallCodingPath(moduleName: name, callName: "force_batch") + } + + static func isBatch(path: CallCodingPath) -> Bool { + [batchPath, batchAllPath, forceBatchPath].contains(path) + } + + struct Call: Codable { + let calls: [RuntimeCall] + } +} diff --git a/novawallet/Common/Substrate/Types/UtilityPallet/UtilityPallet.swift b/novawallet/Common/Substrate/Types/UtilityPallet/UtilityPallet.swift new file mode 100644 index 0000000000..65aba45c5c --- /dev/null +++ b/novawallet/Common/Substrate/Types/UtilityPallet/UtilityPallet.swift @@ -0,0 +1,5 @@ +import Foundation + +enum UtilityPallet { + static let name = "Utility" +} diff --git a/novawallet/Modules/Vote/Governance/Model/ReferendumActionLocal.swift b/novawallet/Modules/Vote/Governance/Model/ReferendumActionLocal.swift index 8574ba9f20..63ab1f6281 100644 --- a/novawallet/Modules/Vote/Governance/Model/ReferendumActionLocal.swift +++ b/novawallet/Modules/Vote/Governance/Model/ReferendumActionLocal.swift @@ -22,6 +22,18 @@ struct ReferendumActionLocal { } } - let amountSpendDetails: AmountSpendDetails? + let amountSpendDetailsList: [AmountSpendDetails] let call: Call>? + + func spentAmount() -> BigUInt? { + guard !amountSpendDetailsList.isEmpty else { + return nil + } + + return amountSpendDetailsList.reduce(BigUInt(0)) { $0 + $1.amount } + } + + var beneficiary: MultiAddress? { + amountSpendDetailsList.first?.beneficiary + } } diff --git a/novawallet/Modules/Vote/Governance/Operation/Action/GovernanceActionOperationFactory.swift b/novawallet/Modules/Vote/Governance/Operation/Action/GovernanceActionOperationFactory.swift index e056593f16..880e30c3c2 100644 --- a/novawallet/Modules/Vote/Governance/Operation/Action/GovernanceActionOperationFactory.swift +++ b/novawallet/Modules/Vote/Governance/Operation/Action/GovernanceActionOperationFactory.swift @@ -100,7 +100,7 @@ class GovernanceActionOperationFactory { codingFactoryOperation: BaseOperation, connection: JSONRPCEngine, requestFactory: StorageRequestFactoryProtocol - ) -> CompoundOperationWrapper { + ) -> CompoundOperationWrapper<[ReferendumActionLocal.AmountSpendDetails]> { let operationManager = OperationManager(operationQueue: operationQueue) let fetchService = OperationCombiningService( operationManager: operationManager @@ -110,59 +110,24 @@ class GovernanceActionOperationFactory { } let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() - let context = codingFactory.createRuntimeJsonContext() - - let codingPath = CallCodingPath(moduleName: call.moduleName, callName: call.callName) - - if codingPath == Treasury.spendCallPath { - let spendCall = try call.args.map(to: Treasury.SpendCall.self, with: context.toRawContext()) - - let details = ReferendumActionLocal.AmountSpendDetails( - amount: spendCall.amount, - beneficiary: spendCall.beneficiary - ) - - return [CompoundOperationWrapper.createWithResult(details)] - } - - if codingPath == Treasury.approveProposalCallPath { - let approveCall = try call.args.map(to: Treasury.ApproveProposal.self, with: context.toRawContext()) - - let keyClosure: () throws -> [StringScaleMapper] = { - [StringScaleMapper(value: approveCall.proposalId)] - } - - let wrapper: CompoundOperationWrapper<[StorageResponse]> = requestFactory.queryItems( - engine: connection, - keyParams: keyClosure, - factory: { codingFactory }, - storagePath: Treasury.proposalsStoragePath - ) - - let mapOperation = ClosureOperation { - let responses = try wrapper.targetOperation.extractNoCancellableResultData() - guard let proposal = responses.first?.value else { - return nil - } - - return ReferendumActionLocal.AmountSpendDetails( - amount: proposal.value, - beneficiary: .accoundId(proposal.beneficiary) - ) - } - - mapOperation.addDependency(wrapper.targetOperation) - - return [CompoundOperationWrapper(targetOperation: mapOperation, dependencies: wrapper.allOperations)] - } + let context = GovSpentAmount.Context( + codingFactory: codingFactory, + connection: connection, + requestFactory: requestFactory + ) - return [CompoundOperationWrapper.createWithResult(nil)] + return try GovSpentAmount.Extractor.defaultExtractor.createExtractionWrappers( + from: call, + context: context + ) ?? [] } let fetchOperation = fetchService.longrunOperation() - let mapOperation = ClosureOperation { - try fetchOperation.extractNoCancellableResultData().first ?? nil + let mapOperation = ClosureOperation<[ReferendumActionLocal.AmountSpendDetails]> { + let details = try fetchOperation.extractNoCancellableResultData() + + return details.compactMap { $0 } } mapOperation.addDependency(fetchOperation) @@ -199,9 +164,9 @@ extension GovernanceActionOperationFactory: ReferendumActionOperationFactoryProt let mapOperation = ClosureOperation { let call = try callFetchWrapper.targetOperation.extractNoCancellableResultData() - let amountDetails = try amountDetailsWrapper.targetOperation.extractNoCancellableResultData() + let amountDetailsList = try amountDetailsWrapper.targetOperation.extractNoCancellableResultData() - return ReferendumActionLocal(amountSpendDetails: amountDetails, call: call) + return ReferendumActionLocal(amountSpendDetailsList: amountDetailsList, call: call) } mapOperation.addDependency(callFetchWrapper.targetOperation) diff --git a/novawallet/Modules/Vote/Governance/Operation/Action/Parser/GovSpentAmountBatchHandler.swift b/novawallet/Modules/Vote/Governance/Operation/Action/Parser/GovSpentAmountBatchHandler.swift new file mode 100644 index 0000000000..06e1a621f8 --- /dev/null +++ b/novawallet/Modules/Vote/Governance/Operation/Action/Parser/GovSpentAmountBatchHandler.swift @@ -0,0 +1,50 @@ +import Foundation +import SubstrateSdk +import RobinHood + +extension GovSpentAmount { + final class BatchHandler { + private func handleInternal( + call: RuntimeCall, + handlers: [GovSpentAmountHandling], + context: GovSpentAmount.Context + ) throws -> [CompoundOperationWrapper]? { + for handler in handlers { + if + let wrappers = try handler.handle( + call: call, + internalHandlers: handlers, + context: context + ) { + return wrappers + } + } + + return nil + } + } +} + +extension GovSpentAmount.BatchHandler: GovSpentAmountHandling { + func handle( + call: RuntimeCall, + internalHandlers: [GovSpentAmountHandling], + context: GovSpentAmount.Context + ) throws -> [CompoundOperationWrapper]? { + let path = CallCodingPath(moduleName: call.moduleName, callName: call.callName) + + guard UtilityPallet.isBatch(path: path) else { + return nil + } + + let runtimeContext = context.codingFactory.createRuntimeJsonContext() + + let calls = try call.args.map(to: UtilityPallet.Call.self, with: runtimeContext.toRawContext()).calls + + let wrappers = try calls.flatMap { call in + try handleInternal(call: call, handlers: internalHandlers, context: context) ?? [] + } + + return wrappers + } +} diff --git a/novawallet/Modules/Vote/Governance/Operation/Action/Parser/GovSpentAmountExtractor.swift b/novawallet/Modules/Vote/Governance/Operation/Action/Parser/GovSpentAmountExtractor.swift new file mode 100644 index 0000000000..aaebfa5feb --- /dev/null +++ b/novawallet/Modules/Vote/Governance/Operation/Action/Parser/GovSpentAmountExtractor.swift @@ -0,0 +1,56 @@ +import Foundation +import SubstrateSdk +import RobinHood + +protocol GovSpentAmountHandling { + func handle( + call: RuntimeCall, + internalHandlers: [GovSpentAmountHandling], + context: GovSpentAmount.Context + ) throws -> [CompoundOperationWrapper]? +} + +enum GovSpentAmount { + struct Context { + let codingFactory: RuntimeCoderFactoryProtocol + let connection: JSONRPCEngine + let requestFactory: StorageRequestFactoryProtocol + } + + final class Extractor { + let handlers: [GovSpentAmountHandling] + + init(handlers: [GovSpentAmountHandling]) { + self.handlers = handlers + } + + func createExtractionWrappers( + from call: RuntimeCall, + context: GovSpentAmount.Context + ) throws -> [CompoundOperationWrapper]? { + for handler in handlers { + if let wrappers = try handler.handle( + call: call, + internalHandlers: handlers, + context: context + ) { + return wrappers + } + } + + return nil + } + } +} + +extension GovSpentAmount.Extractor { + static var defaultExtractor: GovSpentAmount.Extractor { + .init( + handlers: [ + GovSpentAmount.BatchHandler(), + GovSpentAmount.TreasurySpentHandler(), + GovSpentAmount.TreasuryApproveHandler() + ] + ) + } +} diff --git a/novawallet/Modules/Vote/Governance/Operation/Action/Parser/GovTreasuryApproveHandler.swift b/novawallet/Modules/Vote/Governance/Operation/Action/Parser/GovTreasuryApproveHandler.swift new file mode 100644 index 0000000000..f77a0ea458 --- /dev/null +++ b/novawallet/Modules/Vote/Governance/Operation/Action/Parser/GovTreasuryApproveHandler.swift @@ -0,0 +1,61 @@ +import Foundation +import SubstrateSdk +import RobinHood + +extension GovSpentAmount { + final class TreasuryApproveHandler {} +} + +extension GovSpentAmount.TreasuryApproveHandler: GovSpentAmountHandling { + func handle( + call: RuntimeCall, + internalHandlers _: [GovSpentAmountHandling], + context: GovSpentAmount.Context + ) throws -> [CompoundOperationWrapper]? { + let path = CallCodingPath(moduleName: call.moduleName, callName: call.callName) + + guard path == Treasury.approveProposalCallPath else { + return nil + } + + let runtimeContext = context.codingFactory.createRuntimeJsonContext() + let approveCall = try call.args.map( + to: Treasury.ApproveProposal.self, + with: runtimeContext.toRawContext() + ) + + let keyClosure: () throws -> [StringScaleMapper] = { + [StringScaleMapper(value: approveCall.proposalId)] + } + + let wrapper: CompoundOperationWrapper<[StorageResponse]> = context.requestFactory.queryItems( + engine: context.connection, + keyParams: keyClosure, + factory: { context.codingFactory }, + storagePath: Treasury.proposalsStoragePath + ) + + let mapOperation = ClosureOperation { + let responses = try wrapper.targetOperation.extractNoCancellableResultData() + guard let proposal = responses.first?.value else { + return nil + } + + let details = ReferendumActionLocal.AmountSpendDetails( + amount: proposal.value, + beneficiary: .accoundId(proposal.beneficiary) + ) + + return details + } + + mapOperation.addDependency(wrapper.targetOperation) + + let resultWrapper = CompoundOperationWrapper( + targetOperation: mapOperation, + dependencies: wrapper.allOperations + ) + + return [resultWrapper] + } +} diff --git a/novawallet/Modules/Vote/Governance/Operation/Action/Parser/GovTreasurySpentAmountHandler.swift b/novawallet/Modules/Vote/Governance/Operation/Action/Parser/GovTreasurySpentAmountHandler.swift new file mode 100644 index 0000000000..a4bd77f3eb --- /dev/null +++ b/novawallet/Modules/Vote/Governance/Operation/Action/Parser/GovTreasurySpentAmountHandler.swift @@ -0,0 +1,35 @@ +import Foundation +import SubstrateSdk +import RobinHood + +extension GovSpentAmount { + final class TreasurySpentHandler {} +} + +extension GovSpentAmount.TreasurySpentHandler: GovSpentAmountHandling { + func handle( + call: RuntimeCall, + internalHandlers _: [GovSpentAmountHandling], + context: GovSpentAmount.Context + ) throws -> [CompoundOperationWrapper]? { + let path = CallCodingPath(moduleName: call.moduleName, callName: call.callName) + + guard path == Treasury.spendCallPath else { + return nil + } + + let operation = ClosureOperation { + let runtimeContext = context.codingFactory.createRuntimeJsonContext() + let spentCall = try call.args.map(to: Treasury.SpendCall.self, with: runtimeContext.toRawContext()) + + return ReferendumActionLocal.AmountSpendDetails( + amount: spentCall.amount, + beneficiary: spentCall.beneficiary + ) + } + + let wrapper = CompoundOperationWrapper(targetOperation: operation) + + return [wrapper] + } +} diff --git a/novawallet/Modules/Vote/Governance/ReferendumDetails/ReferendumDetailsPresenter.swift b/novawallet/Modules/Vote/Governance/ReferendumDetails/ReferendumDetailsPresenter.swift index 5628aba7e6..d69f0266c8 100644 --- a/novawallet/Modules/Vote/Governance/ReferendumDetails/ReferendumDetailsPresenter.swift +++ b/novawallet/Modules/Vote/Governance/ReferendumDetails/ReferendumDetailsPresenter.swift @@ -136,7 +136,7 @@ final class ReferendumDetailsPresenter { private func provideRequestedAmount() { guard - let requestedAmount = actionDetails?.amountSpendDetails?.amount, + let requestedAmount = actionDetails?.spentAmount(), let precision = chain.utilityAssetDisplayInfo()?.assetPrecision, let decimalAmount = Decimal.fromSubstrateAmount(requestedAmount, precision: precision) else { view?.didReceive(requestedAmount: nil) @@ -355,7 +355,7 @@ final class ReferendumDetailsPresenter { accountIds.insert(proposer) } - if let beneficiary = actionDetails?.amountSpendDetails?.beneficiary.accountId { + if let beneficiary = actionDetails?.beneficiary?.accountId { accountIds.insert(beneficiary) } diff --git a/novawallet/Modules/Vote/Governance/ReferendumFullDetails/ReferendumFullDetailsPresenter.swift b/novawallet/Modules/Vote/Governance/ReferendumFullDetails/ReferendumFullDetailsPresenter.swift index 882cd2c139..44aeb1578a 100644 --- a/novawallet/Modules/Vote/Governance/ReferendumFullDetails/ReferendumFullDetailsPresenter.swift +++ b/novawallet/Modules/Vote/Governance/ReferendumFullDetails/ReferendumFullDetailsPresenter.swift @@ -88,10 +88,10 @@ final class ReferendumFullDetailsPresenter { private func provideBeneficiaryViewModel() { guard let beneficiary = getAccountViewModel( - actionDetails.amountSpendDetails?.beneficiary.accountId + actionDetails.beneficiary?.accountId ), let amount = getBalanceViewModel( - actionDetails.amountSpendDetails?.amount, + actionDetails.spentAmount(), locale: selectedLocale ) else { view?.didReceive(beneficiary: nil) @@ -172,7 +172,7 @@ extension ReferendumFullDetailsPresenter: ReferendumFullDetailsPresenterProtocol func presentBeneficiary() { guard - let address = try? actionDetails.amountSpendDetails?.beneficiary.accountId?.toAddress( + let address = try? actionDetails.beneficiary?.accountId?.toAddress( using: chain.chainFormat ) else { return From fbae0b87a6c47b8026e7843c25e6600139cf5101 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 22 Nov 2023 06:22:23 +0100 Subject: [PATCH 202/204] fix disabled state on buttons --- Podfile | 2 +- Podfile.lock | 60 ++++++++++++++++++++++++++-------------------------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/Podfile b/Podfile index f4a1b0ea66..59f2f8b663 100644 --- a/Podfile +++ b/Podfile @@ -8,7 +8,7 @@ abstract_target 'novawalletAll' do pod 'SwiftLint' pod 'R.swift', :inhibit_warnings => true pod 'SoraKeystore', '~> 1.0.0' - pod 'SoraUI', :git => 'https://github.com/ERussel/UIkit-iOS.git', :tag => '1.11.1' + pod 'SoraUI', :git => 'https://github.com/ERussel/UIkit-iOS.git', :tag => '1.12.0' pod 'RobinHood', '~> 2.6.0' pod 'SoraFoundation', :git => 'https://github.com/ERussel/Foundation-iOS.git', :tag => '1.1.0' pod 'SwiftyBeaver' diff --git a/Podfile.lock b/Podfile.lock index 7581bee044..368977c09a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -60,38 +60,38 @@ PODS: - SoraFoundation/NotificationHandlers - SoraFoundation/ViewModel (1.1.0) - SoraKeystore (1.0.0) - - SoraUI (1.11.1): - - SoraUI/AdaptiveDesign (= 1.11.1) - - SoraUI/Animator (= 1.11.1) - - SoraUI/Camera (= 1.11.1) - - SoraUI/Controls (= 1.11.1) - - SoraUI/DetailsView (= 1.11.1) - - SoraUI/EmptyState (= 1.11.1) - - SoraUI/Helpers (= 1.11.1) - - SoraUI/LoadingView (= 1.11.1) - - SoraUI/ModalPresentation (= 1.11.1) - - SoraUI/PageLoader (= 1.11.1) - - SoraUI/PinView (= 1.11.1) - - SoraUI/Skrull (= 1.11.1) - - SoraUI/AdaptiveDesign (1.11.1) - - SoraUI/Animator (1.11.1) - - SoraUI/Camera (1.11.1) - - SoraUI/Controls (1.11.1): + - SoraUI (1.12.0): + - SoraUI/AdaptiveDesign (= 1.12.0) + - SoraUI/Animator (= 1.12.0) + - SoraUI/Camera (= 1.12.0) + - SoraUI/Controls (= 1.12.0) + - SoraUI/DetailsView (= 1.12.0) + - SoraUI/EmptyState (= 1.12.0) + - SoraUI/Helpers (= 1.12.0) + - SoraUI/LoadingView (= 1.12.0) + - SoraUI/ModalPresentation (= 1.12.0) + - SoraUI/PageLoader (= 1.12.0) + - SoraUI/PinView (= 1.12.0) + - SoraUI/Skrull (= 1.12.0) + - SoraUI/AdaptiveDesign (1.12.0) + - SoraUI/Animator (1.12.0) + - SoraUI/Camera (1.12.0) + - SoraUI/Controls (1.12.0): - SoraUI/Animator - - SoraUI/DetailsView (1.11.1): + - SoraUI/DetailsView (1.12.0): - SoraUI/Controls - - SoraUI/EmptyState (1.11.1): + - SoraUI/EmptyState (1.12.0): - SoraUI/Animator - - SoraUI/Helpers (1.11.1) - - SoraUI/LoadingView (1.11.1): + - SoraUI/Helpers (1.12.0) + - SoraUI/LoadingView (1.12.0): - SoraUI/Controls - - SoraUI/ModalPresentation (1.11.1): + - SoraUI/ModalPresentation (1.12.0): - SoraUI/Animator - SoraUI/Controls - - SoraUI/PageLoader (1.11.1) - - SoraUI/PinView (1.11.1): + - SoraUI/PageLoader (1.12.0) + - SoraUI/PinView (1.12.0): - SoraUI/Controls - - SoraUI/Skrull (1.11.1) + - SoraUI/Skrull (1.12.0) - Sourcery (1.4.1) - Starscream (4.0.8) - SubstrateSdk (1.14.0): @@ -159,7 +159,7 @@ DEPENDENCIES: - SnapKit (~> 5.0.0) - SoraFoundation (from `https://github.com/ERussel/Foundation-iOS.git`, tag `1.1.0`) - SoraKeystore (~> 1.0.0) - - SoraUI (from `https://github.com/ERussel/UIkit-iOS.git`, tag `1.11.1`) + - SoraUI (from `https://github.com/ERussel/UIkit-iOS.git`, commit `a4eb0139a27d77f11b8e5083e125c38ea82d8e5e`) - Sourcery (~> 1.4) - Starscream (from `https://github.com/ERussel/Starscream.git`, tag `4.0.8`) - SubstrateSdk (from `https://github.com/nova-wallet/substrate-sdk-ios.git`, tag `1.14.0`) @@ -213,8 +213,8 @@ EXTERNAL SOURCES: :git: https://github.com/ERussel/Foundation-iOS.git :tag: 1.1.0 SoraUI: + :commit: a4eb0139a27d77f11b8e5083e125c38ea82d8e5e :git: https://github.com/ERussel/UIkit-iOS.git - :tag: 1.11.1 Starscream: :git: https://github.com/ERussel/Starscream.git :tag: 4.0.8 @@ -244,8 +244,8 @@ CHECKOUT OPTIONS: :git: https://github.com/ERussel/Foundation-iOS.git :tag: 1.1.0 SoraUI: + :commit: a4eb0139a27d77f11b8e5083e125c38ea82d8e5e :git: https://github.com/ERussel/UIkit-iOS.git - :tag: 1.11.1 Starscream: :git: https://github.com/ERussel/Starscream.git :tag: 4.0.8 @@ -285,7 +285,7 @@ SPEC CHECKSUMS: SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb SoraFoundation: 5b9d3c82d602150d2c2e65481c5eca5f5987c12c SoraKeystore: 92cff6e2a12f212dd64ed089970ff7c365247b1c - SoraUI: e5ceb2cffe40145e589aa464e2e0a8d054011e0b + SoraUI: a3c1163a95c9dd1b6758ca90eb5bda2f4639d634 Sourcery: db66600e8b285c427701821598d07cf3c7e6c476 Starscream: b676ee89781677a2d8d36029a78c970710e2d3eb SubstrateSdk: 1cb78eac5b05f2c259487f3027c3ae807f24c097 @@ -303,6 +303,6 @@ SPEC CHECKSUMS: ZMarkupParser: a92d31ba40695b790f1da5fec98c3d4505341aff ZNSTextAttachment: 4a9b4e8ee1ed087fc893ae6657dfb678f1a00340 -PODFILE CHECKSUM: f37e3724d47617fb7ce7ed5e0a583491617b5899 +PODFILE CHECKSUM: 812e09964099216590d3e74fcb350c38b2ccbd38 COCOAPODS: 1.13.0 From afe6d3cf59cf19da9e9395ac6a8c7448e4657ffe Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 22 Nov 2023 06:56:47 +0100 Subject: [PATCH 203/204] fix sorting for assets --- .../Models/AssetListAssetModel.swift | 23 ++++++++++++++++--- .../Models/AssetListModelHelpers.swift | 6 ++--- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/novawallet/Modules/AssetList/Models/AssetListAssetModel.swift b/novawallet/Modules/AssetList/Models/AssetListAssetModel.swift index d4c42baca7..65d93d0508 100644 --- a/novawallet/Modules/AssetList/Models/AssetListAssetModel.swift +++ b/novawallet/Modules/AssetList/Models/AssetListAssetModel.swift @@ -12,14 +12,31 @@ struct AssetListAssetModel: Identifiable { let externalBalancesResult: Result? let externalBalancesValue: Decimal? - var totalAmount: BigUInt? { + let totalAmountDecimal: Decimal? + let totalAmount: BigUInt? + + init( + assetModel: AssetModel, + balanceResult: Result?, + balanceValue: Decimal?, + externalBalancesResult: Result?, + externalBalancesValue: Decimal? + ) { + self.assetModel = assetModel + self.balanceResult = balanceResult + self.balanceValue = balanceValue + self.externalBalancesResult = externalBalancesResult + self.externalBalancesValue = externalBalancesValue + let maybeBalanceAmount = try? balanceResult?.get() let maybeExternalBalances = try? externalBalancesResult?.get() if let balanceAmount = maybeBalanceAmount, let externalBalancesAmount = maybeExternalBalances { - return balanceAmount + externalBalancesAmount + totalAmount = balanceAmount + externalBalancesAmount } else { - return maybeBalanceAmount ?? maybeExternalBalances + totalAmount = maybeBalanceAmount ?? maybeExternalBalances } + + totalAmountDecimal = totalAmount?.decimal(precision: assetModel.precision) } var totalValue: Decimal? { diff --git a/novawallet/Modules/AssetList/Models/AssetListModelHelpers.swift b/novawallet/Modules/AssetList/Models/AssetListModelHelpers.swift index 784af8215d..2d8d8961c1 100644 --- a/novawallet/Modules/AssetList/Models/AssetListModelHelpers.swift +++ b/novawallet/Modules/AssetList/Models/AssetListModelHelpers.swift @@ -55,8 +55,8 @@ enum AssetListModelHelpers { from assets: [AssetListAssetModel] ) -> ListDifferenceCalculator { let sortingBlock: (AssetListAssetModel, AssetListAssetModel) -> Bool = { model1, model2 in - let balance1 = (try? model1.balanceResult?.get()) ?? 0 - let balance2 = (try? model2.balanceResult?.get()) ?? 0 + let balance1 = model1.totalAmountDecimal ?? 0 + let balance2 = model2.totalAmountDecimal ?? 0 let assetValue1 = model1.totalValue ?? 0 let assetValue2 = model2.totalValue ?? 0 @@ -68,7 +68,7 @@ enum AssetListModelHelpers { } else if assetValue2 > 0 { return false } else if balance1 > 0, balance2 > 0 { - return model1.assetModel.assetId < model2.assetModel.assetId + return balance1 > balance2 } else if balance1 > 0 { return true } else if balance2 > 0 { From 1f6827ab627526889f3ee881648869d13c71713d Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 22 Nov 2023 13:57:02 +0100 Subject: [PATCH 204/204] sync localization --- .../View/AssetDetailsViewLayout.swift | 2 +- .../View/AssetListTotalBalanceCell.swift | 4 +- .../Confirm/SwapConfirmViewController.swift | 2 +- .../Swaps/Setup/SwapSetupViewController.swift | 2 +- .../WalletHistoryFilterViewModel.swift | 2 +- .../TransactionHistoryViewModelFactory.swift | 2 +- novawallet/en.lproj/InfoPlist.strings | 2 +- novawallet/en.lproj/Localizable.strings | 90 ++++++++-------- novawallet/ru.lproj/InfoPlist.strings | 2 +- novawallet/ru.lproj/Localizable.strings | 102 +++++++++--------- 10 files changed, 105 insertions(+), 105 deletions(-) diff --git a/novawallet/Modules/AssetDetails/View/AssetDetailsViewLayout.swift b/novawallet/Modules/AssetDetails/View/AssetDetailsViewLayout.swift index 98ad858b5f..6e0c4e899d 100644 --- a/novawallet/Modules/AssetDetails/View/AssetDetailsViewLayout.swift +++ b/novawallet/Modules/AssetDetails/View/AssetDetailsViewLayout.swift @@ -182,7 +182,7 @@ final class AssetDetailsViewLayout: UIView { ) receiveButton.invalidateLayout() - swapButton.imageWithTitleView?.title = R.string.localizable.commonSwap( + swapButton.imageWithTitleView?.title = R.string.localizable.commonSwapAction( preferredLanguages: languages ) swapButton.invalidateLayout() diff --git a/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift b/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift index 5a841e9e48..c6cc91cd3d 100644 --- a/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift +++ b/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift @@ -54,7 +54,7 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { icon: R.image.iconReceive() ) lazy var swapButton = createActionButton( - title: R.string.localizable.commonSwap( + title: R.string.localizable.commonSwapAction( preferredLanguages: locale.rLanguages ), icon: R.image.iconActionChange() @@ -217,7 +217,7 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { buyButton.imageWithTitleView?.title = R.string.localizable.walletAssetBuy( preferredLanguages: locale.rLanguages ) - swapButton.imageWithTitleView?.title = R.string.localizable.commonSwap( + swapButton.imageWithTitleView?.title = R.string.localizable.commonSwapAction( preferredLanguages: locale.rLanguages ) } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift index 5e8658d657..cd6ceb36ce 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift @@ -35,7 +35,7 @@ final class SwapConfirmViewController: UIViewController, ViewHolder { private func setupLocalization() { rootView.setup(locale: selectedLocale) - title = R.string.localizable.commonSwap( + title = R.string.localizable.commonSwapTitle( preferredLanguages: selectedLocale.rLanguages ) } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index 78fe7fc840..def687e843 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -96,7 +96,7 @@ final class SwapSetupViewController: UIViewController, ViewHolder { } private func setupLocalization() { - title = R.string.localizable.commonSwap(preferredLanguages: selectedLocale.rLanguages) + title = R.string.localizable.commonSwapTitle(preferredLanguages: selectedLocale.rLanguages) rootView.setup(locale: selectedLocale) setupAccessoryView() } diff --git a/novawallet/Modules/TransactionHistory/HistoryFilter/ViewModel/WalletHistoryFilterViewModel.swift b/novawallet/Modules/TransactionHistory/HistoryFilter/ViewModel/WalletHistoryFilterViewModel.swift index d78214a508..83023f212e 100644 --- a/novawallet/Modules/TransactionHistory/HistoryFilter/ViewModel/WalletHistoryFilterViewModel.swift +++ b/novawallet/Modules/TransactionHistory/HistoryFilter/ViewModel/WalletHistoryFilterViewModel.swift @@ -24,7 +24,7 @@ enum WalletHistoryFilterRow: Int, CaseIterable { } case .swaps: return LocalizableResource { locale in - R.string.localizable.commonSwap(preferredLanguages: locale.rLanguages) + R.string.localizable.commonSwapTitle(preferredLanguages: locale.rLanguages) } } } diff --git a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift index b49ab0b83b..35cf41400d 100644 --- a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift +++ b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift @@ -129,7 +129,7 @@ final class TransactionHistoryViewModelFactory { return .init( identifier: data.identifier, timestamp: data.timestamp, - title: R.string.localizable.commonSwap(preferredLanguages: locale.rLanguages), + title: R.string.localizable.commonSwapTitle(preferredLanguages: locale.rLanguages), subtitle: subtitle, amount: balance.amount, amountDetails: amountDetails, diff --git a/novawallet/en.lproj/InfoPlist.strings b/novawallet/en.lproj/InfoPlist.strings index 9c21500255..99fa3b86ad 100644 --- a/novawallet/en.lproj/InfoPlist.strings +++ b/novawallet/en.lproj/InfoPlist.strings @@ -2,4 +2,4 @@ "NSFaceIDUsageDescription" = "Face ID is used to authorize in Nova Wallet"; "NSPhotoLibraryUsageDescription" = "Photo library access is used to allow selection of QR code with addresses, Polkadot Vault transactions, or connect via WalletConnect"; "NSPhotoLibraryAddUsageDescription" = "Save transfer request as a qr code"; -"NSBluetoothAlwaysUsageDescription" = "Bluetooth is used to communicate with Ledger Nano X devices"; +"NSBluetoothAlwaysUsageDescription" = "Bluetooth is used to communicate with Ledger Nano X devices"; \ No newline at end of file diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 0ecf15cac1..8fa251540d 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -114,7 +114,6 @@ "settings.preferences" = "Preferences"; "settings.security" = "Security"; "settings.community" = "Community"; -"settings.wiki" = "Wiki"; "confirmation.skip.action" = "Skip process"; "account.info.title" = "Account"; "account.info.name.title" = "Name"; @@ -535,6 +534,7 @@ "common.unknown" = "Unknown"; "staking.month.period.title" = "Monthly"; "staking.year.period.title" = "Yearly"; +"common.positive.amount" = "Amount must be positive"; "qr.scan.error.no.info" = "QR can't be decoded"; "qr.scan.upload.gallery" = "Upload from gallery"; "staking.unstake.crossed.min.message" = "When unstaking partially, you should leave at least %@ in stake. Do you want to perform full unstake by unstaking remaining %@ as well?"; @@ -952,7 +952,6 @@ "with.yield.boost" = "with Yield Boost"; "without.yield.boost" = "without Yield Boost"; "common.no.changes" = "No changes"; -"common.not.enough.fee.message_v3.8.0" = "You don’t have enough balance to pay the network fee of %@.\nAvailable balance to pay fee after operation: %@"; "yield.boost.not.enough.execution.fee.message" = "You don’t have enough balance to pay the network fee of %@ and the yield boost execution fee of %@.\nAvailable balance to pay the fee: %@"; "yield.boost.not.enough.execution.fee.title" = "Not enough tokens to pay first execution fee"; "yield.boost.not.enough.threshold.message" = "You don’t have enough balance to pay the network fee of %@ and not drop below the threshold %@.\nAvailable balance to pay the fee: %@"; @@ -1375,57 +1374,58 @@ "wallet.list.empty.action.title" = "Buy tokens"; "asset.operation.send.empty.state.message" = "You don’t have tokens to send.\nBuy or Receive tokens to your\naccount."; "governance.referendums.status.deciding" = "Deciding"; -"common.swap" = "Swap"; -"swaps.setup.asset.select.subtitle" = "Select a token"; -"swaps.setup.asset.pay.title" = "Pay"; -"swaps.setup.asset.receive.title" = "Receive"; -"swaps.setup.asset.action.select.tokens" = "Select tokens to swap"; -"swaps.setup.asset.action.select.receive" = "Select a token to receive"; -"swaps.setup.asset.action.select.pay" = "Select a token to pay"; -"swaps.setup.asset.action.enter.amount" = "Enter amount"; -"swaps.setup.asset.select.pay.title" = "You pay"; -"swaps.setup.asset.select.receive.title" = "You receive"; -"swaps.setup.asset.max" = "Max:"; -"swaps.setup.details.rate" = "Rate"; -"swaps.setup.details.title" = "Swap details"; -"swaps.pay.token.selection.title" = "Token to pay"; -"swaps.receive.token.selection.title" = "Token to receive"; -"swaps.setup.settings.title" = "Swap settings"; -"swaps.setup.slippage" = "Slippage"; -"swaps.setup.price.difference" = "Price difference"; -"swaps.setup.slippage.error.amount.bounds" = "Enter a value between %@ and %@"; -"swaps.setup.slippage.warning.low.amount" = "Transaction might be reverted because of low slippage tolerance."; -"swaps.setup.slippage.warning.high.amount" = "Transaction might be frontrun because of high slippage."; -"swaps.setup.slippage.description" = "Swap slippage is a common occurrence in decentralized trading where the final price of a swap transaction might slightly differ from the expected price, due to changing market conditions."; -"swaps.rate.description" = "Exchange rate between two different cryptocurrencies. It represents how much of one cryptocurrency you can get in exchange for a certain amount of another cryptocurrency."; -"swaps.network.fee.description" = "A network fees charged by the blockchain to process and validate any transactions. May vary depending on network conditions or transaction speed."; -"swaps.setup.error.not.enough.liquidity.title" = "Pool doesn’t have enough liquidity to swap"; -"swaps.setup.error.insufficient.balance.fee.swap.message" = "You can swap up to %@ since you need to pay %@ for network fee and also convert %@ to %@ to meet %@ minimum balance."; -"swaps.setup.error.insufficient.balance.fee.native.message" = "You can swap up to %@ since you need to pay %@ for network fee."; -"common.swap.max" = "Swap max"; "common.alert.external.link.disclaimer.title" = "Continue in browser?"; "common.alert.external.link.disclaimer.message" = "To continue the purchase you will be redirected from Nova Wallet app to %@"; "polkadot.staking.promotion.title" = "Boost your DOT 🚀"; "polkadot.staking.promotion.message" = "Received your DOT back from crowdloans? Start staking your DOT today to get the maximum possible rewards!"; -"swaps.setup.network.fee.token.title" = "Token for paying network fee"; -"swaps.setup.network.fee.token.hint" = "Network fee is added on top of entered amount"; +"swaps.violating.consumers.message" = "You should keep at least %@ after paying %@ network fee as you are holding non sufficient tokens"; +"common.receive.not.sufficient.native.asset.error" = "You must keep at least %@ to receive %@ token"; +"swaps.rate.description" = "Exchange rate between two different cryptocurrencies. It represents how much of one cryptocurrency you can get in exchange for a certain amount of another cryptocurrency."; "swaps.setup.price.difference.description" = "Price difference refers to the difference in price between two different assets. When making a swap in crypto, the price difference is usually the difference between the price of the asset you are swapping for and the price of the asset you are swapping with."; +"swaps.setup.slippage.description" = "Swap slippage is a common occurrence in decentralized trading where the final price of a swap transaction might slightly differ from the expected price, due to changing market conditions."; +"swaps.network.fee.description" = "A network fees charged by the blockchain to process and validate any transactions. May vary depending on network conditions or transaction speed."; +"swaps.setup.asset.action.select.pay" = "Select a token to pay"; +"swaps.not.enough.liquidity" = "Not enough liquidity"; +"swaps.not.enough.tokens" = "Not enough tokens to swap"; +"common.receive.at.least.ed.error" = "You can’t receive less than %@"; "swaps.error.rate.was.updated.title" = "Swap rate was updated"; -"swaps.error.rate.was.updated.message" = "Old rate: %@.\nNew rate: %@"; -"common.action.repeat.operation" = "Repeat the operation"; +"swaps.setup.error.not.enough.liquidity.title" = "Pool doesn’t have enough liquidity to swap"; +"common.dust.remains.title" = "Too small amount remains on your balance"; +"swaps.dust.remains.fee.native.asset.message" = "You should leave at least %1$@ on your balance. Do you want to perform full swap by adding remaining %2$@ as well?"; +"swaps.dust.remains.fee.pay.asset.message" = "You should keep at least %1$@ after paying %2$@ network fee and converting %3$@ to %4$@ to meet %5$@ minimum balance.\n\nDo you want to fully swap by adding remaining %6$@ as well?"; +"swaps.setup.error.insufficient.balance.fee.native.message" = "You can swap up to %1$@ since you need to pay %2$@ for network fee."; +"swaps.setup.error.insufficient.balance.fee.swap.message" = "You can swap up to %1$@ since you need to pay %2$@ for network fee and also convert %3$@ to %4$@ to meet %5$@ minimum balance."; +"common.swap.max" = "Swap max"; "swaps.setup.deposit.by.cross.chain.transfer.title" = "Cross-chain transfer"; +"swaps.setup.deposit.title" = "Get %@ using"; "swaps.setup.deposit.by.cross.chain.transfer.subtitle" = "Transfer %@ from another network"; "swaps.setup.deposit.by.receive.subtitle" = "Receive %@ with QR or your address"; "swaps.setup.deposit.by.buy.subtitle" = "Instantly buy %@ with a credit card"; -"swaps.setup.deposit.title" = "Get %@ using"; -"common.dust.remains.title" = "Too small amount remains on your balance"; -"swaps.dust.remains.fee.native.asset.message" = "You should leave at least %@ on your balance. Do you want to perform full swap by adding remaining %@ as well?"; -"swaps.dust.remains.fee.pay.asset.message" = "You should keep at least %@ after paying %@ network fee and converting %@ to %@ to meet %@ minimum balance.\n\nDo you want to fully swap by adding remaining %@ as well?"; -"common.receive.at.least.ed.error" = "You can’t receive less than %@"; -"common.receive.not.sufficient.native.asset.error" = "You must keep at least %@ to receive %@ token"; -"swaps.violating.consumers.message" = "You should keep at least %@ after paying %@ network fee as you are holding non sufficient tokens."; -"swaps.not.enough.tokens" = "Not enough tokens to swap"; -"swaps.not.enough.liquidity" = "Not enough liquidity"; -"swaps.pay.asset.fee.ed.message" = "To pay network fee with %@, Nova will automatically swap %@ for %@ to maintain your account's minimum %@ balance."; "swaps.setup.deposit.button.title" = "Get %@"; -"common.positive.amount" = "Amount must be positive"; +"swaps.setup.network.fee.token.title" = "Token for paying network fee"; +"swaps.setup.network.fee.token.hint" = "Network fee is added on top of entered amount"; +"swaps.pay.asset.fee.ed.message" = "To pay network fee with %@, Nova will automatically swap %@ for %@ to maintain your account\'s minimum %@ balance."; +"swaps.setup.slippage.error.amount.bounds" = "Enter a value between %@ and %@"; +"swaps.setup.slippage.warning.low.amount" = "Transaction might be reverted because of low slippage tolerance."; +"swaps.setup.slippage.warning.high.amount" = "Transaction might be frontrun because of high slippage."; +"swaps.setup.slippage" = "Slippage"; +"swaps.setup.settings.title" = "Swap settings"; +"swaps.pay.token.selection.title" = "Token to pay"; +"swaps.receive.token.selection.title" = "Token to receive"; +"swaps.setup.asset.select.subtitle" = "Select a token"; +"swaps.setup.asset.pay.title" = "Pay"; +"swaps.setup.asset.receive.title" = "Receive"; +"swaps.setup.details.rate" = "Rate"; +"swaps.setup.price.difference" = "Price difference"; +"swaps.setup.asset.max" = "Max:"; +"swaps.setup.asset.select.pay.title" = "You pay"; +"swaps.setup.asset.select.receive.title" = "You receive"; +"swaps.setup.details.title" = "Swap details"; +"swaps.setup.asset.action.select.receive" = "Select a token to receive"; +"common.swap.action" = "Swap"; +"swaps.setup.asset.action.enter.amount" = "Enter amount"; +"common.swap.title" = "Swap"; +"common.action.repeat.operation" = "Repeat the operation"; +"swaps.error.rate.was.updated.message" = "Old rate: %@.\nNew rate: %@"; +"settings.wiki" = "Wiki"; +"common.not.enough.fee.message_v3.8.0" = "You don’t have enough balance to pay the network fee of %@.\nAvailable balance to pay fee after operation: %@"; \ No newline at end of file diff --git a/novawallet/ru.lproj/InfoPlist.strings b/novawallet/ru.lproj/InfoPlist.strings index 3a4b45f510..56ce3b8975 100644 --- a/novawallet/ru.lproj/InfoPlist.strings +++ b/novawallet/ru.lproj/InfoPlist.strings @@ -2,4 +2,4 @@ "NSFaceIDUsageDescription" = "Face ID используется для авторизации в Nova Wallet"; "NSPhotoLibraryUsageDescription" = "Доступ к фотогалерее используется для выбора QR-кода с адресами, транзакциями Polkadot Vault или подключения через WalletConnect"; "NSPhotoLibraryAddUsageDescription" = "Вы можете сохранить запрос на перевод в виде QR кода"; -"NSBluetoothAlwaysUsageDescription" = "Bluetooth используется для взаимодействия с устройствами Ledger Nano X"; +"NSBluetoothAlwaysUsageDescription" = "Bluetooth используется для взаимодействия с устройствами Ledger Nano X"; \ No newline at end of file diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index d185e8a65c..71e0a226f9 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -114,7 +114,6 @@ "settings.preferences" = "Предпочтения"; "settings.security" = "Безопасность"; "settings.community" = "Сообщество"; -"settings.wiki" = "Руководство пользователя"; "confirmation.skip.action" = "Пропустить"; "account.info.title" = "Аккаунт"; "account.info.name.title" = "Имя"; @@ -535,6 +534,7 @@ "common.unknown" = "Неизвестно"; "staking.month.period.title" = "Eжемесячно"; "staking.year.period.title" = "Eжегодно"; +"common.positive.amount" = "Сумма должна быть больше нуля"; "qr.scan.error.no.info" = "Не удается декодировать QR"; "qr.scan.upload.gallery" = "Из галереи"; "staking.unstake.crossed.min.message" = "При частичном выводе средств вы должны оставить в стейке не менее %@. Хотите ли вы полностью вывести средства, также разблокировав оставшиеся %@?"; @@ -952,7 +952,6 @@ "with.yield.boost" = "с Yield Boost"; "without.yield.boost" = "без Yield Boost"; "common.no.changes" = "Нет изменений"; -"common.not.enough.fee.message_v3.8.0" = "У вас недостаточно средств для оплаты комиссии сети в размере %@.\nДоступный баланс для оплаты комиссии после операции: %@"; "yield.boost.not.enough.execution.fee.message" = "У вас недостаточно средств для оплаты сетевой комиссии в размере %@ и комиссии за выполнение первой Yield Boost операции в размере %@. \nДоступный баланс для оплаты комиссии: %@"; "yield.boost.not.enough.execution.fee.title" = "Недостаточно токенов для оплаты комиссии за первое исполнение"; "yield.boost.not.enough.threshold.message" = "У вас недостаточно средств, чтобы оплатить комиссию сети в размере %@ и не опуститься ниже порога %@.\nДоступный баланс для оплаты комиссии: %@"; @@ -1375,57 +1374,58 @@ "wallet.list.empty.action.title" = "Купить токены"; "asset.operation.send.empty.state.message" = "У вас нет токенов для отправки.\nКупите или получите токены\nна свой аккаунт."; "governance.referendums.status.deciding" = "Решение"; -"common.swap" = "Обмен"; -"swaps.setup.asset.select.subtitle" = "Выберите токeн"; -"swaps.setup.asset.pay.title" = "Оплата в"; -"swaps.setup.asset.receive.title" = "Получение"; -"swaps.setup.asset.action.select.tokens" = "Выберите токены для обмена"; -"swaps.setup.asset.action.select.receive" = "Выберите получаемый токен"; -"swaps.setup.asset.action.select.pay" = "Выберите отдаваемый токен"; -"swaps.setup.asset.action.enter.amount" = "Введите сумму"; -"swaps.setup.asset.select.pay.title" = "Вы платите"; -"swaps.setup.asset.select.receive.title" = "Вы получаете"; -"swaps.setup.asset.max" = "Максимум:"; -"swaps.setup.details.rate" = "Курс"; -"swaps.setup.details.title" = "Детали обмена"; -"swaps.pay.token.selection.title" = "Токен для оплаты"; -"swaps.receive.token.selection.title" = "Токен для получения"; -"swaps.setup.settings.title" = "Настройки обмена"; -"swaps.setup.slippage" = "Slippage"; -"swaps.setup.price.difference" = "Ценовая разница"; -"swaps.setup.slippage.error.amount.bounds" = "Введите значение между %@ и %@"; -"swaps.setup.slippage.warning.low.amount" = "Транзакция может быть отменена из-за низкого значения проскальзывания."; -"swaps.setup.slippage.warning.high.amount" = "Транзакция может быть приостановлена из-за высокого значения проскальзывания."; -"swaps.setup.slippage.description" = "Обменное проскальзывание - обычное явление в децентрализованной торговле, где окончательная цена транзакции обмена может незначительно отличаться от ожидаемой цены из-за изменения рыночных условий."; -"swaps.rate.description" = "Обменный курс между двумя различными криптовалютами. Он представляет, сколько одной криптовалюты вы можете получить в обмен на определенное количество другой криптовалюты."; -"swaps.network.fee.description" = "Это комиссия сети, взимаемая блокчейном за обработку и подтверждение любых транзакций. Она может изменяться в зависимости от условий в сети или скорости выполнения транзакции."; -"swaps.setup.error.not.enough.liquidity.title" = "В пуле недостаточно ликвидности для обмена"; -"swaps.setup.error.insufficient.balance.fee.swap.message" = "You can swap up to %@ since you need to pay %@ for network fee and also convert %@ to %@ to meet %@ minimum balance."; -"swaps.setup.error.insufficient.balance.fee.native.message" = "You can swap up to %@ since you need to pay %@ for network fee."; -"common.swap.max" = "Swap max"; "common.alert.external.link.disclaimer.title" = "Продолжить в браузере?"; "common.alert.external.link.disclaimer.message" = "Для продолжения покупки вы будете перенаправлены из приложения Nova Wallet на сайт %@"; -"polkadot.staking.promotion.title" = "Максимизируйте\nнаграды от DOT 🚀"; +"polkadot.staking.promotion.title" = "Максимизируйте награды от DOT 🚀"; "polkadot.staking.promotion.message" = "Получили свои DOT из краудлоунов? Начните стейкать DOT уже сегодня, чтобы получить максимальные вознаграждения!"; +"swaps.violating.consumers.message" = "Вы должны сохранить как минимум %@ после оплаты сетевой комиссии %@, поскольку вы храните не самодостаточные токены"; +"common.receive.not.sufficient.native.asset.error" = "Вы должны сохранить как минимум %@, чтобы получить %@ токены"; +"swaps.rate.description" = "Курс обмена двух разных криптовалют. Он показывает, сколько криптовалюты вы можете получить в обмен на определенное количество другой криптовалюты."; +"swaps.setup.price.difference.description" = "Разница в цене представляет собой разницу между двумя различными активами. При обмене криптовалюты под разницей в цене обычно имеется ввиду разница между ценой актива, которую вы получаете и ценой актива, которую вы платите."; +"swaps.setup.slippage.description" = "Проскальзывание - распространенное явление в децентрализованной торговле, когда конечная цена сделки по обмену может незначительно отличаться от ожидаемой в связи с изменением рыночных условий."; +"swaps.network.fee.description" = "Комиссия сети, взимается блокчейном за обработку и проверку транзакций. Может варьироваться в зависимости от условий сети или скорости транзакции."; +"swaps.setup.asset.action.select.pay" = "Выберите токен для оплаты"; +"swaps.not.enough.liquidity" = "Недостаточно ликвидности"; +"swaps.not.enough.tokens" = "Недостаточно токенов для обмена"; +"common.receive.at.least.ed.error" = "Вы не можете получить меньше %@"; +"swaps.error.rate.was.updated.title" = "Обменный курс изменился"; +"swaps.setup.error.not.enough.liquidity.title" = "В пуле недостаточно ликвидности для обмена"; +"common.dust.remains.title" = "На вашем балансе остается слишком маленькая сумма"; +"swaps.dust.remains.fee.native.asset.message" = "На вашем балансе должно оставаться не менее %1$@. Хотите обменять максимум, добавив также оставшиеся %2$@?"; +"swaps.dust.remains.fee.pay.asset.message" = "На вашем балансе должно оставаться как минимум %1$@ после оплаты сетевой комиссии %2$@ и конвертации %3$@ в %4$@, для достижения минимального баланса %5$@.\n\nХотите обменять максимум, добавив еще %6$@?"; +"swaps.setup.error.insufficient.balance.fee.native.message" = "Вы можете обменять до %1$@ так как вам нужно заплатить комиссию сети в размере %2$@."; +"swaps.setup.error.insufficient.balance.fee.swap.message" = "Вы можете обменять до %1$@ так как вам нужно заплатить комиссию сети в размере %2$@, а также конвертировать %3$@ в %4$@, чтобы сохранить минимальный баланс %5$@."; +"common.swap.max" = "Использовать максимум"; +"swaps.setup.deposit.by.cross.chain.transfer.title" = "Межсетевой перевод"; +"swaps.setup.deposit.title" = "Получить %@ с помощью"; +"swaps.setup.deposit.by.cross.chain.transfer.subtitle" = "Перевести %@ из другой сети"; +"swaps.setup.deposit.by.receive.subtitle" = "Получить %@ по QR или вашему адресу"; +"swaps.setup.deposit.by.buy.subtitle" = "Мгновенная покупка %@ с помощью кредитной карты"; +"swaps.setup.deposit.button.title" = "Получить %@"; "swaps.setup.network.fee.token.title" = "Токен для оплаты комиссии сети"; -"swaps.setup.network.fee.token.hint" = "Комиссия сети добавляется к введенной сумме."; -"swaps.setup.price.difference.description" = "Разница в цене относится к разнице в цене между двумя различными активами. При совершении обмена в криптовалюте разница в цене обычно представляет собой разницу между ценой актива, на который вы меняете, и ценой актива, на который вы меняетесь."; -"swaps.error.rate.was.updated.title" = "Обменный курс был обновлен"; -"swaps.error.rate.was.updated.message" = "Было: %@.\nСтало: %@"; +"swaps.setup.network.fee.token.hint" = "Комиссия сети добавится к введенной сумме"; +"swaps.pay.asset.fee.ed.message" = "Чтобы оплатить комиссию сети с помощью %@, Nova автоматически обменяет %@ на %@, чтобы поддерживать минимальный %@ баланс вашей учетной записи."; +"swaps.setup.slippage.error.amount.bounds" = "Введите значение между %@ и %@"; +"swaps.setup.slippage.warning.low.amount" = "Транзакция может быть отменена из-за низкой устойчивости к проскальзыванию."; +"swaps.setup.slippage.warning.high.amount" = "Транзакция может подвергнуться фронтрану из-за высокого проскальзывания"; +"swaps.setup.slippage" = "Проскальзывание"; +"swaps.setup.settings.title" = "Настройки обмена"; +"swaps.pay.token.selection.title" = "Токен для оплаты"; +"swaps.receive.token.selection.title" = "Токен для получения"; +"swaps.setup.asset.select.subtitle" = "Выберите токен"; +"swaps.setup.asset.pay.title" = "Заплатить"; +"swaps.setup.asset.receive.title" = "Получить"; +"swaps.setup.details.rate" = "Курс"; +"swaps.setup.price.difference" = "Разница в цене"; +"swaps.setup.asset.max" = "Макс:"; +"swaps.setup.asset.select.pay.title" = "Вы платите"; +"swaps.setup.asset.select.receive.title" = "Вы получаете"; +"swaps.setup.details.title" = "Детали обмена"; +"swaps.setup.asset.action.select.receive" = "Выберите токен для получения"; +"common.swap.action" = "Обменять"; +"swaps.setup.asset.action.enter.amount" = "Введите сумму"; +"common.swap.title" = "Обмен"; "common.action.repeat.operation" = "Повторить операцию"; -"swaps.setup.deposit.by.cross.chain.transfer.title" = "Перевод между сетями"; -"swaps.setup.deposit.by.cross.chain.transfer.subtitle" = "Перевести %@ из другой сети"; -"swaps.setup.deposit.by.receive.subtitle" = "Получить %@ используя QR-код или адрес"; -"swaps.setup.deposit.by.buy.subtitle" = "Купить %@ используя банковскую карту"; -"swaps.setup.deposit.title" = "Пополнить %@"; -"common.dust.remains.title" = "Баланс ниже минимального"; -"swaps.dust.remains.fee.native.asset.message" = "Вам необходимо оставить минимум %@ на вашем балансе. Вы хотите добавить к обмену оставшиеся %@ тоже?"; -"swaps.dust.remains.fee.pay.asset.message" = "Вам необходимо оставить минимум %@ после оплаты %@ комиссии сети и обмена %@ на %@ для поддержания минимального баланса %@.\n\nВы хотите добавить к обмену оставшиеся %@ тоже?"; -"common.receive.at.least.ed.error" = "Вы не можете получить меньше чем %@"; -"common.receive.not.sufficient.native.asset.error" = "У вас должно быть минимум %@ для получения %@ токена"; -"swaps.violating.consumers.message" = "Вам необходимо оставить минимум %@ после уплаты %@ комиссии сети так как вы владеете несамодостаточными токенами."; -"swaps.not.enough.tokens" = "Недостаточно токенов для обмена"; -"swaps.not.enough.liquidity" = "Недостаточно ликвидности"; -"swaps.pay.asset.fee.ed.message" = "Для оплаты комиссии сети %@ токеном, Nova автоматически поменяет %@ в %@ для сохранения минимального %@ баланса аккаунта."; -"swaps.setup.deposit.button.title" = "Пополнить %@"; -"common.positive.amount" = "Значение должно быть положительным"; +"swaps.error.rate.was.updated.message" = "Было: %@.\nСтало: %@"; +"settings.wiki" = "Руководство пользователя"; +"common.not.enough.fee.message_v3.8.0" = "У вас недостаточно средств для оплаты комиссии сети в размере %@.\nДоступный баланс для оплаты комиссии после операции: %@";