From 01775db1db9158dc68d1cc5af5b8e22707d7f995 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sat, 21 Dec 2024 21:42:06 +0100 Subject: [PATCH 1/8] prevent screen sleep during swap --- novawallet.xcodeproj/project.pbxproj | 4 ++++ .../Common/Helpers/OperatingSystemApi.swift | 24 +++++++++++++++++++ .../Execution/SwapExecutionInteractor.swift | 7 ++++++ .../Execution/SwapExecutionViewFactory.swift | 1 + 4 files changed, 36 insertions(+) create mode 100644 novawallet/Common/Helpers/OperatingSystemApi.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index c14d1337f..710bcbcdb 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -880,6 +880,7 @@ 0CFA161E2B0CED07007AF885 /* GovTreasurySpentLocalHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFA161D2B0CED07007AF885 /* GovTreasurySpentLocalHandler.swift */; }; 0CFA16202B0CEF31007AF885 /* GovTreasuryApproveHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFA161F2B0CEF31007AF885 /* GovTreasuryApproveHandler.swift */; }; 0CFFB9D32D11A67C00172E8C /* XcmTokensArrivalDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFFB9D22D11A67C00172E8C /* XcmTokensArrivalDetector.swift */; }; + 0CFFB9D92D17592500172E8C /* OperatingSystemApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFFB9D82D17592500172E8C /* OperatingSystemApi.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 */; }; @@ -6211,6 +6212,7 @@ 0CFA161D2B0CED07007AF885 /* GovTreasurySpentLocalHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovTreasurySpentLocalHandler.swift; sourceTree = ""; }; 0CFA161F2B0CEF31007AF885 /* GovTreasuryApproveHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovTreasuryApproveHandler.swift; sourceTree = ""; }; 0CFFB9D22D11A67C00172E8C /* XcmTokensArrivalDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmTokensArrivalDetector.swift; sourceTree = ""; }; + 0CFFB9D82D17592500172E8C /* OperatingSystemApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatingSystemApi.swift; sourceTree = ""; }; 0D37CF4AFB06AF3AC2F78057 /* ImportCloudPasswordPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImportCloudPasswordPresenter.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 = ""; }; @@ -18751,6 +18753,7 @@ 0CDEF1652C27EC83003878F2 /* RuntimeMetadataRepositoryFactory.swift */, 0C7104782C2AC0F300487E64 /* InMemoryCaching.swift */, 0C33E8B92D011D2E0090096A /* Debouncer.swift */, + 0CFFB9D82D17592500172E8C /* OperatingSystemApi.swift */, ); path = Helpers; sourceTree = ""; @@ -28188,6 +28191,7 @@ 848CCB442832EE9B00A1FD00 /* GeneralStorageSubscriptionFactory.swift in Sources */, 8483B15828F98C9F0048B295 /* ReferendumVotersViewModel.swift in Sources */, 2D7BEBD62C23818B00BBCB57 /* NetworkManageNodeWireframe.swift in Sources */, + 0CFFB9D92D17592500172E8C /* OperatingSystemApi.swift in Sources */, 84BB3CEE267CD6B500676FFE /* CrowdloanContributionDict.swift in Sources */, 8473F4B4282BD5A1007CC55A /* StakingRelaychainInteractor.swift in Sources */, 845B821B26EF80BC00D25C72 /* MetaAccountModel.swift in Sources */, diff --git a/novawallet/Common/Helpers/OperatingSystemApi.swift b/novawallet/Common/Helpers/OperatingSystemApi.swift new file mode 100644 index 000000000..cd9edc5a5 --- /dev/null +++ b/novawallet/Common/Helpers/OperatingSystemApi.swift @@ -0,0 +1,24 @@ +import UIKit + +protocol OperatingSystemMediating: AnyObject { + func disableScreenSleep() + func enableScreenSleep() +} + +final class OperatingSystemMediator { + let application: UIApplication + + init(application: UIApplication = .shared) { + self.application = application + } +} + +extension OperatingSystemMediator: OperatingSystemMediating { + func disableScreenSleep() { + application.isIdleTimerDisabled = true + } + + func enableScreenSleep() { + application.isIdleTimerDisabled = false + } +} diff --git a/novawallet/Modules/Swaps/Execution/SwapExecutionInteractor.swift b/novawallet/Modules/Swaps/Execution/SwapExecutionInteractor.swift index 957299b62..ef2ff18bb 100644 --- a/novawallet/Modules/Swaps/Execution/SwapExecutionInteractor.swift +++ b/novawallet/Modules/Swaps/Execution/SwapExecutionInteractor.swift @@ -4,19 +4,24 @@ final class SwapExecutionInteractor { weak var presenter: SwapExecutionInteractorOutputProtocol? let assetsExchangeService: AssetsExchangeServiceProtocol + let osMediator: OperatingSystemMediating let operationQueue: OperationQueue init( assetsExchangeService: AssetsExchangeServiceProtocol, + osMediator: OperatingSystemMediating, operationQueue: OperationQueue ) { self.assetsExchangeService = assetsExchangeService + self.osMediator = osMediator self.operationQueue = operationQueue } } extension SwapExecutionInteractor: SwapExecutionInteractorInputProtocol { func submit(using estimation: AssetExchangeFee) { + osMediator.disableScreenSleep() + let wrapper = assetsExchangeService.submit( using: estimation, notifyingIn: .main @@ -29,6 +34,8 @@ extension SwapExecutionInteractor: SwapExecutionInteractorInputProtocol { inOperationQueue: operationQueue, runningCallbackIn: .main ) { [weak self] result in + self?.osMediator.enableScreenSleep() + switch result { case let .success(amount): self?.presenter?.didCompleteFullExecution(received: amount) diff --git a/novawallet/Modules/Swaps/Execution/SwapExecutionViewFactory.swift b/novawallet/Modules/Swaps/Execution/SwapExecutionViewFactory.swift index 1e15f7834..cd32822e6 100644 --- a/novawallet/Modules/Swaps/Execution/SwapExecutionViewFactory.swift +++ b/novawallet/Modules/Swaps/Execution/SwapExecutionViewFactory.swift @@ -11,6 +11,7 @@ struct SwapExecutionViewFactory { let interactor = SwapExecutionInteractor( assetsExchangeService: flowState.setupAssetExchangeService(), + osMediator: OperatingSystemMediator(), operationQueue: OperationManagerFacade.sharedDefaultQueue ) From bc5d72bb97e601eae5f50b21b6890c6454f3f0b5 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 24 Dec 2024 12:30:42 +0100 Subject: [PATCH 2/8] allow generic address in DApp --- novawallet.xcodeproj/project.pbxproj | 2 ++ .../Common/Helpers/AddressConversion.swift | 18 ++++++++++++++++++ ...perationConfirmInteractor+Proccessing.swift | 12 ++++-------- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 710bcbcdb..6a229538b 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -165,6 +165,7 @@ 0C252BBF2B3D3CEB0047308F /* TypeRegistry+Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C252BBE2B3D3CEB0047308F /* TypeRegistry+Node.swift */; }; 0C259EA42B46721B00CB86E4 /* ProxyMessageSheetViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C259EA32B46721B00CB86E4 /* ProxyMessageSheetViewFactory.swift */; }; 0C259EA82B46C55C00CB86E4 /* ExtrinsicSigningErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C259EA72B46C55C00CB86E4 /* ExtrinsicSigningErrorHandling.swift */; }; + 0C28DF0D2D1AC69F0016DB8E /* SNAddressType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DAC197268D3DD9002D0DF4 /* SNAddressType.swift */; }; 0C29B5382A4C68A500E35C6D /* AnimationUpdatibleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C29B5372A4C68A500E35C6D /* AnimationUpdatibleView.swift */; }; 0C2A3C932CDC813B00A0E2B3 /* AssetsExchangeOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2A3C922CDC813B00A0E2B3 /* AssetsExchangeOperationFactory.swift */; }; 0C2A3C952CDC8ADB00A0E2B3 /* SwapAssetSelectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2A3C942CDC8ADB00A0E2B3 /* SwapAssetSelectionModel.swift */; }; @@ -25175,6 +25176,7 @@ 7778FE3E2B90CCDD0023E801 /* SubstrateStorageVersion.swift in Sources */, 7778FE3D2B90CCA20023E801 /* String+Split.swift in Sources */, 7778FE502B9108AA0023E801 /* CompoundOperationWrapper+Result.swift in Sources */, + 0C28DF0D2D1AC69F0016DB8E /* SNAddressType.swift in Sources */, 0C8FDBA02CAD022700775D7F /* SubstrateDataModel.xcdatamodeld in Sources */, 77EDF9242B96DCEB003266B1 /* ProxyAccountModel.swift in Sources */, 7778FE402B90F42D0023E801 /* PriceDataMapper.swift in Sources */, diff --git a/novawallet/Common/Helpers/AddressConversion.swift b/novawallet/Common/Helpers/AddressConversion.swift index 31d0ef4c6..3265c7491 100644 --- a/novawallet/Common/Helpers/AddressConversion.swift +++ b/novawallet/Common/Helpers/AddressConversion.swift @@ -58,6 +58,24 @@ extension AccountAddress { } } + func toChainAccountIdOrSubstrateGeneric( + using conversion: ChainFormat + ) throws -> AccountId { + switch conversion { + case .ethereum: + return try extractEthereumAccountId() + case let .substrate(prefix): + let addressFactory = SS58AddressFactory() + let type = try addressFactory.type(fromAddress: self).uint16Value + + guard type == prefix || type == SNAddressType.genericSubstrate.rawValue else { + throw AccountAddressConversionError.invalidChainAddress + } + + return try addressFactory.accountId(fromAddress: self, type: type) + } + } + func toSubstrateAccountId(using prefix: UInt16? = nil) throws -> AccountId { let factory = SS58AddressFactory() diff --git a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Proccessing.swift b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Proccessing.swift index f45ab0d7d..212fa16fb 100644 --- a/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Proccessing.swift +++ b/novawallet/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmInteractor+Proccessing.swift @@ -61,7 +61,10 @@ extension DAppOperationConfirmInteractor { let extrinsic = try extrinsicOperation.extractNoCancellableResultData() - guard let extrinsicAccountId = try? extrinsic.address.toAccountId(using: chain.chainFormat) else { + guard + let extrinsicAccountId = try? extrinsic.address.toChainAccountIdOrSubstrateGeneric( + using: chain.chainFormat + ) else { throw DAppOperationConfirmInteractorError.extrinsicBadField(name: "address: \(extrinsic.address)") } @@ -72,13 +75,6 @@ extension DAppOperationConfirmInteractor { throw ChainAccountFetchingError.accountNotExists } - guard accountResponse.toAddress() == extrinsic.address else { - throw DAppOperationConfirmInteractorError.addressMismatch( - actual: extrinsic.address, - expected: accountResponse.toAddress() ?? "" - ) - } - guard let specVersion = BigUInt.fromHexString(extrinsic.specVersion) else { throw DAppOperationConfirmInteractorError.extrinsicBadField(name: "specVersion") From adea36a1f83e23429a38fcded0af3ebc14764c0a Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 27 Dec 2024 14:07:15 +0100 Subject: [PATCH 3/8] wip refactor native balance --- novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift b/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift index b3615485c..dcbceb27c 100644 --- a/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift +++ b/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift @@ -45,7 +45,7 @@ struct SwapMaxModel { return feeModel.hasOriginPostSubmissionByAccount } - var shouldKeepMinBalance: Bool { + var shouldKeepNativeMinBalance: Bool { needMinBalanceDueConsumers || needMinBalanceDueToPostsubmissionFee || needMinBalanceDueToReceiveInsufficiency @@ -54,7 +54,7 @@ struct SwapMaxModel { private func calculateForNativeAsset(_ payChainAsset: ChainAsset, balance: AssetBalance) -> Decimal { var maxAmount = balance.transferable - if shouldKeepMinBalance, !minBalanceCoveredByFrozen(in: balance) { + if shouldKeepNativeMinBalance, !minBalanceCoveredByFrozen(in: balance) { let minBalance = payAssetExistense?.minBalance ?? 0 maxAmount = maxAmount.subtractOrZero(minBalance) } From 7477d1bfa198f3403ec6352f55b13416c6c4f43d Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 27 Dec 2024 17:04:32 +0100 Subject: [PATCH 4/8] add fungibility restriction validation --- .../Swaps/Base/Model/SwapMaxModel.swift | 37 +++++++++++++++---- .../Validation/SwapDataValidatorFactory.swift | 13 ++++++- .../Validation/SwapErrorPresentable.swift | 4 +- .../Modules/Swaps/Validation/SwapModel.swift | 32 ++++++++++++++++ 4 files changed, 76 insertions(+), 10 deletions(-) diff --git a/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift b/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift index dcbceb27c..3359a7e2b 100644 --- a/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift +++ b/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift @@ -10,8 +10,11 @@ struct SwapMaxModel { let receiveAssetExistense: AssetBalanceExistence? let accountInfo: AccountInfo? - func minBalanceCoveredByFrozen(in balance: AssetBalance) -> Bool { - let minBalance = payAssetExistense?.minBalance ?? 0 + func minBalanceCoveredByFrozen( + in balance: AssetBalance, + assetExistence: AssetBalanceExistence? + ) -> Bool { + let minBalance = assetExistence?.minBalance ?? 0 return balance.transferable + minBalance <= balance.balanceCountingEd } @@ -35,6 +38,15 @@ struct SwapMaxModel { accountInfo?.hasConsumers ?? false } + var needMinBalanceDueToFungibility: Bool { + /* + * We make an assumption that on chains where we have delivery fee + * we also have fungibility restrictions + */ + + feeModel?.hasOriginPostSubmissionByAccount ?? false + } + var needMinBalanceDueToPostsubmissionFee: Bool { guard let payChainAsset, payChainAsset.isUtilityAsset, @@ -51,10 +63,14 @@ struct SwapMaxModel { needMinBalanceDueToReceiveInsufficiency } + var shouldKeepCustomMinBalance: Bool { + needMinBalanceDueToFungibility + } + private func calculateForNativeAsset(_ payChainAsset: ChainAsset, balance: AssetBalance) -> Decimal { var maxAmount = balance.transferable - if shouldKeepNativeMinBalance, !minBalanceCoveredByFrozen(in: balance) { + if shouldKeepNativeMinBalance, !minBalanceCoveredByFrozen(in: balance, assetExistence: payAssetExistense) { let minBalance = payAssetExistense?.minBalance ?? 0 maxAmount = maxAmount.subtractOrZero(minBalance) } @@ -68,8 +84,17 @@ struct SwapMaxModel { } private func calculateForCustomAsset(_ payChainAsset: ChainAsset, balance: AssetBalance) -> Decimal { + var maxAmount = balance.transferable + + if + shouldKeepCustomMinBalance, + !minBalanceCoveredByFrozen(in: balance, assetExistence: payAssetExistense) { + let minBalance = payAssetExistense?.minBalance ?? 0 + maxAmount = maxAmount.subtractOrZero(minBalance) + } + guard let feeModel = feeModel else { - return balance.transferable.decimal(precision: payChainAsset.asset.precision) + return maxAmount.decimal(precision: payChainAsset.asset.precision) } let fee: Balance = if payChainAsset.chainAssetId == feeChainAsset?.chainAssetId { @@ -78,9 +103,7 @@ struct SwapMaxModel { feeModel.postSubmissionFeeInAssetIn(payChainAsset) } - let maxAmount = balance.transferable.subtractOrZero(fee) - - return maxAmount.decimal(precision: payChainAsset.asset.precision) + return maxAmount.subtractOrZero(fee).decimal(precision: payChainAsset.asset.precision) } func calculate() -> Decimal { diff --git a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift index b7eda845a..f3b20a824 100644 --- a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift +++ b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift @@ -161,11 +161,22 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { ).value(for: locale) } - self?.presentable.presentMinBalanceViolatedDueDeliveryFee( + self?.presentable.presentMinBalanceViolatedAfterOperation( from: view, minBalance: minBalance ?? "", locale: locale ) + case let .fungibilityPreservation(model): + let minBalance = viewModelFactory.amountFromValue( + targetAssetInfo: params.payChainAsset.assetDisplayInfo, + value: model.minBalance + ).value(for: locale) + + self?.presentable.presentMinBalanceViolatedAfterOperation( + from: view, + minBalance: minBalance, + locale: locale + ) } }, preservesCondition: { insufficientReason == nil diff --git a/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift b/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift index a7afa3b34..f929355f3 100644 --- a/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift +++ b/novawallet/Modules/Swaps/Validation/SwapErrorPresentable.swift @@ -39,7 +39,7 @@ protocol SwapErrorPresentable: BaseErrorPresentable { locale: Locale ) - func presentMinBalanceViolatedDueDeliveryFee( + func presentMinBalanceViolatedAfterOperation( from view: ControllerBackedProtocol, minBalance: String, locale: Locale @@ -128,7 +128,7 @@ extension SwapErrorPresentable where Self: AlertPresentable & ErrorPresentable { present(message: message, title: title, closeAction: closeAction, from: view) } - func presentMinBalanceViolatedDueDeliveryFee( + func presentMinBalanceViolatedAfterOperation( from view: ControllerBackedProtocol, minBalance: String, locale: Locale diff --git a/novawallet/Modules/Swaps/Validation/SwapModel.swift b/novawallet/Modules/Swaps/Validation/SwapModel.swift index 2cc9424ed..bd74dfd3f 100644 --- a/novawallet/Modules/Swaps/Validation/SwapModel.swift +++ b/novawallet/Modules/Swaps/Validation/SwapModel.swift @@ -25,11 +25,16 @@ struct SwapModel { let minBalance: Decimal } + struct InsufficientDueFungibilityPreservation { + let minBalance: Decimal + } + enum InsufficientBalanceReason { case amountToHigh(InsufficientDueBalance) case feeInNativeAsset(InsufficientDueNativeFee) case feeInPayAsset(InsufficientDuePayAssetFee) case deliveryFee(InsufficientDueDeliveryFee) + case fungibilityPreservation(InsufficientDueFungibilityPreservation) case violatingConsumers(InsufficientDueConsumers) } @@ -207,6 +212,23 @@ struct SwapModel { return .deliveryFee(model) } + func checkEnoughBalanceForFungibilityRestriction() -> InsufficientBalanceReason? { + guard + let feeModel, + feeModel.hasOriginPostSubmissionByAccount, + payTokenProviderWillBeKilled else { + return nil + } + + let minBalance = payAssetExistense?.minBalance.decimal( + assetInfo: payChainAsset.assetDisplayInfo + ) ?? 0 + + let model = InsufficientDueFungibilityPreservation(minBalance: minBalance) + + return .fungibilityPreservation(model) + } + func checkBalanceSufficiency() -> InsufficientBalanceReason? { if let insufficient = checkEnoughBalanceToSpend() { return insufficient @@ -224,6 +246,10 @@ struct SwapModel { return insufficient } + if let insufficient = checkEnoughBalanceForFungibilityRestriction() { + return insufficient + } + if let insufficient = checkNotViolatingConsumers() { return insufficient } @@ -253,6 +279,12 @@ struct SwapModel { return totalInNativeAsset.subtractOrZero(feeInNativeAsset) < minBalance } + var payTokenProviderWillBeKilled: Bool { + let minBalance = payAssetExistense?.minBalance ?? 0 + + return payAssetTotalBalanceAfterSwap < minBalance + } + func checkReceiveBalanceAboveMin() -> CannotReceiveReason? { let amountAfterSwap = (receiveAssetBalance?.balanceCountingEd ?? 0) + (quote?.route.amountOut ?? 0) let minBalance = receiveAssetExistense?.minBalance ?? 0 From b10af6b6b576fbda1da493bf930500a13e133107 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sat, 28 Dec 2024 12:33:05 +0100 Subject: [PATCH 5/8] moved fungibility logic into separate provide --- novawallet.xcodeproj/project.pbxproj | 10 +++-- .../Common/Ledger/SupportedLedgerApps.swift | 4 +- novawallet/Common/Model/KnownChainIds.swift | 4 +- .../AssetHubExchangeMetaOperation.swift | 1 + .../AssetsExchangePathCostEstimator.swift | 2 +- .../AssetExchangeMetaOperationProtocol.swift | 10 +++++ .../CrosschainAssetsExchangeProvider.swift | 4 ++ .../CrosschainExchangeEdge.swift | 7 +++- .../CrosschainExchangeHost.swift | 4 ++ .../CrosschainExchangeMetaOperation.swift | 21 +++++++++- ...CrosschainExchangeOperationPrototype.swift | 2 +- .../Facade/AssetExchangeFacade.swift | 1 + .../HydraExchangeMetaOperation.swift | 1 + .../AssetConverters/ParachainResolver.swift | 4 +- ...AssetFungibilityPreservationProvider.swift | 38 +++++++++++++++++++ .../AssetStorageInfoOperationFactory.swift | 0 .../AssetTransferAggregationFactory.swift | 0 .../Modules/Nft/Services/ChainModel+Nft.swift | 4 +- .../KodaDot/KodaDotNftOperationFactory.swift | 4 +- .../Swaps/Base/Model/SwapMaxModel.swift | 15 +++----- .../Swaps/Base/SwapBasePresenter.swift | 1 + .../Validation/SwapDataValidatorFactory.swift | 2 +- .../Modules/Swaps/Validation/SwapModel.swift | 16 ++++---- 23 files changed, 120 insertions(+), 35 deletions(-) create mode 100644 novawallet/Common/Substrate/AssetOperations/AssetFungibilityPreservationProvider.swift rename novawallet/{Modules/Transfer/Operation => Common/Substrate/AssetOperations}/AssetStorageInfoOperationFactory.swift (100%) rename novawallet/{Modules/Transfer/Operation => Common/Substrate/AssetOperations}/AssetTransferAggregationFactory.swift (100%) diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 6a229538b..bf51fe262 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -166,6 +166,7 @@ 0C259EA42B46721B00CB86E4 /* ProxyMessageSheetViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C259EA32B46721B00CB86E4 /* ProxyMessageSheetViewFactory.swift */; }; 0C259EA82B46C55C00CB86E4 /* ExtrinsicSigningErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C259EA72B46C55C00CB86E4 /* ExtrinsicSigningErrorHandling.swift */; }; 0C28DF0D2D1AC69F0016DB8E /* SNAddressType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DAC197268D3DD9002D0DF4 /* SNAddressType.swift */; }; + 0C28DF1C2D2008670016DB8E /* AssetFungibilityPreservationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C28DF1B2D2008670016DB8E /* AssetFungibilityPreservationProvider.swift */; }; 0C29B5382A4C68A500E35C6D /* AnimationUpdatibleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C29B5372A4C68A500E35C6D /* AnimationUpdatibleView.swift */; }; 0C2A3C932CDC813B00A0E2B3 /* AssetsExchangeOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2A3C922CDC813B00A0E2B3 /* AssetsExchangeOperationFactory.swift */; }; 0C2A3C952CDC8ADB00A0E2B3 /* SwapAssetSelectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2A3C942CDC8ADB00A0E2B3 /* SwapAssetSelectionModel.swift */; }; @@ -5458,6 +5459,7 @@ 0C252BBE2B3D3CEB0047308F /* TypeRegistry+Node.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TypeRegistry+Node.swift"; sourceTree = ""; }; 0C259EA32B46721B00CB86E4 /* ProxyMessageSheetViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyMessageSheetViewFactory.swift; sourceTree = ""; }; 0C259EA72B46C55C00CB86E4 /* ExtrinsicSigningErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtrinsicSigningErrorHandling.swift; sourceTree = ""; }; + 0C28DF1B2D2008670016DB8E /* AssetFungibilityPreservationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetFungibilityPreservationProvider.swift; sourceTree = ""; }; 0C29B5372A4C68A500E35C6D /* AnimationUpdatibleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationUpdatibleView.swift; sourceTree = ""; }; 0C2A3C922CDC813B00A0E2B3 /* AssetsExchangeOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsExchangeOperationFactory.swift; sourceTree = ""; }; 0C2A3C942CDC8ADB00A0E2B3 /* SwapAssetSelectionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapAssetSelectionModel.swift; sourceTree = ""; }; @@ -16698,13 +16700,14 @@ path = PerformOperation; sourceTree = ""; }; - 84468A05286652EF00BCBE00 /* Operation */ = { + 84468A05286652EF00BCBE00 /* AssetOperations */ = { isa = PBXGroup; children = ( 84468A062866530100BCBE00 /* AssetStorageInfoOperationFactory.swift */, 2D32BE1F2C6CF4A20047F520 /* AssetTransferAggregationFactory.swift */, + 0C28DF1B2D2008670016DB8E /* AssetFungibilityPreservationProvider.swift */, ); - path = Operation; + path = AssetOperations; sourceTree = ""; }; 844ADE7C28CB34F600EE29F7 /* AutomationTime */ = { @@ -19109,6 +19112,7 @@ 8490154E24ACD521008F705E /* Substrate */ = { isa = PBXGroup; children = ( + 84468A05286652EF00BCBE00 /* AssetOperations */, 0CEB6B3F2CA50DB400609DC2 /* AssetConverters */, 0CCE3AAD2BF5BBBF00D55F03 /* Operations */, 84A3B89C2836CF8F00DE2669 /* Coders */, @@ -21382,7 +21386,6 @@ isa = PBXGroup; children = ( 84ED6BDC28688C9000B3C558 /* View */, - 84468A05286652EF00BCBE00 /* Operation */, 84E25BEA27E87D3D00290BF1 /* Validation */, 8466780D27EB28FF007935D3 /* BaseTransfer */, DA7D18D3AF772CC2385C228C /* TransferSetup */, @@ -27507,6 +27510,7 @@ 84B8AA7929F905C800347A37 /* WalletConnectStateInitiating.swift in Sources */, 8468B87224F63D3A00B76BC6 /* AddAccount+AccountCreateWireframe.swift in Sources */, 9DFB37659A6B911A4D54623E /* AccountConfirmInteractor.swift in Sources */, + 0C28DF1C2D2008670016DB8E /* AssetFungibilityPreservationProvider.swift in Sources */, 84BAFCD626AF64CB00871E86 /* SelectValidatorsViewLayout.swift in Sources */, 84C5ADD52812745F006D7388 /* WalletAccountViewModel.swift in Sources */, 8863C7AC29D499D30068AD54 /* Web3NameService.swift in Sources */, diff --git a/novawallet/Common/Ledger/SupportedLedgerApps.swift b/novawallet/Common/Ledger/SupportedLedgerApps.swift index 52c19c694..3b7b76c1d 100644 --- a/novawallet/Common/Ledger/SupportedLedgerApps.swift +++ b/novawallet/Common/Ledger/SupportedLedgerApps.swift @@ -17,8 +17,8 @@ extension SupportedLedgerApp { [ SupportedLedgerApp(chainId: KnowChainId.polkadot, coin: 354, cla: 0x90, type: .substrate), SupportedLedgerApp(chainId: KnowChainId.kusama, coin: 434, cla: 0x99, type: .substrate), - SupportedLedgerApp(chainId: KnowChainId.statemint, coin: 354, cla: 0x96, type: .substrate), - SupportedLedgerApp(chainId: KnowChainId.statemine, coin: 434, cla: 0x97, type: .substrate), + SupportedLedgerApp(chainId: KnowChainId.polkadotAssetHub, coin: 354, cla: 0x96, type: .substrate), + SupportedLedgerApp(chainId: KnowChainId.kusamaAssetHub, coin: 434, cla: 0x97, type: .substrate), SupportedLedgerApp(chainId: KnowChainId.karura, coin: 686, cla: 0x9A, type: .substrate), SupportedLedgerApp(chainId: KnowChainId.acala, coin: 787, cla: 0x9B, type: .substrate), SupportedLedgerApp(chainId: KnowChainId.nodle, coin: 1003, cla: 0x98, type: .substrate), diff --git a/novawallet/Common/Model/KnownChainIds.swift b/novawallet/Common/Model/KnownChainIds.swift index 70bfd7ff7..20c80d737 100644 --- a/novawallet/Common/Model/KnownChainIds.swift +++ b/novawallet/Common/Model/KnownChainIds.swift @@ -2,10 +2,10 @@ import Foundation enum KnowChainId { static let kusama = "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe" - static let statemine = "48239ef607d7928874027a43a67689209727dfb3d3dc5e5b03a39bdc2eda771a" + static let kusamaAssetHub = "48239ef607d7928874027a43a67689209727dfb3d3dc5e5b03a39bdc2eda771a" static let polkadot = "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3" + static let polkadotAssetHub = "68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f" static let acala = "fc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c" - static let statemint = "68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f" static let edgeware = "742a2ca70c2fda6cee4f8df98d64c4c670a052d9568058982dad9d5a7a135c5b" static let karura = "baf5aabe40646d11f0ee8abbdc64f4a4b7674925cba08e4a05ff9ebed6e2126b" static let nodle = "97da7ede98d7bad4e36b4d734b6055425a3be036da2a332ea5a7037656427a21" diff --git a/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeMetaOperation.swift b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeMetaOperation.swift index 080045f30..21cce1d78 100644 --- a/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeMetaOperation.swift +++ b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeMetaOperation.swift @@ -4,4 +4,5 @@ final class AssetHubExchangeMetaOperation: AssetExchangeBaseMetaOperation {} extension AssetHubExchangeMetaOperation: AssetExchangeMetaOperationProtocol { var label: AssetExchangeMetaOperationLabel { .swap } + var requiresOriginAccountKeepAlive: Bool { false } } diff --git a/novawallet/Common/Services/AssetExchange/AssetsExchangePathCostEstimator.swift b/novawallet/Common/Services/AssetExchange/AssetsExchangePathCostEstimator.swift index 322071a51..e03d8d077 100644 --- a/novawallet/Common/Services/AssetExchange/AssetsExchangePathCostEstimator.swift +++ b/novawallet/Common/Services/AssetExchange/AssetsExchangePathCostEstimator.swift @@ -32,7 +32,7 @@ extension AssetsExchangePathCostEstimator: AssetsExchangePathCostEstimating { ) -> CompoundOperationWrapper { let operation = ClosureOperation { guard let usdtTiedAsset = self.chainRegistry.getChain( - for: KnowChainId.statemint + for: KnowChainId.polkadotAssetHub )?.chainAssetForSymbol("USDT") else { return .zero } diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeMetaOperationProtocol.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeMetaOperationProtocol.swift index cf39e758b..4acde416d 100644 --- a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeMetaOperationProtocol.swift +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeMetaOperationProtocol.swift @@ -3,6 +3,15 @@ import Foundation enum AssetExchangeMetaOperationLabel: Equatable { case swap case transfer + + var isTransfer: Bool { + switch self { + case .transfer: + true + case .swap: + false + } + } } protocol AssetExchangeMetaOperationProtocol { @@ -11,6 +20,7 @@ protocol AssetExchangeMetaOperationProtocol { var amountIn: Balance { get } var amountOut: Balance { get } var label: AssetExchangeMetaOperationLabel { get } + var requiresOriginAccountKeepAlive: Bool { get } } class AssetExchangeBaseMetaOperation { diff --git a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainAssetsExchangeProvider.swift b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainAssetsExchangeProvider.swift index 6b3811d1b..225a71bea 100644 --- a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainAssetsExchangeProvider.swift +++ b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainAssetsExchangeProvider.swift @@ -10,12 +10,14 @@ final class CrosschainAssetsExchangeProvider: AssetsExchangeBaseProvider { let userStorageFacade: StorageFacadeProtocol let substrateStorageFacade: StorageFacadeProtocol let signingWrapperFactory: SigningWrapperFactoryProtocol + let fungibilityPreservationProvider: AssetFungibilityPreservationProviding init( wallet: MetaAccountModel, syncService: XcmTransfersSyncServiceProtocol, chainRegistry: ChainRegistryProtocol, pathCostEstimator: AssetsExchangePathCostEstimating, + fungibilityPreservationProvider: AssetFungibilityPreservationProviding, signingWrapperFactory: SigningWrapperFactoryProtocol, userStorageFacade: StorageFacadeProtocol, substrateStorageFacade: StorageFacadeProtocol, @@ -27,6 +29,7 @@ final class CrosschainAssetsExchangeProvider: AssetsExchangeBaseProvider { self.signingWrapperFactory = signingWrapperFactory self.userStorageFacade = userStorageFacade self.substrateStorageFacade = substrateStorageFacade + self.fungibilityPreservationProvider = fungibilityPreservationProvider super.init( chainRegistry: chainRegistry, @@ -90,6 +93,7 @@ final class CrosschainAssetsExchangeProvider: AssetsExchangeBaseProvider { ), xcmTransfers: xcmTransfers, executionTimeEstimator: AssetExchangeTimeEstimator(chainRegistry: chainRegistry), + fungibilityPreservationProvider: fungibilityPreservationProvider, operationQueue: operationQueue, logger: logger ) diff --git a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeEdge.swift b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeEdge.swift index b820635a0..cb0091a17 100644 --- a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeEdge.swift +++ b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeEdge.swift @@ -92,11 +92,16 @@ extension CrosschainExchangeEdge: AssetExchangableGraphEdge { throw ChainModelFetchError.noAsset(assetId: destination.assetId) } + let keepAlive = host.fungibilityPreservationProvider.requiresPreservationForCrosschain( + assetIn: assetIn + ) + return CrosschainExchangeMetaOperation( assetIn: assetIn, assetOut: assetOut, amountIn: amountIn, - amountOut: amountOut + amountOut: amountOut, + requiresOriginAccountKeepAlive: keepAlive ) } diff --git a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeHost.swift b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeHost.swift index c331341ba..730b79999 100644 --- a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeHost.swift +++ b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeHost.swift @@ -10,6 +10,7 @@ protocol CrosschainExchangeHostProtocol { var xcmTransfers: XcmTransfers { get } var operationQueue: OperationQueue { get } var executionTimeEstimator: AssetExchangeTimeEstimating { get } + var fungibilityPreservationProvider: AssetFungibilityPreservationProviding { get } var logger: LoggerProtocol { get } } @@ -22,6 +23,7 @@ final class CrosschainExchangeHost: CrosschainExchangeHostProtocol { let resolutionFactory: XcmTransferResolutionFactoryProtocol let xcmTransfers: XcmTransfers let executionTimeEstimator: AssetExchangeTimeEstimating + let fungibilityPreservationProvider: AssetFungibilityPreservationProviding let operationQueue: OperationQueue let logger: LoggerProtocol @@ -34,6 +36,7 @@ final class CrosschainExchangeHost: CrosschainExchangeHostProtocol { resolutionFactory: XcmTransferResolutionFactoryProtocol, xcmTransfers: XcmTransfers, executionTimeEstimator: AssetExchangeTimeEstimating, + fungibilityPreservationProvider: AssetFungibilityPreservationProviding, operationQueue: OperationQueue, logger: LoggerProtocol ) { @@ -45,6 +48,7 @@ final class CrosschainExchangeHost: CrosschainExchangeHostProtocol { self.resolutionFactory = resolutionFactory self.xcmTransfers = xcmTransfers self.executionTimeEstimator = executionTimeEstimator + self.fungibilityPreservationProvider = fungibilityPreservationProvider self.operationQueue = operationQueue self.logger = logger } diff --git a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeMetaOperation.swift b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeMetaOperation.swift index 9b0cdf7ec..aaa0c4d1a 100644 --- a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeMetaOperation.swift +++ b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeMetaOperation.swift @@ -1,6 +1,25 @@ import Foundation -final class CrosschainExchangeMetaOperation: AssetExchangeBaseMetaOperation {} +final class CrosschainExchangeMetaOperation: AssetExchangeBaseMetaOperation { + let requiresOriginAccountKeepAlive: Bool + + init( + assetIn: ChainAsset, + assetOut: ChainAsset, + amountIn: Balance, + amountOut: Balance, + requiresOriginAccountKeepAlive: Bool + ) { + self.requiresOriginAccountKeepAlive = requiresOriginAccountKeepAlive + + super.init( + assetIn: assetIn, + assetOut: assetOut, + amountIn: amountIn, + amountOut: amountOut + ) + } +} extension CrosschainExchangeMetaOperation: AssetExchangeMetaOperationProtocol { var label: AssetExchangeMetaOperationLabel { .transfer } diff --git a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeOperationPrototype.swift b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeOperationPrototype.swift index 1ffdc3e92..c9c3b8d6a 100644 --- a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeOperationPrototype.swift +++ b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeOperationPrototype.swift @@ -26,7 +26,7 @@ final class CrosschainExchangeOperationPrototype: AssetExchangeBaseOperationProt private extension CrosschainExchangeOperationPrototype { private func isChainWithExpensiveCrossChain(chainId: ChainModel.Id) -> Bool { - chainId == KnowChainId.polkadot || chainId == KnowChainId.statemint + chainId == KnowChainId.polkadot || chainId == KnowChainId.polkadotAssetHub } } diff --git a/novawallet/Common/Services/AssetExchange/Facade/AssetExchangeFacade.swift b/novawallet/Common/Services/AssetExchange/Facade/AssetExchangeFacade.swift index 5ced93030..5b55794f1 100644 --- a/novawallet/Common/Services/AssetExchange/Facade/AssetExchangeFacade.swift +++ b/novawallet/Common/Services/AssetExchange/Facade/AssetExchangeFacade.swift @@ -22,6 +22,7 @@ final class AssetExchangeFacade { ), chainRegistry: params.chainRegistry, pathCostEstimator: pathCostEstimator, + fungibilityPreservationProvider: AssetFungibilityPreservationProvider.createFromKnownChains(), signingWrapperFactory: params.signingWrapperFactory, userStorageFacade: params.userDataStorageFacade, substrateStorageFacade: params.substrateStorageFacade, diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeMetaOperation.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeMetaOperation.swift index 10ce0f3a8..ba3988c48 100644 --- a/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeMetaOperation.swift +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/HydraExchangeMetaOperation.swift @@ -4,4 +4,5 @@ class HydraExchangeMetaOperation: AssetExchangeBaseMetaOperation {} extension HydraExchangeMetaOperation: AssetExchangeMetaOperationProtocol { var label: AssetExchangeMetaOperationLabel { .swap } + var requiresOriginAccountKeepAlive: Bool { false } } diff --git a/novawallet/Common/Substrate/AssetConverters/ParachainResolver.swift b/novawallet/Common/Substrate/AssetConverters/ParachainResolver.swift index a530d247a..d6866ad23 100644 --- a/novawallet/Common/Substrate/AssetConverters/ParachainResolver.swift +++ b/novawallet/Common/Substrate/AssetConverters/ParachainResolver.swift @@ -21,9 +21,9 @@ final class ParachainResolver: ParachainResolving { switch relaychainId { case KnowChainId.polkadot: - return .createWithResult(KnowChainId.statemint) + return .createWithResult(KnowChainId.polkadotAssetHub) case KnowChainId.kusama: - return .createWithResult(KnowChainId.statemine) + return .createWithResult(KnowChainId.kusamaAssetHub) default: return .createWithResult(nil) } diff --git a/novawallet/Common/Substrate/AssetOperations/AssetFungibilityPreservationProvider.swift b/novawallet/Common/Substrate/AssetOperations/AssetFungibilityPreservationProvider.swift new file mode 100644 index 000000000..476917e5f --- /dev/null +++ b/novawallet/Common/Substrate/AssetOperations/AssetFungibilityPreservationProvider.swift @@ -0,0 +1,38 @@ +import Foundation + +protocol AssetFungibilityPreservationProviding { + func requiresPreservationForCrosschain(assetIn: ChainAsset) -> Bool +} + +final class AssetFungibilityPreservationProvider { + let allAssets: Set + let concreteAssets: Set + + init(allAssets: Set, concreteAssets: Set) { + self.allAssets = allAssets + self.concreteAssets = concreteAssets + } +} + +extension AssetFungibilityPreservationProvider: AssetFungibilityPreservationProviding { + func requiresPreservationForCrosschain(assetIn: ChainAsset) -> Bool { + allAssets.contains(assetIn.chain.chainId) || concreteAssets.contains(assetIn.chainAssetId) + } +} + +extension AssetFungibilityPreservationProvider { + static func createFromKnownChains() -> AssetFungibilityPreservationProvider { + AssetFungibilityPreservationProvider( + allAssets: [ + KnowChainId.polkadotAssetHub, + KnowChainId.kusamaAssetHub + ], + concreteAssets: [ + ChainAssetId( + chainId: KnowChainId.astar, + assetId: AssetModel.utilityAssetId + ) + ] + ) + } +} diff --git a/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift b/novawallet/Common/Substrate/AssetOperations/AssetStorageInfoOperationFactory.swift similarity index 100% rename from novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift rename to novawallet/Common/Substrate/AssetOperations/AssetStorageInfoOperationFactory.swift diff --git a/novawallet/Modules/Transfer/Operation/AssetTransferAggregationFactory.swift b/novawallet/Common/Substrate/AssetOperations/AssetTransferAggregationFactory.swift similarity index 100% rename from novawallet/Modules/Transfer/Operation/AssetTransferAggregationFactory.swift rename to novawallet/Common/Substrate/AssetOperations/AssetTransferAggregationFactory.swift diff --git a/novawallet/Modules/Nft/Services/ChainModel+Nft.swift b/novawallet/Modules/Nft/Services/ChainModel+Nft.swift index 4dfbb2151..230c81abb 100644 --- a/novawallet/Modules/Nft/Services/ChainModel+Nft.swift +++ b/novawallet/Modules/Nft/Services/ChainModel+Nft.swift @@ -5,13 +5,13 @@ extension ChainModel { switch chainId { case KnowChainId.kusama: return [NftSource(chainId: chainId, type: .rmrkV2)] - case KnowChainId.statemine: + case KnowChainId.kusamaAssetHub: return [ NftSource(chainId: chainId, type: .kodadot) ] case KnowChainId.polkadot: return [NftSource(chainId: chainId, type: .pdc20)] - case KnowChainId.statemint: + case KnowChainId.polkadotAssetHub: return [NftSource(chainId: chainId, type: .kodadot)] default: return [] diff --git a/novawallet/Modules/Nft/Services/KodaDot/KodaDotNftOperationFactory.swift b/novawallet/Modules/Nft/Services/KodaDot/KodaDotNftOperationFactory.swift index 3fd8330b2..d86717267 100644 --- a/novawallet/Modules/Nft/Services/KodaDot/KodaDotNftOperationFactory.swift +++ b/novawallet/Modules/Nft/Services/KodaDot/KodaDotNftOperationFactory.swift @@ -13,9 +13,9 @@ enum KodaDotAssetHubApi { static func apiForChain(_ chainId: ChainModel.Id) -> URL? { switch chainId { - case KnowChainId.statemine: + case KnowChainId.kusamaAssetHub: return KodaDotAssetHubApi.kusamaAssetHub - case KnowChainId.statemint: + case KnowChainId.polkadotAssetHub: return KodaDotAssetHubApi.polkadotAssetHub default: return nil diff --git a/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift b/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift index 3359a7e2b..38519cd1d 100644 --- a/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift +++ b/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift @@ -6,6 +6,7 @@ struct SwapMaxModel { let receiveChainAsset: ChainAsset? let balance: AssetBalance? let feeModel: AssetExchangeFee? + let quote: AssetExchangeQuote? let payAssetExistense: AssetBalanceExistence? let receiveAssetExistense: AssetBalanceExistence? let accountInfo: AccountInfo? @@ -38,13 +39,8 @@ struct SwapMaxModel { accountInfo?.hasConsumers ?? false } - var needMinBalanceDueToFungibility: Bool { - /* - * We make an assumption that on chains where we have delivery fee - * we also have fungibility restrictions - */ - - feeModel?.hasOriginPostSubmissionByAccount ?? false + var needMinBalanceDueToOriginKeepAlive: Bool { + quote?.metaOperations.first?.requiresOriginAccountKeepAlive ?? false } var needMinBalanceDueToPostsubmissionFee: Bool { @@ -58,13 +54,14 @@ struct SwapMaxModel { } var shouldKeepNativeMinBalance: Bool { - needMinBalanceDueConsumers || + needMinBalanceDueToOriginKeepAlive || + needMinBalanceDueConsumers || needMinBalanceDueToPostsubmissionFee || needMinBalanceDueToReceiveInsufficiency } var shouldKeepCustomMinBalance: Bool { - needMinBalanceDueToFungibility + needMinBalanceDueToOriginKeepAlive } private func calculateForNativeAsset(_ payChainAsset: ChainAsset, balance: AssetBalance) -> Decimal { diff --git a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift index cca2f5785..9153503cf 100644 --- a/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift +++ b/novawallet/Modules/Swaps/Base/SwapBasePresenter.swift @@ -123,6 +123,7 @@ class SwapBasePresenter { receiveChainAsset: getReceiveChainAsset(), balance: payAssetBalance, feeModel: fee, + quote: quote, payAssetExistense: payAssetBalanceExistense, receiveAssetExistense: receiveAssetBalanceExistense, accountInfo: accountInfo diff --git a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift index f3b20a824..46bf6b03f 100644 --- a/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift +++ b/novawallet/Modules/Swaps/Validation/SwapDataValidatorFactory.swift @@ -166,7 +166,7 @@ final class SwapDataValidatorFactory: SwapDataValidatorFactoryProtocol { minBalance: minBalance ?? "", locale: locale ) - case let .fungibilityPreservation(model): + case let .originKeepAlive(model): let minBalance = viewModelFactory.amountFromValue( targetAssetInfo: params.payChainAsset.assetDisplayInfo, value: model.minBalance diff --git a/novawallet/Modules/Swaps/Validation/SwapModel.swift b/novawallet/Modules/Swaps/Validation/SwapModel.swift index bd74dfd3f..b6015e33a 100644 --- a/novawallet/Modules/Swaps/Validation/SwapModel.swift +++ b/novawallet/Modules/Swaps/Validation/SwapModel.swift @@ -25,7 +25,7 @@ struct SwapModel { let minBalance: Decimal } - struct InsufficientDueFungibilityPreservation { + struct InsufficientDueOriginKeepAlive { let minBalance: Decimal } @@ -34,7 +34,7 @@ struct SwapModel { case feeInNativeAsset(InsufficientDueNativeFee) case feeInPayAsset(InsufficientDuePayAssetFee) case deliveryFee(InsufficientDueDeliveryFee) - case fungibilityPreservation(InsufficientDueFungibilityPreservation) + case originKeepAlive(InsufficientDueOriginKeepAlive) case violatingConsumers(InsufficientDueConsumers) } @@ -212,10 +212,10 @@ struct SwapModel { return .deliveryFee(model) } - func checkEnoughBalanceForFungibilityRestriction() -> InsufficientBalanceReason? { + func checkEnoughBalanceForOriginKeepAlive() -> InsufficientBalanceReason? { guard - let feeModel, - feeModel.hasOriginPostSubmissionByAccount, + let firstOperation = quote?.metaOperations.first, + firstOperation.requiresOriginAccountKeepAlive, payTokenProviderWillBeKilled else { return nil } @@ -224,9 +224,9 @@ struct SwapModel { assetInfo: payChainAsset.assetDisplayInfo ) ?? 0 - let model = InsufficientDueFungibilityPreservation(minBalance: minBalance) + let model = InsufficientDueOriginKeepAlive(minBalance: minBalance) - return .fungibilityPreservation(model) + return .originKeepAlive(model) } func checkBalanceSufficiency() -> InsufficientBalanceReason? { @@ -246,7 +246,7 @@ struct SwapModel { return insufficient } - if let insufficient = checkEnoughBalanceForFungibilityRestriction() { + if let insufficient = checkEnoughBalanceForOriginKeepAlive() { return insufficient } From 3015bd97d27a4fac5da1a6cdf1194650c191bfc9 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sat, 28 Dec 2024 14:12:21 +0100 Subject: [PATCH 6/8] integrate fungibility validation into crosschain transfer --- .../CrossChainTransferInteractor.swift | 12 ++++ .../CrossChainTransferPresenter.swift | 43 +++++++++++++++ .../TransferCrossChainConfirmInteractor.swift | 1 + .../TransferCrossChainConfirmPresenter.swift | 6 +- .../CrossChainTransferSetupPresenter.swift | 22 +++----- .../CrossChainTransferSetupProtocols.swift | 1 + ...sferSetupPresenterFactory+CrossChain.swift | 1 + .../TransferSetupProtocols.swift | 1 - .../TransferDataValidatorFactory.swift | 55 +++++++++++++++++++ .../Validation/TransferErrorPresentable.swift | 22 ++++++++ 10 files changed, 148 insertions(+), 16 deletions(-) diff --git a/novawallet/Modules/Transfer/BaseTransfer/CrossChain/CrossChainTransferInteractor.swift b/novawallet/Modules/Transfer/BaseTransfer/CrossChain/CrossChainTransferInteractor.swift index c0144c2a9..368a019b5 100644 --- a/novawallet/Modules/Transfer/BaseTransfer/CrossChain/CrossChainTransferInteractor.swift +++ b/novawallet/Modules/Transfer/BaseTransfer/CrossChain/CrossChainTransferInteractor.swift @@ -25,6 +25,7 @@ class CrossChainTransferInteractor: RuntimeConstantFetching { let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol let substrateStorageFacade: StorageFacadeProtocol + let fungibilityPreservationProvider: AssetFungibilityPreservationProviding let operationQueue: OperationQueue private lazy var callFactory = SubstrateCallFactory() @@ -71,6 +72,7 @@ class CrossChainTransferInteractor: RuntimeConstantFetching { feeProxy: XcmExtrinsicFeeProxyProtocol, extrinsicService: XcmTransferServiceProtocol, resolutionFactory: XcmTransferResolutionFactoryProtocol, + fungibilityPreservationProvider: AssetFungibilityPreservationProviding, walletRemoteWrapper: WalletRemoteSubscriptionWrapperProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, @@ -86,6 +88,7 @@ class CrossChainTransferInteractor: RuntimeConstantFetching { self.feeProxy = feeProxy self.extrinsicService = extrinsicService self.resolutionFactory = resolutionFactory + self.fungibilityPreservationProvider = fungibilityPreservationProvider self.walletRemoteWrapper = walletRemoteWrapper self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory @@ -231,6 +234,7 @@ class CrossChainTransferInteractor: RuntimeConstantFetching { setupUtilityAssetPriceProviderIfNeeded() provideMinBalance() + provideOriginRequiresKeepAlive() presenter?.didCompleteSetup(result: .success(())) } @@ -356,6 +360,14 @@ class CrossChainTransferInteractor: RuntimeConstantFetching { } } + private func provideOriginRequiresKeepAlive() { + let keepAlive = fungibilityPreservationProvider.requiresPreservationForCrosschain( + assetIn: originChainAsset + ) + + presenter?.didReceiveRequiresOriginKeepAlive(keepAlive) + } + private func cancelSetupCall() { let cancellingCall = setupCall setupCall = nil diff --git a/novawallet/Modules/Transfer/BaseTransfer/CrossChain/CrossChainTransferPresenter.swift b/novawallet/Modules/Transfer/BaseTransfer/CrossChain/CrossChainTransferPresenter.swift index 24e378ca2..22ee9d01e 100644 --- a/novawallet/Modules/Transfer/BaseTransfer/CrossChain/CrossChainTransferPresenter.swift +++ b/novawallet/Modules/Transfer/BaseTransfer/CrossChain/CrossChainTransferPresenter.swift @@ -33,6 +33,7 @@ class CrossChainTransferPresenter { private(set) var networkFee: ExtrinsicFeeProtocol? private(set) var crossChainFee: XcmFeeModelProtocol? + private(set) var requiresOriginKeepAlive: Bool? let networkViewModelFactory: NetworkViewModelFactoryProtocol let sendingBalanceViewModelFactory: BalanceViewModelFactoryProtocol @@ -104,6 +105,31 @@ class CrossChainTransferPresenter { fatalError("Child classes must implement this method") } + func getSendingAmount() -> Decimal? { + fatalError("Child classes must implement this method") + } + + func getTotalSpendingWithoutNetworkFee() -> Decimal? { + guard let utilityAsset = originChainAsset.chain.utilityAsset() else { + return nil + } + + let utilityAssetInfo = ChainAsset(chain: originChainAsset.chain, asset: utilityAsset).assetDisplayInfo + + let sendingAmount = getSendingAmount() + + let originDeliveryFeeSpending = isOriginUtilityTransfer ? + crossChainFee?.senderPart.decimal(assetInfo: utilityAssetInfo) : + nil + + let crosschainFeeSpending = crossChainFee?.holdingPart.decimal( + assetInfo: originChainAsset.assetDisplayInfo + ) + + return (sendingAmount ?? 0) + (originDeliveryFeeSpending ?? 0) + + (crosschainFeeSpending ?? 0) + } + func updateCrossChainFee(_ newValue: XcmFeeModelProtocol?) { crossChainFee = newValue } @@ -195,6 +221,17 @@ class CrossChainTransferPresenter { locale: selectedLocale ), + dataValidatingFactory.notViolatingKeepAlive( + for: .init( + totalSpendingAmount: getTotalSpendingWithoutNetworkFee(), + networkFee: isOriginUtilityTransfer ? networkFee : nil, + balance: senderSendingAssetBalance?.balanceCountingEd, + minBalance: originSendingMinBalance, + requiresKeepAlive: requiresOriginKeepAlive + ), + locale: selectedLocale + ), + dataValidatingFactory.receiverWillHaveAssetAccount( sendingAmount: sendingAmount, totalAmount: recepientSendingAssetBalance?.balanceCountingEd, @@ -284,6 +321,12 @@ class CrossChainTransferPresenter { destUtilityMinBalance = value } + func didReceiveRequiresOriginKeepAlive(_ value: Bool) { + logger?.debug("Requires origin keep alive: \(value)") + + requiresOriginKeepAlive = value + } + func didCompleteSetup(result _: Result) {} func didReceiveError(_: Error) {} diff --git a/novawallet/Modules/Transfer/TransferConfirm/CrossChain/TransferCrossChainConfirmInteractor.swift b/novawallet/Modules/Transfer/TransferConfirm/CrossChain/TransferCrossChainConfirmInteractor.swift index 6fa77f521..69aee61c7 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/CrossChain/TransferCrossChainConfirmInteractor.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/CrossChain/TransferCrossChainConfirmInteractor.swift @@ -43,6 +43,7 @@ final class TransferCrossChainConfirmInteractor: CrossChainTransferInteractor { feeProxy: feeProxy, extrinsicService: extrinsicService, resolutionFactory: resolutionFactory, + fungibilityPreservationProvider: AssetFungibilityPreservationProvider.createFromKnownChains(), walletRemoteWrapper: walletRemoteWrapper, walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, diff --git a/novawallet/Modules/Transfer/TransferConfirm/CrossChain/TransferCrossChainConfirmPresenter.swift b/novawallet/Modules/Transfer/TransferConfirm/CrossChain/TransferCrossChainConfirmPresenter.swift index 7ec0a377d..4e07d5e96 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/CrossChain/TransferCrossChainConfirmPresenter.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/CrossChain/TransferCrossChainConfirmPresenter.swift @@ -158,6 +158,10 @@ final class TransferCrossChainConfirmPresenter: CrossChainTransferPresenter { // MARK: Subsclass + override func getSendingAmount() -> Decimal? { + amount + } + override func refreshOriginFee() { let assetInfo = originChainAsset.assetDisplayInfo @@ -290,7 +294,7 @@ extension TransferCrossChainConfirmPresenter: TransferConfirmPresenterProtocol { let utilityAssetInfo = ChainAsset(chain: originChainAsset.chain, asset: utilityAsset).assetDisplayInfo let validators: [DataValidating] = baseValidators( - for: amount, + for: getSendingAmount(), recepientAddress: recepientAccountAddress, utilityAssetInfo: utilityAssetInfo, selectedLocale: selectedLocale diff --git a/novawallet/Modules/Transfer/TransferSetup/CrossChain/CrossChainTransferSetupPresenter.swift b/novawallet/Modules/Transfer/TransferSetup/CrossChain/CrossChainTransferSetupPresenter.swift index a6e86a3da..5808787a0 100644 --- a/novawallet/Modules/Transfer/TransferSetup/CrossChain/CrossChainTransferSetupPresenter.swift +++ b/novawallet/Modules/Transfer/TransferSetup/CrossChain/CrossChainTransferSetupPresenter.swift @@ -200,7 +200,8 @@ final class CrossChainTransferSetupPresenter: CrossChainTransferPresenter, * before paying delivery fee. So make sure we will have at least ed and don't burn any tokens on account kill */ let hasOriginDeliveryFee = (crossChainFee?.senderPart ?? 0) > 0 - let minimumBalanceValue = hasOriginDeliveryFee && isOriginUtilityTransfer ? originSendingMinBalance ?? 0 : 0 + let keepAlive = (hasOriginDeliveryFee && isOriginUtilityTransfer) || (requiresOriginKeepAlive ?? false) + let minimumBalanceValue = keepAlive ? originSendingMinBalance ?? 0 : 0 let precision = originChainAsset.assetDisplayInfo.assetPrecision @@ -366,6 +367,10 @@ final class CrossChainTransferSetupPresenter: CrossChainTransferPresenter, _ = wireframe.present(error: error, from: view, locale: selectedLocale) } + + override func getSendingAmount() -> Decimal? { + inputResult?.absoluteValue(from: maxTransferrable()) + } } extension CrossChainTransferSetupPresenter: TransferSetupChildPresenterProtocol { @@ -420,18 +425,7 @@ extension CrossChainTransferSetupPresenter: TransferSetupChildPresenterProtocol let utilityAssetInfo = ChainAsset(chain: originChainAsset.chain, asset: utilityAsset).assetDisplayInfo - let sendingAmount = inputResult?.absoluteValue(from: maxTransferrable()) - - let originDeliveryFeeSpending = isOriginUtilityTransfer ? - crossChainFee?.senderPart.decimal(assetInfo: utilityAssetInfo) : - nil - - let crosschainFeeSpending = crossChainFee?.holdingPart.decimal( - assetInfo: originChainAsset.assetDisplayInfo - ) - - let totalSpending = (sendingAmount ?? 0) + (originDeliveryFeeSpending ?? 0) + - (crosschainFeeSpending ?? 0) + let sendingAmount = getSendingAmount() var validators: [DataValidating] = baseValidators( for: sendingAmount, @@ -447,7 +441,7 @@ extension CrossChainTransferSetupPresenter: TransferSetupChildPresenterProtocol ), dataValidatingFactory.willBeReaped( - amount: totalSpending, + amount: getTotalSpendingWithoutNetworkFee(), fee: isOriginUtilityTransfer ? networkFee : nil, totalAmount: senderSendingAssetBalance?.balanceCountingEd, minBalance: originSendingMinBalance, diff --git a/novawallet/Modules/Transfer/TransferSetup/CrossChain/CrossChainTransferSetupProtocols.swift b/novawallet/Modules/Transfer/TransferSetup/CrossChain/CrossChainTransferSetupProtocols.swift index c1840cb92..2dd7882cb 100644 --- a/novawallet/Modules/Transfer/TransferSetup/CrossChain/CrossChainTransferSetupProtocols.swift +++ b/novawallet/Modules/Transfer/TransferSetup/CrossChain/CrossChainTransferSetupProtocols.swift @@ -21,6 +21,7 @@ protocol CrossChainTransferSetupInteractorOutputProtocol: AnyObject { func didReceiveOriginSendingMinBalance(_ value: BigUInt) func didReceiveDestSendingExistence(_ value: AssetBalanceExistence) func didReceiveDestUtilityMinBalance(_ value: BigUInt) + func didReceiveRequiresOriginKeepAlive(_ value: Bool) func didCompleteSetup(result: Result) func didReceiveError(_ error: Error) } diff --git a/novawallet/Modules/Transfer/TransferSetup/CrossChain/TransferSetupPresenterFactory+CrossChain.swift b/novawallet/Modules/Transfer/TransferSetup/CrossChain/TransferSetupPresenterFactory+CrossChain.swift index 051d4a55d..2923a5273 100644 --- a/novawallet/Modules/Transfer/TransferSetup/CrossChain/TransferSetupPresenterFactory+CrossChain.swift +++ b/novawallet/Modules/Transfer/TransferSetup/CrossChain/TransferSetupPresenterFactory+CrossChain.swift @@ -142,6 +142,7 @@ extension TransferSetupPresenterFactory { feeProxy: XcmExtrinsicFeeProxy(), extrinsicService: extrinsicService, resolutionFactory: resolutionFactory, + fungibilityPreservationProvider: AssetFungibilityPreservationProvider.createFromKnownChains(), walletRemoteWrapper: walletRemoteSubscriptionWrapper, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, diff --git a/novawallet/Modules/Transfer/TransferSetup/TransferSetupProtocols.swift b/novawallet/Modules/Transfer/TransferSetup/TransferSetupProtocols.swift index 945b9d42d..90045900b 100644 --- a/novawallet/Modules/Transfer/TransferSetup/TransferSetupProtocols.swift +++ b/novawallet/Modules/Transfer/TransferSetup/TransferSetupProtocols.swift @@ -1,5 +1,4 @@ import BigInt - import SoraFoundation protocol TransferSetupChildViewProtocol: ControllerBackedProtocol, Localizable { diff --git a/novawallet/Modules/Transfer/Validation/TransferDataValidatorFactory.swift b/novawallet/Modules/Transfer/Validation/TransferDataValidatorFactory.swift index aafa75803..94c4aa1df 100644 --- a/novawallet/Modules/Transfer/Validation/TransferDataValidatorFactory.swift +++ b/novawallet/Modules/Transfer/Validation/TransferDataValidatorFactory.swift @@ -13,6 +13,14 @@ struct CrossChainValidationAtLeastEdForDeliveryFee { let minBalance: BigUInt? } +struct CrossChainValidationOriginKeepAlive { + let totalSpendingAmount: Decimal? + let networkFee: ExtrinsicFeeProtocol? + let balance: Balance? + let minBalance: Balance? + let requiresKeepAlive: Bool? +} + protocol TransferDataValidatorFactoryProtocol: BaseDataValidatingFactoryProtocol { func willBeReaped( amount: Decimal?, @@ -72,6 +80,11 @@ protocol TransferDataValidatorFactoryProtocol: BaseDataValidatingFactoryProtocol locale: Locale ) -> DataValidating + func notViolatingKeepAlive( + for params: CrossChainValidationOriginKeepAlive, + locale: Locale + ) -> DataValidating + func has(crosschainFee: XcmFeeModelProtocol?, locale: Locale, onError: (() -> Void)?) -> DataValidating } @@ -333,6 +346,48 @@ final class TransferDataValidatorFactory: TransferDataValidatorFactoryProtocol { }) } + func notViolatingKeepAlive( + for params: CrossChainValidationOriginKeepAlive, + locale: Locale + ) -> DataValidating { + let totalSpendingInPlank = params.totalSpendingAmount?.toSubstrateAmount( + precision: assetDisplayInfo.assetPrecision + ) ?? 0 + + return ErrorConditionViolation(onError: { [weak self] in + guard let view = self?.view, let assetInfo = self?.assetDisplayInfo else { + return + } + + let tokenFormatter = AssetBalanceFormatterFactory().createTokenFormatter(for: assetInfo) + + let minBalanceDecimal = params.minBalance?.decimal(assetInfo: assetInfo) ?? 0 + + let minBalanceString = tokenFormatter.value(for: locale).stringFromDecimal(minBalanceDecimal) ?? "" + + self?.presentable.presentKeepAliveViolatedForCrosschain( + from: view, + minBalance: minBalanceString, + locale: locale + ) + + }, preservesCondition: { + guard + let balance = params.balance, + let minBalance = params.minBalance, + let requiresKeepAlive = params.requiresKeepAlive else { + return true + } + + if requiresKeepAlive { + let feeAmount = params.networkFee?.amountForCurrentAccount ?? 0 + return totalSpendingInPlank + feeAmount + minBalance <= balance + } else { + return true + } + }) + } + func canPayCrossChainFee( for amount: Decimal?, fee: CrossChainValidationFee?, diff --git a/novawallet/Modules/Transfer/Validation/TransferErrorPresentable.swift b/novawallet/Modules/Transfer/Validation/TransferErrorPresentable.swift index 9e6c308c9..a6a0fd78e 100644 --- a/novawallet/Modules/Transfer/Validation/TransferErrorPresentable.swift +++ b/novawallet/Modules/Transfer/Validation/TransferErrorPresentable.swift @@ -25,6 +25,12 @@ protocol TransferErrorPresentable: BaseErrorPresentable { availableBalance: String, locale: Locale? ) + + func presentKeepAliveViolatedForCrosschain( + from view: ControllerBackedProtocol, + minBalance: String, + locale: Locale? + ) } extension TransferErrorPresentable where Self: AlertPresentable & ErrorPresentable { @@ -141,4 +147,20 @@ extension TransferErrorPresentable where Self: AlertPresentable & ErrorPresentab present(message: message, title: title, closeAction: closeAction, from: view) } + + func presentKeepAliveViolatedForCrosschain( + from view: ControllerBackedProtocol, + minBalance: String, + locale: Locale? + ) { + let title = R.string.localizable.commonInsufficientBalance(preferredLanguages: locale?.rLanguages) + let message = R.string.localizable.swapDeliveryFeeErrorMessage( + minBalance, + preferredLanguages: locale?.rLanguages + ) + + let closeAction = R.string.localizable.commonClose(preferredLanguages: locale?.rLanguages) + + present(message: message, title: title, closeAction: closeAction, from: view) + } } From 607db03e391d0ac6257a2eea7de836313b2e11a1 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sun, 29 Dec 2024 20:34:40 +0100 Subject: [PATCH 7/8] fix tets --- .../HydraExchange/AssetsHydraExchangeProvider.swift | 5 ++++- .../AssetsExchange/AssetsExchangeTests.swift | 13 ++++++++++--- .../WalletRemoteQueryFactoryTests.swift | 4 ++-- .../XcmTransfersFeeTests.swift | 4 ++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/AssetsHydraExchangeProvider.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/AssetsHydraExchangeProvider.swift index 757a96bc1..f18746a67 100644 --- a/novawallet/Common/Services/AssetExchange/HydraExchange/AssetsHydraExchangeProvider.swift +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/AssetsHydraExchangeProvider.swift @@ -227,7 +227,10 @@ final class AssetsHydraExchangeProvider: AssetsExchangeBaseProvider { let omnipoolExchange = createOmnipoolExchange(from: swapHost, registeringStateIn: exchangeStateRegistrar) - let stableswapExchange = createStableswapExchange(from: swapHost, registeringStateIn: exchangeStateRegistrar) + let stableswapExchange = createStableswapExchange( + from: swapHost, + registeringStateIn: exchangeStateRegistrar + ) let xykExchange = createXYKExchange(from: swapHost, registeringStateIn: exchangeStateRegistrar) diff --git a/novawalletIntegrationTests/AssetsExchange/AssetsExchangeTests.swift b/novawalletIntegrationTests/AssetsExchange/AssetsExchangeTests.swift index d213e4ff7..7860fc3cc 100644 --- a/novawalletIntegrationTests/AssetsExchange/AssetsExchangeTests.swift +++ b/novawalletIntegrationTests/AssetsExchange/AssetsExchangeTests.swift @@ -8,7 +8,9 @@ final class AssetsExchangeTests: XCTestCase { guard let dotPolkadot = params.chainRegistry.getChain(for: KnowChainId.polkadot)?.utilityChainAsset(), - let usdtAssetHubId = params.chainRegistry.getChain(for: KnowChainId.statemint)?.chainAssetForSymbol("USDT")?.chainAssetId else { + let usdtAssetHubId = params.chainRegistry.getChain( + for: KnowChainId.polkadotAssetHub + )?.chainAssetForSymbol("USDT")?.chainAssetId else { XCTFail("No chain or asset") return } @@ -52,7 +54,9 @@ final class AssetsExchangeTests: XCTestCase { guard let polkadotUtilityAsset = params.chainRegistry.getChain(for: KnowChainId.polkadot)?.utilityChainAssetId(), let hydraUtilityAsset = params.chainRegistry.getChain(for: KnowChainId.hydra)?.utilityChainAssetId(), - let assetHubUtilityAsset = params.chainRegistry.getChain(for: KnowChainId.statemint)?.utilityChainAssetId() else { + let assetHubUtilityAsset = params.chainRegistry.getChain( + for: KnowChainId.polkadotAssetHub + )?.utilityChainAssetId() else { XCTFail("No chain or asset") return } @@ -125,7 +129,9 @@ final class AssetsExchangeTests: XCTestCase { guard let dotPolkadot = params.chainRegistry.getChain(for: KnowChainId.polkadot)?.utilityChainAsset(), - let usdtAssetHubId = params.chainRegistry.getChain(for: KnowChainId.statemint)?.chainAssetForSymbol("USDT")?.chainAssetId else { + let usdtAssetHubId = params.chainRegistry.getChain( + for: KnowChainId.polkadotAssetHub + )?.chainAssetForSymbol("USDT")?.chainAssetId else { XCTFail("No chain or asset") return } @@ -233,6 +239,7 @@ final class AssetsExchangeTests: XCTestCase { ), chainRegistry: params.chainRegistry, pathCostEstimator: pathCostEstimator, + fungibilityPreservationProvider: AssetFungibilityPreservationProvider.createFromKnownChains(), signingWrapperFactory: SigningWrapperFactory(), userStorageFacade: params.userDataStorageFacade, substrateStorageFacade: params.substrateStorageFacade, diff --git a/novawalletIntegrationTests/WalletRemoteQueryFactoryTests.swift b/novawalletIntegrationTests/WalletRemoteQueryFactoryTests.swift index cdc6da549..0a5a2d464 100644 --- a/novawalletIntegrationTests/WalletRemoteQueryFactoryTests.swift +++ b/novawalletIntegrationTests/WalletRemoteQueryFactoryTests.swift @@ -107,7 +107,7 @@ final class WalletRemoteQueryFactoryTests: XCTestCase { let accountId = try "F53d3jeyFvb2eYsgAERhjC8mogao4Kg4GsdezrqiT8aj55v".toAccountId() let balance = try performQuery( for: accountId, - chainId: KnowChainId.statemine, + chainId: KnowChainId.kusamaAssetHub, assetId: 7 ) Logger.shared.info("Did receive: \(balance)") @@ -121,7 +121,7 @@ final class WalletRemoteQueryFactoryTests: XCTestCase { let accountId = try "Cn1mVjBBvLJUWE8GQoeR7JduGt2GxhUXrx191ob3Si6HA9E".toAccountId() let balance = try performQuery( for: accountId, - chainId: KnowChainId.statemine, + chainId: KnowChainId.kusamaAssetHub, assetId: 7 ) Logger.shared.info("Did receive: \(balance)") diff --git a/novawalletIntegrationTests/XcmTransfersFeeTests.swift b/novawalletIntegrationTests/XcmTransfersFeeTests.swift index 9f816ebe6..856f3dc47 100644 --- a/novawalletIntegrationTests/XcmTransfersFeeTests.swift +++ b/novawalletIntegrationTests/XcmTransfersFeeTests.swift @@ -120,7 +120,7 @@ class XcmTransfersFeeTests: XCTestCase { func testKusamaStatemineCrosschainFee() throws { let originChainId = KnowChainId.kusama - let destinationChainId = KnowChainId.statemine + let destinationChainId = KnowChainId.kusamaAssetHub let assetId: AssetModel.Id = 0 let beneficiary = AccountId.zeroAccountId(of: 32) let amount: BigUInt = 1_000_000_000_00 @@ -138,7 +138,7 @@ class XcmTransfersFeeTests: XCTestCase { } func testStatemineKusamaCrosschainFee() throws { - let originChainId = KnowChainId.statemine + let originChainId = KnowChainId.kusamaAssetHub let destinationChainId = KnowChainId.kusama let assetId: AssetModel.Id = 0 let beneficiary = AccountId.zeroAccountId(of: 32) From ad9ab8c6cffe74af57f53d5cfc6198b8bb8a63f2 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 30 Dec 2024 09:58:30 +0100 Subject: [PATCH 8/8] filter intermediate paths that require keep alive --- .../AssetExchange/AssetExchangePathFilter.swift | 4 ++++ .../AssetHubExchange/AssetHubExchangeEdge.swift | 4 ++++ .../AssetExchange/Common/AnyAssetExchangeEdge.swift | 6 ++++++ .../Common/AssetExchangeGraphEdge.swift | 2 ++ .../CrosschainExchange/CrosschainExchangeEdge.swift | 12 ++++++++++++ .../HydraExchange/AssetsHydraExchangeEdge.swift | 4 ++++ 6 files changed, 32 insertions(+) diff --git a/novawallet/Common/Services/AssetExchange/AssetExchangePathFilter.swift b/novawallet/Common/Services/AssetExchange/AssetExchangePathFilter.swift index 673182395..aac5d7f6f 100644 --- a/novawallet/Common/Services/AssetExchange/AssetExchangePathFilter.swift +++ b/novawallet/Common/Services/AssetExchange/AssetExchangePathFilter.swift @@ -49,6 +49,10 @@ extension AssetExchangePathFilter: GraphEdgeFiltering { return true } + if edge.requiresOriginKeepAliveOnIntermediatePosition() { + return false + } + let canPayFees = (chainAssetIn.isUtilityAsset || feeSupport.canPayFee(inNonNative: chainAssetIn)) && edge.canPayNonNativeFeesInIntermediatePosition() diff --git a/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeEdge.swift b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeEdge.swift index c8cb786bf..af5debf9d 100644 --- a/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeEdge.swift +++ b/novawallet/Common/Services/AssetExchange/AssetHubExchange/AssetHubExchangeEdge.swift @@ -78,6 +78,10 @@ extension AssetHubExchangeEdge: AssetExchangableGraphEdge { true } + func requiresOriginKeepAliveOnIntermediatePosition() -> Bool { + false + } + func beginMetaOperation( for amountIn: Balance, amountOut: Balance diff --git a/novawallet/Common/Services/AssetExchange/Common/AnyAssetExchangeEdge.swift b/novawallet/Common/Services/AssetExchange/Common/AnyAssetExchangeEdge.swift index b96a0ac4d..69cffb555 100644 --- a/novawallet/Common/Services/AssetExchange/Common/AnyAssetExchangeEdge.swift +++ b/novawallet/Common/Services/AssetExchange/Common/AnyAssetExchangeEdge.swift @@ -16,6 +16,7 @@ class AnyAssetExchangeEdge { private let shouldIgnoreFeeRequirementClosure: (any AssetExchangableGraphEdge) -> Bool private let canPayFeesInIntermediatePositionClosure: () -> Bool + private let requiresKeepAliveOnIntermediatePositionClosure: () -> Bool private let typeClosure: () -> AssetExchangeEdgeType private let beginMetaOperationClosure: (Balance, Balance) throws -> AssetExchangeMetaOperationProtocol @@ -37,6 +38,7 @@ class AnyAssetExchangeEdge { appendToOperationClosure = edge.appendToOperation shouldIgnoreFeeRequirementClosure = edge.shouldIgnoreFeeRequirement canPayFeesInIntermediatePositionClosure = edge.canPayNonNativeFeesInIntermediatePosition + requiresKeepAliveOnIntermediatePositionClosure = edge.requiresOriginKeepAliveOnIntermediatePosition typeClosure = { edge.type } beginMetaOperationClosure = edge.beginMetaOperation appendToMetaOperationClosure = edge.appendToMetaOperation @@ -74,6 +76,10 @@ extension AnyAssetExchangeEdge: AssetExchangableGraphEdge { canPayFeesInIntermediatePositionClosure() } + func requiresOriginKeepAliveOnIntermediatePosition() -> Bool { + requiresKeepAliveOnIntermediatePositionClosure() + } + func beginMetaOperation(for amountIn: Balance, amountOut: Balance) throws -> AssetExchangeMetaOperationProtocol { try beginMetaOperationClosure(amountIn, amountOut) } diff --git a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeGraphEdge.swift b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeGraphEdge.swift index fdc6cfe76..d7cd4680a 100644 --- a/novawallet/Common/Services/AssetExchange/Common/AssetExchangeGraphEdge.swift +++ b/novawallet/Common/Services/AssetExchange/Common/AssetExchangeGraphEdge.swift @@ -13,6 +13,8 @@ protocol AssetExchangableGraphEdge: GraphQuotableEdge { func canPayNonNativeFeesInIntermediatePosition() -> Bool + func requiresOriginKeepAliveOnIntermediatePosition() -> Bool + var type: AssetExchangeEdgeType { get } func beginMetaOperation(for amountIn: Balance, amountOut: Balance) throws -> AssetExchangeMetaOperationProtocol diff --git a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeEdge.swift b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeEdge.swift index cb0091a17..2b999ead9 100644 --- a/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeEdge.swift +++ b/novawallet/Common/Services/AssetExchange/CrosschainExchange/CrosschainExchangeEdge.swift @@ -72,6 +72,18 @@ extension CrosschainExchangeEdge: AssetExchangableGraphEdge { deliveryFeeNotPaidOrFromHolding() } + func requiresOriginKeepAliveOnIntermediatePosition() -> Bool { + guard + let chainIn = host.allChains[origin.chainId], + let chainAssetIn = chainIn.chainAsset(for: origin.assetId) else { + return false + } + + return host.fungibilityPreservationProvider.requiresPreservationForCrosschain( + assetIn: chainAssetIn + ) + } + func beginMetaOperation( for amountIn: Balance, amountOut: Balance diff --git a/novawallet/Common/Services/AssetExchange/HydraExchange/AssetsHydraExchangeEdge.swift b/novawallet/Common/Services/AssetExchange/HydraExchange/AssetsHydraExchangeEdge.swift index 9ee1abf16..b00ae0b6f 100644 --- a/novawallet/Common/Services/AssetExchange/HydraExchange/AssetsHydraExchangeEdge.swift +++ b/novawallet/Common/Services/AssetExchange/HydraExchange/AssetsHydraExchangeEdge.swift @@ -52,6 +52,10 @@ class AssetsHydraExchangeEdge { true } + func requiresOriginKeepAliveOnIntermediatePosition() -> Bool { + false + } + func beginMetaOperation( for amountIn: Balance, amountOut: Balance