From 03a3177c1f25fb3f3be09fb51310fbdf94c12602 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 5 Sep 2022 00:34:33 +0300 Subject: [PATCH 01/52] transfer all if a user sends via max button --- novawallet.xcodeproj/project.pbxproj | 4 + .../Common/Model/AmountInputResult.swift | 9 ++ .../Calls/Common/OrmlTokenTransfer.swift | 12 +++ .../Calls/Common/SubstrateCallFactory.swift | 27 ++++++ .../Substrate/Calls/Common/TransferCall.swift | 10 ++ .../OnChain/OnChainTransferInteractor.swift | 92 +++++++++++++++---- .../TransferOnChainConfirmInteractor.swift | 4 +- .../TransferOnChainConfirmPresenter.swift | 16 ++-- .../TransferConfirmOnChainViewFactory.swift | 2 +- .../TransferConfirmProtocols.swift | 2 +- .../Model/OnChainTransferAmount.swift | 46 ++++++++++ .../OnChainTransferSetupPresenter.swift | 20 +++- .../OnChainTransferSetupProtocols.swift | 5 +- .../OnChainTransferSetupWireframe.swift | 2 +- 14 files changed, 214 insertions(+), 37 deletions(-) create mode 100644 novawallet/Modules/Transfer/TransferSetup/Model/OnChainTransferAmount.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index c976c01a47..23c3974d00 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -1432,6 +1432,7 @@ 84B018AC26E01A4100C75E28 /* StakingStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B018AB26E01A4100C75E28 /* StakingStateView.swift */; }; 84B018AE26E03FB500C75E28 /* NominatorStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B018AD26E03FB500C75E28 /* NominatorStateView.swift */; }; 84B018B026E0450F00C75E28 /* ValidatorStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B018AF26E0450F00C75E28 /* ValidatorStateView.swift */; }; + 84B28FC428C54441007A1006 /* OnChainTransferAmount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B28FC328C54441007A1006 /* OnChainTransferAmount.swift */; }; 84B5DE53283F7BE500193ED3 /* CollatorsSortType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B5DE52283F7BE500193ED3 /* CollatorsSortType.swift */; }; 84B5DE56283F7C8500193ED3 /* CollatorSelectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B5DE55283F7C8500193ED3 /* CollatorSelectionCell.swift */; }; 84B5DE59283F8B5400193ED3 /* CollatorSelectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B5DE58283F8B5400193ED3 /* CollatorSelectionViewModel.swift */; }; @@ -3991,6 +3992,7 @@ 84B018AB26E01A4100C75E28 /* StakingStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingStateView.swift; sourceTree = ""; }; 84B018AD26E03FB500C75E28 /* NominatorStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominatorStateView.swift; sourceTree = ""; }; 84B018AF26E0450F00C75E28 /* ValidatorStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatorStateView.swift; sourceTree = ""; }; + 84B28FC328C54441007A1006 /* OnChainTransferAmount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnChainTransferAmount.swift; sourceTree = ""; }; 84B5DE52283F7BE500193ED3 /* CollatorsSortType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollatorsSortType.swift; sourceTree = ""; }; 84B5DE55283F7C8500193ED3 /* CollatorSelectionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollatorSelectionCell.swift; sourceTree = ""; }; 84B5DE58283F8B5400193ED3 /* CollatorSelectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollatorSelectionViewModel.swift; sourceTree = ""; }; @@ -7701,6 +7703,7 @@ children = ( 848F8B212863BD1000204BC4 /* TransferSetupInputState.swift */, 842B17FE28649CCD0014CC57 /* CrossChainDestinationSelectionState.swift */, + 84B28FC328C54441007A1006 /* OnChainTransferAmount.swift */, ); path = Model; sourceTree = ""; @@ -13059,6 +13062,7 @@ 84D1F31E260F585E0077DDFE /* AddAccount.swift in Sources */, 846CA77C27099DD90011124C /* WeaklyAnalyticsRewardSource.swift in Sources */, 84452F9D25D6768000F47EC5 /* RuntimeMetadataItem.swift in Sources */, + 84B28FC428C54441007A1006 /* OnChainTransferAmount.swift in Sources */, 84A3B8A82836E4A100DE2669 /* ParachainStakingCandidateMetadata.swift in Sources */, 849842FE26592C2B006BBB9F /* StatusSectionView.swift in Sources */, F42D125F26C1B14D00E59214 /* AnalyticsValidatorsViewModelFactory.swift in Sources */, diff --git a/novawallet/Common/Model/AmountInputResult.swift b/novawallet/Common/Model/AmountInputResult.swift index 326a424bac..58e95c76f3 100644 --- a/novawallet/Common/Model/AmountInputResult.swift +++ b/novawallet/Common/Model/AmountInputResult.swift @@ -12,4 +12,13 @@ enum AmountInputResult { return value } } + + var isMax: Bool { + switch self { + case let .rate(value): + return value == 1 + case .absolute: + return false + } + } } diff --git a/novawallet/Common/Substrate/Calls/Common/OrmlTokenTransfer.swift b/novawallet/Common/Substrate/Calls/Common/OrmlTokenTransfer.swift index 391639623f..b7a2a63a54 100644 --- a/novawallet/Common/Substrate/Calls/Common/OrmlTokenTransfer.swift +++ b/novawallet/Common/Substrate/Calls/Common/OrmlTokenTransfer.swift @@ -13,3 +13,15 @@ struct OrmlTokenTransfer: Codable { let currencyId: JSON @StringCodable var amount: BigUInt } + +struct OrmlTokenTransferAll: Codable { + enum CodingKeys: String, CodingKey { + case dest + case currencyId = "currency_id" + case keepAlive = "keep_alive" + } + + let dest: MultiAddress + let currencyId: JSON + let keepAlive: Bool +} diff --git a/novawallet/Common/Substrate/Calls/Common/SubstrateCallFactory.swift b/novawallet/Common/Substrate/Calls/Common/SubstrateCallFactory.swift index a47f342e2f..5bcfd53985 100644 --- a/novawallet/Common/Substrate/Calls/Common/SubstrateCallFactory.swift +++ b/novawallet/Common/Substrate/Calls/Common/SubstrateCallFactory.swift @@ -9,6 +9,8 @@ protocol SubstrateCallFactoryProtocol { amount: BigUInt ) -> RuntimeCall + func nativeTransferAll(to receiver: AccountId) -> RuntimeCall + func assetsTransfer( to receiver: AccountId, assetId: String, @@ -22,6 +24,12 @@ protocol SubstrateCallFactoryProtocol { amount: BigUInt ) -> RuntimeCall + func ormlTransferAll( + in moduleName: String, + currencyId: JSON, + receiverId: AccountId + ) -> RuntimeCall + func bond( amount: BigUInt, controller: String, @@ -135,6 +143,11 @@ final class SubstrateCallFactory: SubstrateCallFactoryProtocol { return RuntimeCall(moduleName: "Balances", callName: "transfer", args: args) } + func nativeTransferAll(to receiver: AccountId) -> RuntimeCall { + let args = TransferAllCall(dest: .accoundId(receiver), keepAlive: false) + return RuntimeCall(moduleName: "Balances", callName: "transfer_all", args: args) + } + func ormlTransfer( in moduleName: String, currencyId: JSON, @@ -150,6 +163,20 @@ final class SubstrateCallFactory: SubstrateCallFactoryProtocol { return RuntimeCall(moduleName: moduleName, callName: "transfer", args: args) } + func ormlTransferAll( + in moduleName: String, + currencyId: JSON, + receiverId: AccountId + ) -> RuntimeCall { + let args = OrmlTokenTransferAll( + dest: .accoundId(receiverId), + currencyId: currencyId, + keepAlive: false + ) + + return RuntimeCall(moduleName: moduleName, callName: "transfer_all", args: args) + } + func setPayee(for destination: RewardDestinationArg) -> RuntimeCall { let args = SetPayeeCall(payee: destination) return RuntimeCall(moduleName: "Staking", callName: "set_payee", args: args) diff --git a/novawallet/Common/Substrate/Calls/Common/TransferCall.swift b/novawallet/Common/Substrate/Calls/Common/TransferCall.swift index 2b0e250f1a..93c4239352 100644 --- a/novawallet/Common/Substrate/Calls/Common/TransferCall.swift +++ b/novawallet/Common/Substrate/Calls/Common/TransferCall.swift @@ -6,3 +6,13 @@ struct TransferCall: Codable { let dest: MultiAddress @StringCodable var value: BigUInt } + +struct TransferAllCall: Codable { + enum CodingKeys: String, CodingKey { + case dest + case keepAlive = "keep_alive" + } + + let dest: MultiAddress + let keepAlive: Bool +} diff --git a/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift b/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift index 2aaaa346e1..8c373a5d23 100644 --- a/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift +++ b/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift @@ -227,42 +227,94 @@ class OnChainTransferInteractor: RuntimeConstantFetching { } } - func addingTransferCommand( + func addingOrmlTransferCommand( to builder: ExtrinsicBuilderProtocol, - amount: BigUInt, - recepient: AccountId + amount: OnChainTransferAmount, + recepient: AccountId, + currencyId: JSON, + module: String ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { - guard let sendingAssetInfo = sendingAssetInfo else { - return (builder, nil) - } - - switch sendingAssetInfo { - case let .orml(currencyId, _, module, _): + switch amount { + case let .concrete(value): let call = callFactory.ormlTransfer( in: module, currencyId: currencyId, receiverId: recepient, - amount: amount + amount: value ) let newBuilder = try builder.adding(call: call) return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) - case let .statemine(extras): - let call = callFactory.assetsTransfer( - to: recepient, - assetId: extras.assetId, - amount: amount - ) + case .all: + let call = callFactory.ormlTransferAll(in: module, currencyId: currencyId, receiverId: recepient) + let newBuilder = try builder.adding(call: call) + return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) + } + } + func addingNativeTransferCommand( + to builder: ExtrinsicBuilderProtocol, + amount: OnChainTransferAmount, + recepient: AccountId + ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { + switch amount { + case let .concrete(value): + let call = callFactory.nativeTransfer(to: recepient, amount: value) let newBuilder = try builder.adding(call: call) return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) - case .native: - let call = callFactory.nativeTransfer(to: recepient, amount: amount) + case .all: + let call = callFactory.nativeTransferAll(to: recepient) let newBuilder = try builder.adding(call: call) return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) } } + func addingAssetsTransferCommand( + to builder: ExtrinsicBuilderProtocol, + amount: OnChainTransferAmount, + recepient: AccountId, + extras: StatemineAssetExtras + ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { + let call = callFactory.assetsTransfer( + to: recepient, + assetId: extras.assetId, + amount: amount.value + ) + + let newBuilder = try builder.adding(call: call) + return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) + } + + func addingTransferCommand( + to builder: ExtrinsicBuilderProtocol, + amount: OnChainTransferAmount, + recepient: AccountId + ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { + guard let sendingAssetInfo = sendingAssetInfo else { + return (builder, nil) + } + + switch sendingAssetInfo { + case let .orml(currencyId, _, module, _): + return try addingOrmlTransferCommand( + to: builder, + amount: amount, + recepient: recepient, + currencyId: currencyId, + module: module + ) + case let .statemine(extras): + return try addingAssetsTransferCommand( + to: builder, + amount: amount, + recepient: recepient, + extras: extras + ) + case .native: + return try addingNativeTransferCommand(to: builder, amount: amount, recepient: recepient) + } + } + private func cancelSetupCall() { let cancellingCall = setupCall setupCall = nil @@ -393,10 +445,10 @@ extension OnChainTransferInteractor { operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) } - func estimateFee(for amount: BigUInt, recepient: AccountId?) { + func estimateFee(for amount: OnChainTransferAmount, recepient: AccountId?) { let recepientAccountId = recepient ?? AccountId.zeroAccountId(of: chain.accountIdSize) - let identifier = String(amount) + "-" + recepientAccountId.toHex() + let identifier = String(amount.value) + "-" + recepientAccountId.toHex() + "-" + amount.name feeProxy.estimateFee( using: extrinsicService, diff --git a/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmInteractor.swift b/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmInteractor.swift index 383c44f979..0250eee15e 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmInteractor.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmInteractor.swift @@ -68,7 +68,7 @@ final class TransferOnChainConfirmInteractor: OnChainTransferInteractor { } extension TransferOnChainConfirmInteractor: TransferConfirmOnChainInteractorInputProtocol { - func submit(amount: BigUInt, recepient: AccountAddress, lastFee: BigUInt?) { + func submit(amount: OnChainTransferAmount, recepient: AccountAddress, lastFee: BigUInt?) { do { let accountId = try recepient.toAccountId(using: chain.chainFormat) @@ -105,7 +105,7 @@ extension TransferOnChainConfirmInteractor: TransferConfirmOnChainInteractorInpu let details = PersistTransferDetails( sender: sender, receiver: recepient, - amount: amount, + amount: amount.value, txHash: txHashData, callPath: callCodingPath, fee: lastFee diff --git a/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmPresenter.swift b/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmPresenter.swift index 9d0d3162c1..c5cf352c1a 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmPresenter.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/OnChain/TransferOnChainConfirmPresenter.swift @@ -12,7 +12,7 @@ final class TransferOnChainConfirmPresenter: OnChainTransferPresenter { let recepientAccountAddress: AccountAddress let wallet: MetaAccountModel - let amount: Decimal + let amount: OnChainTransferAmount private lazy var walletIconGenerator = NovaIconGenerator() @@ -21,7 +21,7 @@ final class TransferOnChainConfirmPresenter: OnChainTransferPresenter { wireframe: TransferConfirmWireframeProtocol, wallet: MetaAccountModel, recepient: AccountAddress, - amount: Decimal, + amount: OnChainTransferAmount, displayAddressViewModelFactory: DisplayAddressViewModelFactoryProtocol, chainAsset: ChainAsset, networkViewModelFactory: NetworkViewModelFactoryProtocol, @@ -104,7 +104,7 @@ final class TransferOnChainConfirmPresenter: OnChainTransferPresenter { private func provideAmountViewModel() { let viewModel = sendingBalanceViewModelFactory.spendingAmountFromPrice( - amount, + amount.value, priceData: sendingAssetPrice ).value(for: selectedLocale) @@ -129,11 +129,11 @@ final class TransferOnChainConfirmPresenter: OnChainTransferPresenter { override func refreshFee() { let assetInfo = chainAsset.assetDisplayInfo - guard let amountValue = amount.toSubstrateAmount(precision: assetInfo.assetPrecision) else { + guard let amountInPlank = amount.flatMap({ $0.toSubstrateAmount(precision: assetInfo.assetPrecision) }) else { return } - interactor.estimateFee(for: amountValue, recepient: getRecepientAccountId()) + interactor.estimateFee(for: amountInPlank, recepient: getRecepientAccountId()) } override func askFeeRetry() { @@ -202,7 +202,7 @@ extension TransferOnChainConfirmPresenter: TransferConfirmPresenterProtocol { func submit() { let assetPrecision = chainAsset.assetDisplayInfo.assetPrecision guard - let amountValue = amount.toSubstrateAmount(precision: assetPrecision), + let amountInPlank = amount.flatMap({ $0.toSubstrateAmount(precision: assetPrecision) }), let utilityAsset = chainAsset.chain.utilityAsset() else { return } @@ -210,7 +210,7 @@ extension TransferOnChainConfirmPresenter: TransferConfirmPresenterProtocol { let utilityAssetInfo = ChainAsset(chain: chainAsset.chain, asset: utilityAsset).assetDisplayInfo let validators: [DataValidating] = baseValidators( - for: amount, + for: amount.value, recepientAddress: recepientAccountAddress, utilityAssetInfo: utilityAssetInfo, selectedLocale: selectedLocale @@ -224,7 +224,7 @@ extension TransferOnChainConfirmPresenter: TransferConfirmPresenterProtocol { strongSelf.view?.didStartLoading() strongSelf.interactor.submit( - amount: amountValue, + amount: amountInPlank, recepient: strongSelf.recepientAccountAddress, lastFee: strongSelf.fee ) diff --git a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift index b21a80fba7..b8e08b1e99 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmOnChainViewFactory.swift @@ -7,7 +7,7 @@ struct TransferConfirmOnChainViewFactory { static func createView( chainAsset: ChainAsset, recepient: AccountAddress, - amount: Decimal + amount: OnChainTransferAmount ) -> TransferConfirmOnChainViewProtocol? { let walletSettings = SelectedWalletSettings.shared diff --git a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmProtocols.swift b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmProtocols.swift index 7de0eb4d17..8bb0710f11 100644 --- a/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmProtocols.swift +++ b/novawallet/Modules/Transfer/TransferConfirm/TransferConfirmProtocols.swift @@ -24,7 +24,7 @@ protocol TransferConfirmPresenterProtocol: AnyObject { } protocol TransferConfirmOnChainInteractorInputProtocol: OnChainTransferSetupInteractorInputProtocol { - func submit(amount: BigUInt, recepient: AccountAddress, lastFee: BigUInt?) + func submit(amount: OnChainTransferAmount, recepient: AccountAddress, lastFee: BigUInt?) } protocol TransferConfirmCrossChainInteractorInputProtocol: CrossChainTransferSetupInteractorInputProtocol { diff --git a/novawallet/Modules/Transfer/TransferSetup/Model/OnChainTransferAmount.swift b/novawallet/Modules/Transfer/TransferSetup/Model/OnChainTransferAmount.swift new file mode 100644 index 0000000000..76c7bfbe04 --- /dev/null +++ b/novawallet/Modules/Transfer/TransferSetup/Model/OnChainTransferAmount.swift @@ -0,0 +1,46 @@ +import Foundation + +enum OnChainTransferAmount { + case concrete(value: T) + case all(value: T) + + var value: T { + switch self { + case let .concrete(value): + return value + case let .all(value): + return value + } + } + + var name: String { + switch self { + case .concrete: + return "concrete" + case .all: + return "all" + } + } + + func flatMap(_ closure: (T) -> V?) -> OnChainTransferAmount? { + guard let newValue = closure(value) else { + return nil + } + + switch self { + case .concrete: + return .concrete(value: newValue) + case .all: + return .all(value: newValue) + } + } + + func map(_ closure: (T) -> V) -> OnChainTransferAmount { + switch self { + case let .concrete(value): + return .concrete(value: closure(value)) + case let .all(value): + return .all(value: closure(value)) + } + } +} diff --git a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupPresenter.swift b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupPresenter.swift index 6c41f9160f..53c0ae67c0 100644 --- a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupPresenter.swift +++ b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupPresenter.swift @@ -199,12 +199,20 @@ final class OnChainTransferSetupPresenter: OnChainTransferPresenter, OnChainTran let inputAmount = inputResult?.absoluteValue(from: balanceMinusFee()) ?? 0 let assetInfo = chainAsset.assetDisplayInfo - guard let amount = inputAmount.toSubstrateAmount( + guard let amountValue = inputAmount.toSubstrateAmount( precision: assetInfo.assetPrecision ) else { return } + let amount: OnChainTransferAmount + + if let inputResult = inputResult, inputResult.isMax { + amount = .all(value: amountValue) + } else { + amount = .concrete(value: amountValue) + } + updateFee(nil) updateFeeView() @@ -339,7 +347,7 @@ extension OnChainTransferSetupPresenter: TransferSetupChildPresenterProtocol { DataValidationRunner(validators: validators).runValidation { [weak self] in guard - let amount = sendingAmount, + let amountValue = sendingAmount, let recepient = self?.partialRecepientAddress, let chainAsset = self?.chainAsset else { return @@ -347,6 +355,14 @@ extension OnChainTransferSetupPresenter: TransferSetupChildPresenterProtocol { self?.logger?.debug("Did complete validation") + let amount: OnChainTransferAmount + + if let inputResult = self?.inputResult, inputResult.isMax { + amount = .all(value: amountValue) + } else { + amount = .concrete(value: amountValue) + } + self?.wireframe.showConfirmation( from: self?.view, chainAsset: chainAsset, diff --git a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupProtocols.swift b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupProtocols.swift index cde52c5109..5f1c8647d5 100644 --- a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupProtocols.swift +++ b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupProtocols.swift @@ -1,8 +1,9 @@ import BigInt +import Foundation protocol OnChainTransferSetupInteractorInputProtocol: AnyObject { func setup() - func estimateFee(for amount: BigUInt, recepient: AccountId?) + func estimateFee(for amount: OnChainTransferAmount, recepient: AccountId?) func change(recepient: AccountId?) } @@ -25,7 +26,7 @@ protocol OnChainTransferSetupWireframeProtocol: AlertPresentable, ErrorPresentab func showConfirmation( from view: TransferSetupChildViewProtocol?, chainAsset: ChainAsset, - sendingAmount: Decimal, + sendingAmount: OnChainTransferAmount, recepient: AccountAddress ) } diff --git a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupWireframe.swift b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupWireframe.swift index 56b5424a96..c63759b863 100644 --- a/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupWireframe.swift +++ b/novawallet/Modules/Transfer/TransferSetup/OnChain/OnChainTransferSetupWireframe.swift @@ -7,7 +7,7 @@ final class OnChainTransferSetupWireframe: OnChainTransferSetupWireframeProtocol func showConfirmation( from _: TransferSetupChildViewProtocol?, chainAsset: ChainAsset, - sendingAmount: Decimal, + sendingAmount: OnChainTransferAmount, recepient: AccountAddress ) { guard let confirmView = TransferConfirmOnChainViewFactory.createView( From 0b1e076978f600d2d85b9032d73bed123f7e5f4b Mon Sep 17 00:00:00 2001 From: Gulnaz <666lynx666@mail.ru> Date: Wed, 14 Sep 2022 16:02:57 +0400 Subject: [PATCH 02/52] Total balance breakdown modal sheet (#394) * init * added balances locks * remove dynamic key creation * added balances locks * added module, bugfix subscription * convert prices * ui * bugfix * bugfix * use formatter for percent value * renaming * clean up * bugfix * added check for localization setup * PR fixes * CollectionView delegate for ModalSheet * fix formatter, bugfix locks * fix nil in locks subscription * bugfixes * added filter for repository * move locks from base to assetlist * buildfix * remove sorting from subscription * rename ext * fix filter for locks --- novawallet.xcodeproj/project.pbxproj | 100 +++++++++- .../iconTransferable.imageset/Contents.json | 12 ++ .../iconTransferable.imageset/Icon.pdf | Bin 0 -> 3119 bytes .../WalletLocalStorageSubscriber.swift | 42 ++++ .../WalletLocalSubscriptionHandler.swift | 10 + .../WalletLocalSubscriptionFactory.swift | 46 +++++ .../Foundation/Array+AddOrReplace.swift | 11 ++ .../Foundation/NSPredicate+Filter.swift | 33 ++++ ...llectionViewDiffableDataSource+apply.swift | 13 ++ .../Helpers/SubstrateRepositoryFactory.swift | 22 +++ ...countInfoSubscriptionHandlingFactory.swift | 55 +++--- .../AccountInfoUpdatingService.swift | 14 +- .../AssetsUpdatingService.swift | 11 +- ...rmlAccountSubcriptionHandlingFactory.swift | 49 ----- ...OrmlTokenSubscriptionHandlingFactory.swift | 41 ++++ .../TokenSubscriptionFactory.swift | 145 ++++++++++++++ .../WalletRemoteSubscriptionService.swift | 130 +++++++++---- .../WalletRemoteSubscriptionWrapper.swift | 12 +- .../LocksSubscription.swift | 182 ++++++++++++++++++ .../EntityToModel/AssetLockMapper.swift | 41 ++++ .../SubstrateDataModel.xcdatamodel/contents | 11 +- .../Common/Substrate/Types/AssetLock.swift | 47 +++++ .../AssetList/AssetListInteractor.swift | 89 ++++++++- .../AssetList/AssetListPresenter.swift | 27 ++- .../AssetList/AssetListProtocols.swift | 10 + .../AssetList/AssetListViewController.swift | 4 +- .../AssetList/AssetListWireframe.swift | 25 +++ .../Base/AssetListBaseInteractor.swift | 91 +++++---- .../AssetListBaseInteractorProtocol.swift | 2 +- .../Base/AssetListBasePresenter.swift | 11 +- .../AssetsSearch/AssetsSearchPresenter.swift | 2 +- .../Locks/LocksBalanceViewModelFactory.swift | 147 ++++++++++++++ novawallet/Modules/Locks/LocksPresenter.swift | 170 ++++++++++++++++ novawallet/Modules/Locks/LocksProtocols.swift | 35 ++++ .../Modules/Locks/LocksViewController.swift | 110 +++++++++++ .../Modules/Locks/LocksViewFactory.swift | 36 ++++ novawallet/Modules/Locks/LocksViewInput.swift | 6 + .../Modules/Locks/LocksViewLayout.swift | 22 +++ novawallet/Modules/Locks/LocksWireframe.swift | 7 + .../Locks/View/LockCollectionViewCell.swift | 40 ++++ .../Modules/Locks/View/LocksHeaderView.swift | 47 +++++ .../YourWallets/CollectionViewDelegate.swift | 15 ++ .../GenericCollectionViewLayout.swift | 121 ++++++++++++ .../ModalSheetCollectionViewProtocol.swift | 19 ++ .../YourWalletsViewController.swift | 62 +++--- .../YourWallets/YourWalletsViewLayout.swift | 107 +--------- 46 files changed, 1913 insertions(+), 319 deletions(-) create mode 100644 novawallet/Assets.xcassets/iconTransferable.imageset/Contents.json create mode 100644 novawallet/Assets.xcassets/iconTransferable.imageset/Icon.pdf create mode 100644 novawallet/Common/Extension/Foundation/Array+AddOrReplace.swift create mode 100644 novawallet/Common/Extension/UIKit/UICollectionViewDiffableDataSource+apply.swift delete mode 100644 novawallet/Common/Services/RemoteSubscription/OrmlAccountSubcriptionHandlingFactory.swift create mode 100644 novawallet/Common/Services/RemoteSubscription/OrmlTokenSubscriptionHandlingFactory.swift create mode 100644 novawallet/Common/Services/RemoteSubscription/TokenSubscriptionFactory.swift create mode 100644 novawallet/Common/Services/WebSocketService/StorageSubscription/LocksSubscription.swift create mode 100644 novawallet/Common/Storage/EntityToModel/AssetLockMapper.swift create mode 100644 novawallet/Common/Substrate/Types/AssetLock.swift create mode 100644 novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift create mode 100644 novawallet/Modules/Locks/LocksPresenter.swift create mode 100644 novawallet/Modules/Locks/LocksProtocols.swift create mode 100644 novawallet/Modules/Locks/LocksViewController.swift create mode 100644 novawallet/Modules/Locks/LocksViewFactory.swift create mode 100644 novawallet/Modules/Locks/LocksViewInput.swift create mode 100644 novawallet/Modules/Locks/LocksViewLayout.swift create mode 100644 novawallet/Modules/Locks/LocksWireframe.swift create mode 100644 novawallet/Modules/Locks/View/LockCollectionViewCell.swift create mode 100644 novawallet/Modules/Locks/View/LocksHeaderView.swift create mode 100644 novawallet/Modules/YourWallets/CollectionViewDelegate.swift create mode 100644 novawallet/Modules/YourWallets/GenericCollectionViewLayout.swift create mode 100644 novawallet/Modules/YourWallets/ModalSheetCollectionViewProtocol.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index eea439268f..0fbfe4464b 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -124,6 +124,7 @@ 2F6FA089995FD12FB2AA814B /* ParitySignerWelcomePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45CB342D1F680257A548CF5 /* ParitySignerWelcomePresenter.swift */; }; 2F95EEA6CBFDF483124ECF8F /* ParaStkUnstakePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CBA1296E4C6E04EC9C5CA98 /* ParaStkUnstakePresenter.swift */; }; 2FCB062A2D873BD72B795DB3 /* AssetSelectionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A0A5EE9BE2862B085712A0 /* AssetSelectionPresenter.swift */; }; + 30413A3C5ADB96B7D663F94D /* LocksWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = E30E541992BF608923DABE5F /* LocksWireframe.swift */; }; 30542C0BD486FD1583F36BA2 /* LedgerNetworkSelectionProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B62C2CBCFF1865A1CA0F1B4 /* LedgerNetworkSelectionProtocols.swift */; }; 3086C94FE01CDFC4F79A9D7F /* DAppAuthConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21BCBFCBE606A354CB652289 /* DAppAuthConfirmViewController.swift */; }; 3133215566E418F40844A60E /* ExportMnemonicWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ACA4A5B186EE6D40BFE9D66 /* ExportMnemonicWireframe.swift */; }; @@ -1908,7 +1909,7 @@ 84F13F1426F20AA2006725FF /* StakingSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F13F1326F20AA2006725FF /* StakingSettingsTests.swift */; }; 84F13F1C26F2B8C2006725FF /* JSONRPCError+Presentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F13F1B26F2B8C1006725FF /* JSONRPCError+Presentable.swift */; }; 84F18D4A27A1869E00CA7554 /* OrmlAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F18D4927A1869E00CA7554 /* OrmlAccount.swift */; }; - 84F18D4C27A1874000CA7554 /* OrmlAccountSubcriptionHandlingFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F18D4B27A1874000CA7554 /* OrmlAccountSubcriptionHandlingFactory.swift */; }; + 84F18D4C27A1874000CA7554 /* TokenSubscriptionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F18D4B27A1874000CA7554 /* TokenSubscriptionFactory.swift */; }; 84F18D4E27A18C1400CA7554 /* OrmlAccountSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F18D4D27A18C1400CA7554 /* OrmlAccountSubscription.swift */; }; 84F1A0712869DA51007DB053 /* AssetBalanceExistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F1A0702869DA51007DB053 /* AssetBalanceExistence.swift */; }; 84F1A073286A5DDA007DB053 /* CommonRetryable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F1A072286A5DDA007DB053 /* CommonRetryable.swift */; }; @@ -2034,6 +2035,7 @@ 85A093F6055DDD2E2E9253F2 /* ControllerAccountProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F829E7F8B39EE7D977001510 /* ControllerAccountProtocols.swift */; }; 86EB789787B731691B36C827 /* OnChainTransferSetupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1A2F7E5E278FDCC89FE097 /* OnChainTransferSetupPresenter.swift */; }; 87F7556E02F6F5BB6F1B1AEA /* ParitySignerTxQrViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A5DCA28ABF42D342BBDF9A /* ParitySignerTxQrViewLayout.swift */; }; + 880855ED28D062A9004255E7 /* Array+AddOrReplace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855EC28D062A9004255E7 /* Array+AddOrReplace.swift */; }; 8828C05828B4A67000555CB6 /* Prism.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8828C05728B4A67000555CB6 /* Prism.swift */; }; 8828C05A28B4A6A800555CB6 /* Samples.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8828C05928B4A6A800555CB6 /* Samples.swift */; }; 8828F4F328AD2734009E0B7C /* CrowdloansCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8828F4F228AD2734009E0B7C /* CrowdloansCalculator.swift */; }; @@ -2041,10 +2043,12 @@ 882A5CED28AFCE3600D0D798 /* ReturnInIntervalsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882A5CEC28AFCE3600D0D798 /* ReturnInIntervalsViewModel.swift */; }; 882A5CEF28AFCE6000D0D798 /* FormattedReturnInIntervalsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882A5CEE28AFCE6000D0D798 /* FormattedReturnInIntervalsViewModel.swift */; }; 882AA13028AE64DC0093BC63 /* CrowdloanYourContributionsTotalCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882AA12F28AE64DC0093BC63 /* CrowdloanYourContributionsTotalCell.swift */; }; + 8831F10028C65B95009F7682 /* AssetLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8831F0FF28C65B95009F7682 /* AssetLock.swift */; }; 8836AF4428AA293500A94EDD /* CurrencyManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8836AF4328AA293500A94EDD /* CurrencyManagerTests.swift */; }; 8836AF4828AA49AB00A94EDD /* Currency+btc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8836AF4728AA49AB00A94EDD /* Currency+btc.swift */; }; 8836AF4A28AA4B9300A94EDD /* CurrencyRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8836AF4928AA4B9300A94EDD /* CurrencyRepositoryTests.swift */; }; 8836AF4D28AA515900A94EDD /* currencies.json in Resources */ = {isa = PBXBuildFile; fileRef = 8836AF4B28AA4E4800A94EDD /* currencies.json */; }; + 884048D428C723F00085FFA6 /* OrmlTokenSubscriptionHandlingFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884048D328C723F00085FFA6 /* OrmlTokenSubscriptionHandlingFactory.swift */; }; 8842104C289BBA6400306F2C /* currencies.json in Resources */ = {isa = PBXBuildFile; fileRef = 8842104B289BBA6400306F2C /* currencies.json */; }; 88421055289BBA8D00306F2C /* CurrencyViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8842104E289BBA8D00306F2C /* CurrencyViewLayout.swift */; }; 88421056289BBA8D00306F2C /* CurrencyPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8842104F289BBA8D00306F2C /* CurrencyPresenter.swift */; }; @@ -2079,7 +2083,18 @@ 88A5317B28B9149600AF18F5 /* UIImage+DrawableIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A5317A28B9149600AF18F5 /* UIImage+DrawableIcon.swift */; }; 88A5317D28B9170100AF18F5 /* NSCollectionLayoutSection+create.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A5317C28B9170100AF18F5 /* NSCollectionLayoutSection+create.swift */; }; 88A5318028B9328E00AF18F5 /* YourWalletsViewSectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A5317F28B9328E00AF18F5 /* YourWalletsViewSectionModel.swift */; }; + 88A6BCFF28CA15400047E4C2 /* LocksBalanceViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A6BCFE28CA15400047E4C2 /* LocksBalanceViewModelFactory.swift */; }; + 88A6BD0128CA15710047E4C2 /* LocksViewInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A6BD0028CA15710047E4C2 /* LocksViewInput.swift */; }; 88AA0FB828B60E6A00931800 /* YourWalletsControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AA0FB728B60E6A00931800 /* YourWalletsControlView.swift */; }; + 88AC186128CA3EE100892A9B /* LocksViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AC186028CA3EE100892A9B /* LocksViewLayout.swift */; }; + 88AC186328CA3F0000892A9B /* GenericCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AC186228CA3F0000892A9B /* GenericCollectionViewLayout.swift */; }; + 88AC186528CA461F00892A9B /* ModalSheetCollectionViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AC186428CA461F00892A9B /* ModalSheetCollectionViewProtocol.swift */; }; + 88AF35DE28C21D28003730DA /* LocksSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AF35DD28C21D28003730DA /* LocksSubscription.swift */; }; + 88C017E628C60A65003B2D28 /* AssetLockMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C017E528C60A65003B2D28 /* AssetLockMapper.swift */; }; + 88C7165428C894510015D1E9 /* CollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C7165328C894510015D1E9 /* CollectionViewDelegate.swift */; }; + 88C7165628C8CD050015D1E9 /* UICollectionViewDiffableDataSource+apply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C7165528C8CD050015D1E9 /* UICollectionViewDiffableDataSource+apply.swift */; }; + 88C7165828C8D3280015D1E9 /* LockCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C7165728C8D3270015D1E9 /* LockCollectionViewCell.swift */; }; + 88C7165A28C8D3450015D1E9 /* LocksHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C7165928C8D3450015D1E9 /* LocksHeaderView.swift */; }; 88D997AE28AB86FE006135A5 /* YourContributionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D997AD28AB86FD006135A5 /* YourContributionsView.swift */; }; 88D997B028ABC8C0006135A5 /* BlurredTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D997AF28ABC8C0006135A5 /* BlurredTableViewCell.swift */; }; 88D997B228ABC90E006135A5 /* AboutCrowdloansView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D997B128ABC90E006135A5 /* AboutCrowdloansView.swift */; }; @@ -2090,6 +2105,7 @@ 88F7716428BF6B59008C028A /* GenericMultiValueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F7716328BF6B59008C028A /* GenericMultiValueView.swift */; }; 8916E9179CF5409E65D1B3A6 /* NftDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A46EE888D60C1538A0A3EFC /* NftDetailsProtocols.swift */; }; 8A19EC93E6A6972327116D80 /* ParaStkStakeConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1139FB38E7D8D25A36726089 /* ParaStkStakeConfirmProtocols.swift */; }; + 8A23DD1F4146639EA2F7AEF6 /* LocksViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81B239BD9C150BFE9A82B0 /* LocksViewFactory.swift */; }; 8AEF593AFE8F59F7DC0A5753 /* CustomValidatorListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 365CAE2753E7D5F9B9DB7D1F /* CustomValidatorListInteractor.swift */; }; 8BBA871751CAB9F0A8506317 /* AnalyticsValidatorsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B896BA49EE0D4C77401D097 /* AnalyticsValidatorsWireframe.swift */; }; 8BF525D6B5DFB7CF6C03B015 /* AnalyticsValidatorsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955A6977CCE5861E4F5DCFBB /* AnalyticsValidatorsViewController.swift */; }; @@ -2138,6 +2154,7 @@ 9D5926790B055C56FB74B282 /* AccountManagementProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B5072E250B7277F605855B3 /* AccountManagementProtocols.swift */; }; 9DFB37659A6B911A4D54623E /* AccountConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E992CCDC1D581F7E9D3F1CA /* AccountConfirmInteractor.swift */; }; 9E15912C35D50C6D738FD04C /* AccountConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5002B8FA2695F470587677D2 /* AccountConfirmProtocols.swift */; }; + 9E40464B7687006B1EE75C72 /* LocksProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4F9944B0577EFF25A0643FE /* LocksProtocols.swift */; }; 9E4E458C92D12B24D5EAD893 /* ControllerAccountInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 748E0AF1A286016CB220155C /* ControllerAccountInteractor.swift */; }; 9F3E2D64D77BF89B474BF1E3 /* DAppOperationConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43FBC2EA83A121CEBD25549D /* DAppOperationConfirmViewController.swift */; }; 9F4A48B1BE3A1110A0CF9F36 /* ReferralCrowdloanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDDB2B35CD3299F50613141 /* ReferralCrowdloanViewController.swift */; }; @@ -2300,6 +2317,7 @@ BA7AEE82627CFC0AFD69B299 /* RecommendedValidatorListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2580363AC3E4A9CD40256E /* RecommendedValidatorListPresenter.swift */; }; BB29490A4E8472A7DB781BC4 /* TransferOnChainConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D385A8FCB0E6F3F6B6872F01 /* TransferOnChainConfirmPresenter.swift */; }; BD571417BD18C711B76E1D62 /* ExportSeedWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B4C1B5D56DB69BA0AECF731 /* ExportSeedWireframe.swift */; }; + BE301A0F2286CCEF6A02D341 /* LocksPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F9E9C70B0C14570AB783AF /* LocksPresenter.swift */; }; BE3F6213B26F35EB6324DBD8 /* ControllerAccountWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB9EDB05686DF11958145E1 /* ControllerAccountWireframe.swift */; }; BE8CF97B6EA62C75277B78AA /* MoonbeamTermsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D8F02830944DBAF72D8A41 /* MoonbeamTermsProtocols.swift */; }; BEA539EE97A287868FD8BE46 /* AssetSelectionViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9622C6C3102EF12BEE78D63D /* AssetSelectionViewFactory.swift */; }; @@ -2343,6 +2361,7 @@ CDB78A5A733E4A4F1A2C48C8 /* AssetSelectionWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7AD1285797131E836CD994B /* AssetSelectionWireframe.swift */; }; CDED41B125E1D5128736B933 /* ParitySignerTxScanViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CD8C618D61C78EA8C58532 /* ParitySignerTxScanViewLayout.swift */; }; CE2792E78B14CE02394D8CF4 /* ReferralCrowdloanViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 594BC61689EC942ED0A64A4A /* ReferralCrowdloanViewLayout.swift */; }; + CE4C1344F03A5132C601A594 /* LocksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3D4D2E89D40718677685CE1 /* LocksViewController.swift */; }; CE773CEC15A83AA6D0B404B8 /* DAppListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2BB29AE8E556E6756A4F02 /* DAppListViewController.swift */; }; D1C6EABB48DC3EE254E5A095 /* CrowdloanContributionConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28F5B57A24265C36A5F19B78 /* CrowdloanContributionConfirmPresenter.swift */; }; D344C6DAC1F8BB6152BA8DD0 /* RecommendedValidatorListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C6573C52692E4A56E35FF9 /* RecommendedValidatorListProtocols.swift */; }; @@ -2709,6 +2728,7 @@ 256215C11DC0E091660034EA /* CrowdloanYourContributionsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanYourContributionsViewController.swift; sourceTree = ""; }; 2667181A57442FB4D93B7F36 /* ParitySignerWelcomeViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerWelcomeViewFactory.swift; sourceTree = ""; }; 26A0FFF412031C1373EBE2B8 /* ParaStkRebondProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkRebondProtocols.swift; sourceTree = ""; }; + 26F9E9C70B0C14570AB783AF /* LocksPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocksPresenter.swift; sourceTree = ""; }; 270B309EC85D8897A4ADD98A /* CustomValidatorListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CustomValidatorListViewController.swift; sourceTree = ""; }; 27D5AF2F7609ADE855308089 /* AccountExportPasswordViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordViewController.swift; sourceTree = ""; }; 289E4923B76F126DD8E3902B /* NftListViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftListViewFactory.swift; sourceTree = ""; }; @@ -2913,6 +2933,7 @@ 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 = ""; }; 7B1A00299D9B50045E1A1983 /* DAppAddFavoriteProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAddFavoriteProtocols.swift; sourceTree = ""; }; + 7B81B239BD9C150BFE9A82B0 /* LocksViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocksViewFactory.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 = ""; }; 7DDDB2B35CD3299F50613141 /* ReferralCrowdloanViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferralCrowdloanViewController.swift; sourceTree = ""; }; @@ -4563,7 +4584,7 @@ 84F13F1326F20AA2006725FF /* StakingSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingSettingsTests.swift; sourceTree = ""; }; 84F13F1B26F2B8C1006725FF /* JSONRPCError+Presentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONRPCError+Presentable.swift"; sourceTree = ""; }; 84F18D4927A1869E00CA7554 /* OrmlAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrmlAccount.swift; sourceTree = ""; }; - 84F18D4B27A1874000CA7554 /* OrmlAccountSubcriptionHandlingFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrmlAccountSubcriptionHandlingFactory.swift; sourceTree = ""; }; + 84F18D4B27A1874000CA7554 /* TokenSubscriptionFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenSubscriptionFactory.swift; sourceTree = ""; }; 84F18D4D27A18C1400CA7554 /* OrmlAccountSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrmlAccountSubscription.swift; sourceTree = ""; }; 84F1A0702869DA51007DB053 /* AssetBalanceExistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetBalanceExistence.swift; sourceTree = ""; }; 84F1A072286A5DDA007DB053 /* CommonRetryable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonRetryable.swift; sourceTree = ""; }; @@ -4691,6 +4712,7 @@ 86F7A369E31DCB9ABD556EE9 /* CrowdloanListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanListPresenter.swift; sourceTree = ""; }; 86F9063B2DF46E7B65B5248E /* Pods_novawalletAll_novawalletIntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_novawalletAll_novawalletIntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 87CD8C618D61C78EA8C58532 /* ParitySignerTxScanViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxScanViewLayout.swift; sourceTree = ""; }; + 880855EC28D062A9004255E7 /* Array+AddOrReplace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+AddOrReplace.swift"; sourceTree = ""; }; 8821119C96944A0E3526E93A /* StakingRedeemViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRedeemViewFactory.swift; sourceTree = ""; }; 8828C05728B4A67000555CB6 /* Prism.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Prism.swift; sourceTree = ""; }; 8828C05928B4A6A800555CB6 /* Samples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Samples.swift; sourceTree = ""; }; @@ -4699,10 +4721,12 @@ 882A5CEC28AFCE3600D0D798 /* ReturnInIntervalsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReturnInIntervalsViewModel.swift; sourceTree = ""; }; 882A5CEE28AFCE6000D0D798 /* FormattedReturnInIntervalsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormattedReturnInIntervalsViewModel.swift; sourceTree = ""; }; 882AA12F28AE64DC0093BC63 /* CrowdloanYourContributionsTotalCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanYourContributionsTotalCell.swift; sourceTree = ""; }; + 8831F0FF28C65B95009F7682 /* AssetLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetLock.swift; sourceTree = ""; }; 8836AF4328AA293500A94EDD /* CurrencyManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyManagerTests.swift; sourceTree = ""; }; 8836AF4728AA49AB00A94EDD /* Currency+btc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Currency+btc.swift"; sourceTree = ""; }; 8836AF4928AA4B9300A94EDD /* CurrencyRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyRepositoryTests.swift; sourceTree = ""; }; 8836AF4B28AA4E4800A94EDD /* currencies.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = currencies.json; sourceTree = ""; }; + 884048D328C723F00085FFA6 /* OrmlTokenSubscriptionHandlingFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrmlTokenSubscriptionHandlingFactory.swift; sourceTree = ""; }; 8842104B289BBA6400306F2C /* currencies.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = currencies.json; sourceTree = ""; }; 8842104E289BBA8D00306F2C /* CurrencyViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyViewLayout.swift; sourceTree = ""; }; 8842104F289BBA8D00306F2C /* CurrencyPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyPresenter.swift; sourceTree = ""; }; @@ -4736,7 +4760,18 @@ 88A5317A28B9149600AF18F5 /* UIImage+DrawableIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+DrawableIcon.swift"; sourceTree = ""; }; 88A5317C28B9170100AF18F5 /* NSCollectionLayoutSection+create.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSCollectionLayoutSection+create.swift"; sourceTree = ""; }; 88A5317F28B9328E00AF18F5 /* YourWalletsViewSectionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourWalletsViewSectionModel.swift; sourceTree = ""; }; + 88A6BCFE28CA15400047E4C2 /* LocksBalanceViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocksBalanceViewModelFactory.swift; sourceTree = ""; }; + 88A6BD0028CA15710047E4C2 /* LocksViewInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocksViewInput.swift; sourceTree = ""; }; 88AA0FB728B60E6A00931800 /* YourWalletsControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourWalletsControlView.swift; sourceTree = ""; }; + 88AC186028CA3EE100892A9B /* LocksViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocksViewLayout.swift; sourceTree = ""; }; + 88AC186228CA3F0000892A9B /* GenericCollectionViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericCollectionViewLayout.swift; sourceTree = ""; }; + 88AC186428CA461F00892A9B /* ModalSheetCollectionViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalSheetCollectionViewProtocol.swift; sourceTree = ""; }; + 88AF35DD28C21D28003730DA /* LocksSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocksSubscription.swift; sourceTree = ""; }; + 88C017E528C60A65003B2D28 /* AssetLockMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetLockMapper.swift; sourceTree = ""; }; + 88C7165328C894510015D1E9 /* CollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewDelegate.swift; sourceTree = ""; }; + 88C7165528C8CD050015D1E9 /* UICollectionViewDiffableDataSource+apply.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionViewDiffableDataSource+apply.swift"; sourceTree = ""; }; + 88C7165728C8D3270015D1E9 /* LockCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockCollectionViewCell.swift; sourceTree = ""; }; + 88C7165928C8D3450015D1E9 /* LocksHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocksHeaderView.swift; sourceTree = ""; }; 88D997AD28AB86FD006135A5 /* YourContributionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YourContributionsView.swift; sourceTree = ""; }; 88D997AF28ABC8C0006135A5 /* BlurredTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurredTableViewCell.swift; sourceTree = ""; }; 88D997B128ABC90E006135A5 /* AboutCrowdloansView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutCrowdloansView.swift; sourceTree = ""; }; @@ -5044,6 +5079,7 @@ E20124142C4011901EF55AAA /* ParitySignerAddressesViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerAddressesViewLayout.swift; sourceTree = ""; }; E29DAC8F2DB0F7BF909812FA /* DAppTxDetailsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppTxDetailsViewLayout.swift; sourceTree = ""; }; E2F3E725280823CF00CF31B5 /* ETHAccountInjection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ETHAccountInjection.swift; sourceTree = ""; }; + E30E541992BF608923DABE5F /* LocksWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocksWireframe.swift; sourceTree = ""; }; E4C77FD258A19F08F3955AC4 /* ParaStkUnstakeConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkUnstakeConfirmInteractor.swift; sourceTree = ""; }; E4E78D69E8EBC3EB4D01F8EF /* CrowdloanListInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanListInteractor.swift; sourceTree = ""; }; E54289A8A9354D5DDA15F0E1 /* ChangeWatchOnlyViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChangeWatchOnlyViewFactory.swift; sourceTree = ""; }; @@ -5079,6 +5115,7 @@ F23EDFB699CAEEADC9263A0D /* DAppAuthSettingsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAuthSettingsViewFactory.swift; sourceTree = ""; }; F28EDDF9277242505FDDECA1 /* CustomValidatorListProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CustomValidatorListProtocols.swift; sourceTree = ""; }; F2B676982F60C55530BDD569 /* AccountManagementPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountManagementPresenter.swift; sourceTree = ""; }; + F3D4D2E89D40718677685CE1 /* LocksViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocksViewController.swift; sourceTree = ""; }; F400A7C1260CE1670061D576 /* StakingRewardStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardStatus.swift; sourceTree = ""; }; F402BC82273ACDC30075F803 /* AstarBonusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AstarBonusService.swift; sourceTree = ""; }; F402BC8A273AD20D0075F803 /* AstarBonusServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AstarBonusServiceError.swift; sourceTree = ""; }; @@ -5242,6 +5279,7 @@ F4F65C3726D8B86F002EE838 /* FWXAxisEmptyValueFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FWXAxisEmptyValueFormatter.swift; sourceTree = ""; }; F4F65C3C26D8B9DD002EE838 /* FWYAxisChartFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FWYAxisChartFormatter.swift; sourceTree = ""; }; F4F69E272731B0B200214542 /* CrowdloanTableHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrowdloanTableHeaderView.swift; sourceTree = ""; }; + F4F9944B0577EFF25A0643FE /* LocksProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocksProtocols.swift; sourceTree = ""; }; F4FDA0F726A57626003D753B /* BabeEraOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BabeEraOperationFactory.swift; sourceTree = ""; }; F4FDA0FC26A57860003D753B /* EraCountdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraCountdown.swift; sourceTree = ""; }; F52B8815D6AF5E69B145D245 /* CustomValidatorListViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CustomValidatorListViewFactory.swift; sourceTree = ""; }; @@ -5304,6 +5342,9 @@ 84ED3A7899C88876AB3DCA5F /* YourWalletsViewController.swift */, 6E2509FEA85677165C4CCCFF /* YourWalletsViewLayout.swift */, C9978451AB2F4958E6FF117D /* YourWalletsViewFactory.swift */, + 88C7165328C894510015D1E9 /* CollectionViewDelegate.swift */, + 88AC186228CA3F0000892A9B /* GenericCollectionViewLayout.swift */, + 88AC186428CA461F00892A9B /* ModalSheetCollectionViewProtocol.swift */, ); path = YourWallets; sourceTree = ""; @@ -6028,6 +6069,22 @@ path = SelectValidatorsStart; sourceTree = ""; }; + 83C426015DF863EBA46F1E3E /* Locks */ = { + isa = PBXGroup; + children = ( + 88C7165B28C8D37E0015D1E9 /* View */, + F4F9944B0577EFF25A0643FE /* LocksProtocols.swift */, + E30E541992BF608923DABE5F /* LocksWireframe.swift */, + 26F9E9C70B0C14570AB783AF /* LocksPresenter.swift */, + F3D4D2E89D40718677685CE1 /* LocksViewController.swift */, + 7B81B239BD9C150BFE9A82B0 /* LocksViewFactory.swift */, + 88A6BCFE28CA15400047E4C2 /* LocksBalanceViewModelFactory.swift */, + 88A6BD0028CA15710047E4C2 /* LocksViewInput.swift */, + 88AC186028CA3EE100892A9B /* LocksViewLayout.swift */, + ); + path = Locks; + sourceTree = ""; + }; 8401620F25E144DA0087A5F3 /* AmountInputView */ = { isa = PBXGroup; children = ( @@ -6203,13 +6260,14 @@ 841E2E4D2738159400F250C1 /* RemoteSubscriptionHandlingFactory.swift */, 841E2E4F27381B2A00F250C1 /* AccountInfoSubscriptionHandlingFactory.swift */, 84B73AD9279D265A0071AE16 /* AssetsSubscriptionHandlingFactory.swift */, - 84F18D4B27A1874000CA7554 /* OrmlAccountSubcriptionHandlingFactory.swift */, + 84F18D4B27A1874000CA7554 /* TokenSubscriptionFactory.swift */, 84D1ABD927E0ACFA0073C631 /* WalletRemoteSubscriptionWrapper.swift */, 848DAEF6282274E700D56F55 /* ParachainStakingRemoteSubscriptionService.swift */, 841E553B282D44BA00C8438F /* ParachainStakingAccountSubscriptionService.swift */, 849E07F12849E70C00DE0440 /* ParaStkScheduledRequestsUpdater.swift */, 849E07F3284A04F400DE0440 /* ParaStkAccountSubscribeHandlingFactory.swift */, 842643BA2878572D0031B5B5 /* TuringStakingRemoteSubscriptionService.swift */, + 884048D328C723F00085FFA6 /* OrmlTokenSubscriptionHandlingFactory.swift */, ); path = RemoteSubscription; sourceTree = ""; @@ -6845,6 +6903,7 @@ 84F1CB3F27CF6BEF0095D523 /* UniquesClassDetails.swift */, 8430D6C42800040A00FFB6AE /* EthereumExecuted.swift */, 84A3B8A12836DA2600DE2669 /* LastAccountIdKeyWrapper.swift */, + 8831F0FF28C65B95009F7682 /* AssetLock.swift */, ); path = Types; sourceTree = ""; @@ -7237,6 +7296,7 @@ 84CA68DE26BEAA0F003B9453 /* ChainModelMapper.swift */, 845B822026EF8F1A00D25C72 /* ManagedMetaAccountMapper.swift */, 849A4EF9279ABC8800AB6709 /* AssetBalanceMapper.swift */, + 88C017E528C60A65003B2D28 /* AssetLockMapper.swift */, 8499FECB27BF8F4A00712589 /* NftModelMapper.swift */, 84F3B27727F4179A00D64CF5 /* PhishingSiteMapper.swift */, 849E07F5284A114B00DE0440 /* ParaStkScheduledRequestsMapper.swift */, @@ -7721,6 +7781,7 @@ 84F18D4D27A18C1400CA7554 /* OrmlAccountSubscription.swift */, 84C3420C283192D000156569 /* CallbackStorageSubscription.swift */, 84FA7C1D284FED0A00B648E1 /* CallbackBatchStorageSubscription.swift */, + 88AF35DD28C21D28003730DA /* LocksSubscription.swift */, ); path = StorageSubscription; sourceTree = ""; @@ -8213,6 +8274,7 @@ A29C55960FE9EADBDEAC6F03 /* AssetsSearch */, 486ADD5F84F3B18E6F5BC0DA /* WalletsList */, 018DE0E8A60963E2BDD94D13 /* YourWallets */, + 83C426015DF863EBA46F1E3E /* Locks */, ); path = Modules; sourceTree = ""; @@ -8482,6 +8544,7 @@ 88E1E897289C024400C123A8 /* UIView+Create.swift */, 88A5317A28B9149600AF18F5 /* UIImage+DrawableIcon.swift */, 88A5317C28B9170100AF18F5 /* NSCollectionLayoutSection+create.swift */, + 88C7165528C8CD050015D1E9 /* UICollectionViewDiffableDataSource+apply.swift */, ); path = UIKit; sourceTree = ""; @@ -8555,6 +8618,7 @@ 840DC83F288090030039A054 /* Bool+Int.swift */, 844DAAE028AD106B008E11DA /* UInt+Serialization.swift */, 84D9C8F228ADA42F007FB23B /* Data+Chunk.swift */, + 880855EC28D062A9004255E7 /* Array+AddOrReplace.swift */, ); path = Foundation; sourceTree = ""; @@ -11209,6 +11273,15 @@ path = CrowdloanList; sourceTree = ""; }; + 88C7165B28C8D37E0015D1E9 /* View */ = { + isa = PBXGroup; + children = ( + 88C7165728C8D3270015D1E9 /* LockCollectionViewCell.swift */, + 88C7165928C8D3450015D1E9 /* LocksHeaderView.swift */, + ); + path = View; + sourceTree = ""; + }; 88E1E894289C020D00C123A8 /* View */ = { isa = PBXGroup; children = ( @@ -13425,6 +13498,7 @@ 84D17ED628053D6D00F7BAFF /* DAppFavorite.swift in Sources */, 84EE2FB1289128E400A98816 /* WalletManageProtocols.swift in Sources */, 84364D55252FAD7100281F9A /* AssetDetailsConfigurator.swift in Sources */, + 88AC186528CA461F00892A9B /* ModalSheetCollectionViewProtocol.swift in Sources */, 8460E711284AB99E002896E9 /* ParaStkHintsViewModelFactory.swift in Sources */, 2A66CFAF25D10EDF0006E4C1 /* PhishingItem.swift in Sources */, 8487583E27F070B300495306 /* ApplicationSettingsPresentable.swift in Sources */, @@ -13743,6 +13817,7 @@ 842A736B27DB7A2E006EE1EA /* OperationDetailsViewModel.swift in Sources */, 8425EA9A25EA83FA00C307C9 /* ChainData+Value.swift in Sources */, 8887813E28B7AA3100E7290F /* RoundedIconTitleCollectionHeaderView.swift in Sources */, + 88C017E628C60A65003B2D28 /* AssetLockMapper.swift in Sources */, AEA2C1BA2681E9C50069492E /* ValidatorSearchInteractor.swift in Sources */, 84585A2F251BFC8400390F7A /* TriangularedButton+Style.swift in Sources */, 84C6801A24D75E2A00006BF5 /* BorderedSubtitleActionView+Inspectable.swift in Sources */, @@ -13797,6 +13872,7 @@ 8430AADC26022C58005B1066 /* NoStashState.swift in Sources */, 84F4A9182550331D000CF0A3 /* SecretSource.swift in Sources */, 84FD3DB12540C09800A234E3 /* TransactionHistoryMergeManager.swift in Sources */, + 88C7165A28C8D3450015D1E9 /* LocksHeaderView.swift in Sources */, 840B3D6E289A56BA00DA1DA9 /* ParitySignerScanWireframe.swift in Sources */, F48EB5462722BB7000AE15ED /* AcalaBonusService.swift in Sources */, 84466B4028B77B4500FA1E0D /* SignatureVerificationWrapper.swift in Sources */, @@ -13840,6 +13916,7 @@ AEA0C8C12681180900F9666F /* InitBondingCustomValidatorListWireframe.swift in Sources */, 849976C127B2823F00B14A6C /* DAppMetamaskBaseState.swift in Sources */, 842876A624AE049B00D91AD8 /* SelectableViewModelProtocol.swift in Sources */, + 8831F10028C65B95009F7682 /* AssetLock.swift in Sources */, 842B1806286506EE0014CC57 /* CrossChainTransferSetupProtocols.swift in Sources */, 84D1F2FB260F51770077DDFE /* SwitchAccount.swift in Sources */, 84466B3C28B7680300FA1E0D /* LedgerDiscoverInteractor.swift in Sources */, @@ -14250,6 +14327,7 @@ 8459A9CC2746A1E9000D6278 /* CrowdloanOffchainSubscriptionHandler.swift in Sources */, 84D331AF2519E8080078D044 /* TriangularedView+Style.swift in Sources */, 84C74365251E4D60009576C6 /* SigningWrapperProtocol.swift in Sources */, + 88AF35DE28C21D28003730DA /* LocksSubscription.swift in Sources */, 844CB56A26F9C57D00396E13 /* WalletLocalStorageSubscriber.swift in Sources */, 842A738A27DE14B3006EE1EA /* TransactionLocalStorageHandler.swift in Sources */, 8454C2652632B0EF00657DAD /* EventCodingPath.swift in Sources */, @@ -14355,7 +14433,7 @@ 8472C5B0265CF9C500E2481B /* StakingRewardDestConfirmWireframe.swift in Sources */, 84FB1F792527065A00E0242B /* HistoryConstants.swift in Sources */, 848F8B292864503A00204BC4 /* TransferSetupWireframe.swift in Sources */, - 84F18D4C27A1874000CA7554 /* OrmlAccountSubcriptionHandlingFactory.swift in Sources */, + 84F18D4C27A1874000CA7554 /* TokenSubscriptionFactory.swift in Sources */, 4448B591D4A193DBC9E2E3BF /* AccountCreateInteractor.swift in Sources */, 84DF21A92535AA8F005454AE /* ExistentialDepositInfoCommand.swift in Sources */, 84E6D57C262E2CE8000EA3F5 /* OperationCombiningService.swift in Sources */, @@ -14487,6 +14565,7 @@ 849A4EF6279A7AEF00AB6709 /* StateminAssetExtras.swift in Sources */, 849E17E627914394002D1744 /* NavigationBarSettings.swift in Sources */, 849E17F02791909C002D1744 /* DAppSettings.swift in Sources */, + 884048D428C723F00085FFA6 /* OrmlTokenSubscriptionHandlingFactory.swift in Sources */, 846A2C4D2529FBB700731018 /* NSPredicate+Filter.swift in Sources */, 84282292289BB45700163031 /* ParitySignerWallet.swift in Sources */, 84D17EDA28054C7500F7BAFF /* DAppLocalStorageSubscriber.swift in Sources */, @@ -14561,6 +14640,7 @@ 848B59C228BCC1E60009543C /* LedgerAddAccountConfirmationInteractor.swift in Sources */, 843461CF26E25AD400DCE0CD /* SubscanHistoryItem+Wallet.swift in Sources */, 882A5CED28AFCE3600D0D798 /* ReturnInIntervalsViewModel.swift in Sources */, + 88C7165428C894510015D1E9 /* CollectionViewDelegate.swift in Sources */, F458D3982642911B0055CB75 /* ControllerAccountViewModel.swift in Sources */, 84DAC198268D3DD9002D0DF4 /* SNAddressType.swift in Sources */, 84AC0B6A28C0D8CE00FA5B5D /* NoLedgerSupportCommand.swift in Sources */, @@ -14761,6 +14841,7 @@ 84B5DE59283F8B5400193ED3 /* CollatorSelectionViewModel.swift in Sources */, 84E4932727325D4E000534F2 /* AssetListViewModelFactory.swift in Sources */, 8401AEC02642A71D000B03E3 /* StakingRebondConfirmationViewModelFactory.swift in Sources */, + 880855ED28D062A9004255E7 /* Array+AddOrReplace.swift in Sources */, 8472C5AD265CF9C500E2481B /* StakingRewardDestConfirmViewModelFactory.swift in Sources */, 8422F2F328881C9B00C7B840 /* WatchOnlyWallet.swift in Sources */, 8446F5F228172BBC00B7A86C /* StakingUnbonHintView.swift in Sources */, @@ -14888,6 +14969,7 @@ 6D47EAB127FAB7559A9FA107 /* StakingPayoutConfirmationViewController.swift in Sources */, F4EF24C826BA713300F28B4E /* AnalyticsStakeHeaderView.swift in Sources */, 849ABE772628103200011A2A /* ControllersListReducer.swift in Sources */, + 88C7165628C8CD050015D1E9 /* UICollectionViewDiffableDataSource+apply.swift in Sources */, 840DFF532894189D001B11EA /* ChainAddressDetailsMeasurement.swift in Sources */, 9565BEB636E6D386B0C0FBE5 /* StakingPayoutConfirmationViewFactory.swift in Sources */, 6F0CFDAB9D0C35075BD74A77 /* WalletHistoryFilterProtocols.swift in Sources */, @@ -14913,6 +14995,7 @@ F4223ED127329767003D8E4E /* AcalaTransferRequest.swift in Sources */, 8473F4B8282BFFF8007CC55A /* StakingRelaychainInteractor+Subscription.swift in Sources */, 841E2E5027381B2A00F250C1 /* AccountInfoSubscriptionHandlingFactory.swift in Sources */, + 88AC186128CA3EE100892A9B /* LocksViewLayout.swift in Sources */, A32E1373E3671D518FFC3BC2 /* YourValidatorListViewController.swift in Sources */, 84D2F1A927744C280040C680 /* PolkadotExtensionError.swift in Sources */, 37E1E9782B9752BC50AF2476 /* YourValidatorListViewFactory.swift in Sources */, @@ -14958,8 +15041,10 @@ AEE5FB1C264A610C002B8FDC /* StakingRewardDestSetupLayout.swift in Sources */, 84350ADB28461E5B0031EF24 /* ParaStkYourCollatorsViewModelFactory.swift in Sources */, B7CF31A548C02AD7AAC16A8D /* StakingRedeemWireframe.swift in Sources */, + 88C7165828C8D3280015D1E9 /* LockCollectionViewCell.swift in Sources */, C0B0DDF638915E8259B1CD67 /* StakingRedeemPresenter.swift in Sources */, C4A4D40A08DAB4A71C21C1A8 /* StakingRedeemInteractor.swift in Sources */, + 88A6BD0128CA15710047E4C2 /* LocksViewInput.swift in Sources */, F409672B26B29C3B008CD244 /* RewardAnalyticsWidgetView.swift in Sources */, B1CCC5B7BF30F6ACA309B112 /* StakingRedeemViewController.swift in Sources */, C21129B2B8D8B33BCBD5843E /* StakingRedeemViewFactory.swift in Sources */, @@ -15045,6 +15130,7 @@ 8499FED827BFCABD00712589 /* NftModel+Identifier.swift in Sources */, 88421064289BBD9100306F2C /* Currency.swift in Sources */, 921E4891E85C0DC6FDD8A0D0 /* CrowdloanContributionConfirmInteractor.swift in Sources */, + 88A6BCFF28CA15400047E4C2 /* LocksBalanceViewModelFactory.swift in Sources */, 5C796EF8ED29F564B5D1126B /* CrowdloanContributionConfirmViewController.swift in Sources */, 2793D406FD618A892D54EA84 /* CrowdloanContributionConfirmViewLayout.swift in Sources */, 840D92A3278EDB2E0007B979 /* DAppParsedCall.swift in Sources */, @@ -15193,6 +15279,7 @@ 9BADFCBF3AF5186094DB8D67 /* DAppTxDetailsInteractor.swift in Sources */, B409644ED1E20062A3EA0316 /* DAppTxDetailsViewController.swift in Sources */, DAD46B2B29A446C19A6ABF2D /* DAppTxDetailsViewLayout.swift in Sources */, + 88AC186328CA3F0000892A9B /* GenericCollectionViewLayout.swift in Sources */, A97F32D057BFEFBCC478A09C /* DAppTxDetailsViewFactory.swift in Sources */, D567BAAF620EDB9F4975C800 /* DAppAuthConfirmProtocols.swift in Sources */, 84C342092831645800156569 /* EraCountdownDisplay.swift in Sources */, @@ -15481,6 +15568,11 @@ B317AB093D99677D292121C4 /* YourWalletsViewController.swift in Sources */, E8C54C2441B78248B6067204 /* YourWalletsViewLayout.swift in Sources */, 964DE461970AF6B89D0968C5 /* YourWalletsViewFactory.swift in Sources */, + 9E40464B7687006B1EE75C72 /* LocksProtocols.swift in Sources */, + 30413A3C5ADB96B7D663F94D /* LocksWireframe.swift in Sources */, + BE301A0F2286CCEF6A02D341 /* LocksPresenter.swift in Sources */, + CE4C1344F03A5132C601A594 /* LocksViewController.swift in Sources */, + 8A23DD1F4146639EA2F7AEF6 /* LocksViewFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/novawallet/Assets.xcassets/iconTransferable.imageset/Contents.json b/novawallet/Assets.xcassets/iconTransferable.imageset/Contents.json new file mode 100644 index 0000000000..bfbeb098c2 --- /dev/null +++ b/novawallet/Assets.xcassets/iconTransferable.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconTransferable.imageset/Icon.pdf b/novawallet/Assets.xcassets/iconTransferable.imageset/Icon.pdf new file mode 100644 index 0000000000000000000000000000000000000000..59c3f7ff79b84f36d6fc88aa7712f2b1d7e224f5 GIT binary patch literal 3119 zcmds3%Wm8@6y5VHxG|6vh=vqNkpcn(@e@TG#GUCPK+uIeR_sZ~k}JuD)35Kj6h%p+ zDVlUyEo{yq@B5JVp4?nqy^)D!Ome~N+b@jsw{Q9C*RM|{Uw!;=`=#7=!0@epF01?Q zfu}gQ4$yT|KR-?A^&3<{vhA9(cx0=K^3UD2TwkB@^Ud_vwq<{?s1X?n zy5l15*+eU;tl*=K?IVllmWy^EjD869fPO#%ji?s> z&QkD!Z2sEN&8FwU6Os^5KSDfhB{_&!!k6xVe!g^(eyptS!Yhdtbpl2&ecuwK5|$2l z7}Wc=PJv4NPdi1tpz&G~-}`p7+>2Ydsd2qrP@{haHx ze|3(Ooyyji9sM9y9rt%q^)MOu*8fp*8XneThrWF2t~XuLm45$H`hT1EMfY$zM<2O8}p zQ(C2>PE5b6s=90WCz^rAkZy|mG63(( zyIt|U(-dhM2Q#9@Fb$s9RaaK9NEsNw8yfmLo(E`>CxZG2XyW{7cL!HFMX=_B$LIAk zWaYyD7RW@C#MClTMG>~q;u!gP(G~mpelGjcl%E-TAwiI|cA literal 0 HcmV?d00001 diff --git a/novawallet/Common/DataProvider/Subscription/WalletLocalStorageSubscriber.swift b/novawallet/Common/DataProvider/Subscription/WalletLocalStorageSubscriber.swift index d49c371aba..3fda9d5721 100644 --- a/novawallet/Common/DataProvider/Subscription/WalletLocalStorageSubscriber.swift +++ b/novawallet/Common/DataProvider/Subscription/WalletLocalStorageSubscriber.swift @@ -22,6 +22,10 @@ protocol WalletLocalStorageSubscriber where Self: AnyObject { ) -> StreamableProvider? func subscribeAllBalancesProvider() -> StreamableProvider? + + func subscribeToAllLocksProvider( + for accountId: AccountId + ) -> StreamableProvider? } extension WalletLocalStorageSubscriber { @@ -201,6 +205,44 @@ extension WalletLocalStorageSubscriber { return provider } + + func subscribeToAllLocksProvider( + for accountId: AccountId + ) -> StreamableProvider? { + guard let locksProvider = try? walletLocalSubscriptionFactory.getLocksProvider(for: accountId) else { + return nil + } + + let updateClosure = { [weak self] (changes: [DataProviderChange]) in + self?.walletLocalSubscriptionHandler.handleAccountLocks(result: .success(changes), accountId: accountId) + return + } + + let failureClosure = { [weak self] (error: Error) in + self?.walletLocalSubscriptionHandler.handleAccountLocks( + result: .failure(error), + accountId: accountId + ) + return + } + + let options = StreamableProviderObserverOptions( + alwaysNotifyOnRefresh: false, + waitsInProgressSyncOnAdd: false, + initialSize: 0, + refreshWhenEmpty: false + ) + + locksProvider.addObserver( + self, + deliverOn: .main, + executing: updateClosure, + failing: failureClosure, + options: options + ) + + return locksProvider + } } extension WalletLocalStorageSubscriber where Self: WalletLocalSubscriptionHandler { diff --git a/novawallet/Common/DataProvider/Subscription/WalletLocalSubscriptionHandler.swift b/novawallet/Common/DataProvider/Subscription/WalletLocalSubscriptionHandler.swift index 3abf747bb3..64de1966bf 100644 --- a/novawallet/Common/DataProvider/Subscription/WalletLocalSubscriptionHandler.swift +++ b/novawallet/Common/DataProvider/Subscription/WalletLocalSubscriptionHandler.swift @@ -21,6 +21,11 @@ protocol WalletLocalSubscriptionHandler { ) func handleAllBalances(result: Result<[DataProviderChange], Error>) + + func handleAccountLocks( + result: Result<[DataProviderChange], Error>, + accountId: AccountId + ) } extension WalletLocalSubscriptionHandler { @@ -43,4 +48,9 @@ extension WalletLocalSubscriptionHandler { ) {} func handleAllBalances(result _: Result<[DataProviderChange], Error>) {} + + func handleAccountLocks( + result _: Result<[DataProviderChange], Error>, + accountId _: AccountId + ) {} } diff --git a/novawallet/Common/DataProvider/WalletLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/WalletLocalSubscriptionFactory.swift index e6372d18d5..4b1c5f8294 100644 --- a/novawallet/Common/DataProvider/WalletLocalSubscriptionFactory.swift +++ b/novawallet/Common/DataProvider/WalletLocalSubscriptionFactory.swift @@ -16,6 +16,8 @@ protocol WalletLocalSubscriptionFactoryProtocol { func getAccountBalanceProvider(for accountId: AccountId) throws -> StreamableProvider func getAllBalancesProvider() throws -> StreamableProvider + + func getLocksProvider(for accountId: AccountId) throws -> StreamableProvider } final class WalletLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, @@ -172,4 +174,48 @@ final class WalletLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, return provider } + + func getLocksProvider(for accountId: AccountId) throws -> StreamableProvider { + let cacheKey = "locks-\(accountId.toHex())" + + if let provider = getProvider(for: cacheKey) as? StreamableProvider { + return provider + } + + let source = EmptyStreamableSource() + + let mapper = AssetLockMapper() + let filter = NSPredicate.assetLock(for: accountId) + + let repository = storageFacade.createRepository( + filter: filter, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + + let observable = CoreDataContextObservable( + service: storageFacade.databaseService, + mapper: AnyCoreDataMapper(mapper), + predicate: { entity in + accountId.toHex() == entity.chainAccountId + } + ) + + observable.start { [weak self] error in + if let error = error { + self?.logger.error("Did receive error: \(error)") + } + } + + let provider = StreamableProvider( + source: AnyStreamableSource(source), + repository: AnyDataProviderRepository(repository), + observable: AnyDataProviderRepositoryObservable(observable), + operationManager: operationManager + ) + + saveProvider(provider, for: cacheKey) + + return provider + } } diff --git a/novawallet/Common/Extension/Foundation/Array+AddOrReplace.swift b/novawallet/Common/Extension/Foundation/Array+AddOrReplace.swift new file mode 100644 index 0000000000..ed6375d7b6 --- /dev/null +++ b/novawallet/Common/Extension/Foundation/Array+AddOrReplace.swift @@ -0,0 +1,11 @@ +import RobinHood + +extension Array where Element: Identifiable { + mutating func addOrReplaceSingle(_ element: Element) { + if let index = firstIndex(where: { $0.identifier == element.identifier }) { + self[index] = element + } else { + append(element) + } + } +} diff --git a/novawallet/Common/Extension/Foundation/NSPredicate+Filter.swift b/novawallet/Common/Extension/Foundation/NSPredicate+Filter.swift index 3633470af4..c153dd2bbb 100644 --- a/novawallet/Common/Extension/Foundation/NSPredicate+Filter.swift +++ b/novawallet/Common/Extension/Foundation/NSPredicate+Filter.swift @@ -204,6 +204,39 @@ extension NSPredicate { ) } + static func assetLock( + for accountId: AccountId + ) -> NSPredicate { + NSPredicate( + format: "%K == %@", + #keyPath(CDAssetLock.chainAccountId), + accountId.toHex() + ) + } + + static func assetLock( + for accountId: AccountId, + chainAssetId: ChainAssetId + ) -> NSPredicate { + let accountPredicate = assetLock(for: accountId) + + let chainIdPredicate = NSPredicate( + format: "%K == %@", + #keyPath(CDAssetLock.chainId), + chainAssetId.chainId + ) + + let assetIdPredicate = NSPredicate( + format: "%K == %d", + #keyPath(CDAssetLock.assetId), + chainAssetId.assetId + ) + + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + accountPredicate, chainIdPredicate, assetIdPredicate + ]) + } + static func nfts(for chainId: ChainModel.Id, ownerId: AccountId) -> NSPredicate { let chainPredicate = NSPredicate(format: "%K == %@", #keyPath(CDNft.chainId), chainId) let ownerPredicate = NSPredicate(format: "%K == %@", #keyPath(CDNft.ownerId), ownerId.toHex()) diff --git a/novawallet/Common/Extension/UIKit/UICollectionViewDiffableDataSource+apply.swift b/novawallet/Common/Extension/UIKit/UICollectionViewDiffableDataSource+apply.swift new file mode 100644 index 0000000000..266262718e --- /dev/null +++ b/novawallet/Common/Extension/UIKit/UICollectionViewDiffableDataSource+apply.swift @@ -0,0 +1,13 @@ +import UIKit + +extension UICollectionViewDiffableDataSource where SectionIdentifierType: SectionProtocol { + func apply(_ viewModel: [SectionIdentifierType]) where SectionIdentifierType.CellModel == ItemIdentifierType { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections(viewModel) + viewModel.forEach { section in + snapshot.appendItems(section.cells, toSection: section) + } + + apply(snapshot) + } +} diff --git a/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift b/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift index 0cae4c5749..f64449a2ea 100644 --- a/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift +++ b/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift @@ -13,6 +13,11 @@ protocol SubstrateRepositoryFactoryProtocol { func createTxRepository() -> AnyDataProviderRepository func createPhishingRepository() -> AnyDataProviderRepository + func createAssetLocksRepository( + for accountId: AccountId, + chainAssetId: ChainAssetId + ) -> AnyDataProviderRepository + func createChainAddressTxRepository( for address: AccountAddress, chainId: ChainModel.Id @@ -181,4 +186,21 @@ final class SubstrateRepositoryFactory: SubstrateRepositoryFactoryProtocol { return AnyDataProviderRepository(repository) } + + func createAssetLocksRepository( + for accountId: AccountId, + chainAssetId: ChainAssetId + ) -> AnyDataProviderRepository { + createAssetLocksRepository(.assetLock(for: accountId, chainAssetId: chainAssetId)) + } + + private func createAssetLocksRepository(_ filter: NSPredicate) -> AnyDataProviderRepository { + let mapper = AssetLockMapper() + let repository = storageFacade.createRepository( + filter: filter, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + return AnyDataProviderRepository(repository) + } } diff --git a/novawallet/Common/Services/RemoteSubscription/AccountInfoSubscriptionHandlingFactory.swift b/novawallet/Common/Services/RemoteSubscription/AccountInfoSubscriptionHandlingFactory.swift index 9a911a52fb..4789a9f790 100644 --- a/novawallet/Common/Services/RemoteSubscription/AccountInfoSubscriptionHandlingFactory.swift +++ b/novawallet/Common/Services/RemoteSubscription/AccountInfoSubscriptionHandlingFactory.swift @@ -2,27 +2,18 @@ import Foundation import RobinHood final class AccountInfoSubscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol { - let chainAssetId: ChainAssetId - let accountId: AccountId - let chainRegistry: ChainRegistryProtocol - let assetRepository: AnyDataProviderRepository - let eventCenter: EventCenterProtocol - let transactionSubscription: TransactionSubscription? + let accountLocalStorageKey: String + let locksLocalStorageKey: String + let factory: NativeTokenSubscriptionFactoryProtocol init( - chainAssetId: ChainAssetId, - accountId: AccountId, - chainRegistry: ChainRegistryProtocol, - assetRepository: AnyDataProviderRepository, - transactionSubscription: TransactionSubscription?, - eventCenter: EventCenterProtocol + accountLocalStorageKey: String, + locksLocalStorageKey: String, + factory: NativeTokenSubscriptionFactoryProtocol ) { - self.chainAssetId = chainAssetId - self.accountId = accountId - self.chainRegistry = chainRegistry - self.assetRepository = assetRepository - self.transactionSubscription = transactionSubscription - self.eventCenter = eventCenter + self.accountLocalStorageKey = accountLocalStorageKey + self.locksLocalStorageKey = locksLocalStorageKey + self.factory = factory } func createHandler( @@ -32,18 +23,20 @@ final class AccountInfoSubscriptionHandlingFactory: RemoteSubscriptionHandlingFa operationManager: OperationManagerProtocol, logger: LoggerProtocol ) -> StorageChildSubscribing { - AccountInfoSubscription( - chainAssetId: chainAssetId, - accountId: accountId, - chainRegistry: chainRegistry, - assetRepository: assetRepository, - transactionSubscription: transactionSubscription, - remoteStorageKey: remoteStorageKey, - localStorageKey: localStorageKey, - storage: storage, - operationManager: operationManager, - logger: logger, - eventCenter: eventCenter - ) + if locksLocalStorageKey == localStorageKey { + return factory.createBalanceLocksSubscription( + remoteStorageKey: remoteStorageKey, + operationManager: operationManager, + logger: logger + ) + } else { + return factory.createAccountInfoSubscription( + remoteStorageKey: remoteStorageKey, + localStorageKey: localStorageKey, + storage: storage, + operationManager: operationManager, + logger: logger + ) + } } } diff --git a/novawallet/Common/Services/RemoteSubscription/AccountInfoUpdatingService.swift b/novawallet/Common/Services/RemoteSubscription/AccountInfoUpdatingService.swift index 2ebb980bcd..5a80517701 100644 --- a/novawallet/Common/Services/RemoteSubscription/AccountInfoUpdatingService.swift +++ b/novawallet/Common/Services/RemoteSubscription/AccountInfoUpdatingService.swift @@ -114,16 +114,22 @@ final class AccountInfoUpdatingService { logger: logger ) + let chainAssetId = ChainAssetId(chainId: chain.chainId, assetId: asset.assetId) let assetBalanceMapper = AssetBalanceMapper() let assetRepository = storageFacade.createRepository(mapper: AnyCoreDataMapper(assetBalanceMapper)) + let locksRepository = repositoryFactory.createAssetLocksRepository( + for: accountId, + chainAssetId: chainAssetId + ) - let subscriptionHandlingFactory = AccountInfoSubscriptionHandlingFactory( - chainAssetId: ChainAssetId(chainId: chain.chainId, assetId: asset.assetId), + let subscriptionHandlingFactory = TokenSubscriptionFactory( + chainAssetId: chainAssetId, accountId: accountId, chainRegistry: chainRegistry, assetRepository: AnyDataProviderRepository(assetRepository), - transactionSubscription: transactionSubscription, - eventCenter: eventCenter + locksRepository: AnyDataProviderRepository(locksRepository), + eventCenter: eventCenter, + transactionSubscription: transactionSubscription ) let maybeSubscriptionId = remoteSubscriptionService.attachToAccountInfo( diff --git a/novawallet/Common/Services/RemoteSubscription/AssetsUpdatingService.swift b/novawallet/Common/Services/RemoteSubscription/AssetsUpdatingService.swift index 2963ca11b4..897a086390 100644 --- a/novawallet/Common/Services/RemoteSubscription/AssetsUpdatingService.swift +++ b/novawallet/Common/Services/RemoteSubscription/AssetsUpdatingService.swift @@ -144,9 +144,11 @@ final class AssetsUpdatingService { let assetRepository = repositoryFactory.createAssetBalanceRepository() let chainItemRepository = repositoryFactory.createChainStorageItemRepository() + let chainAssetId = ChainAssetId(chainId: chainId, assetId: asset.assetId) + let locksRepository = repositoryFactory.createAssetLocksRepository(for: accountId, chainAssetId: chainAssetId) let assetBalanceUpdater = AssetsBalanceUpdater( - chainAssetId: ChainAssetId(chainId: chainId, assetId: asset.assetId), + chainAssetId: chainAssetId, accountId: accountId, chainRegistry: chainRegistry, assetRepository: assetRepository, @@ -184,12 +186,15 @@ final class AssetsUpdatingService { return nil } + let chainAssetId = ChainAssetId(chainId: chainId, assetId: asset.assetId) let assetsRepository = repositoryFactory.createAssetBalanceRepository() - let subscriptionHandlingFactory = OrmlAccountSubscriptionHandlingFactory( - chainAssetId: ChainAssetId(chainId: chainId, assetId: asset.assetId), + let locksRepository = repositoryFactory.createAssetLocksRepository(for: accountId, chainAssetId: chainAssetId) + let subscriptionHandlingFactory = TokenSubscriptionFactory( + chainAssetId: chainAssetId, accountId: accountId, chainRegistry: chainRegistry, assetRepository: assetsRepository, + locksRepository: locksRepository, eventCenter: eventCenter, transactionSubscription: transactionSubscription ) diff --git a/novawallet/Common/Services/RemoteSubscription/OrmlAccountSubcriptionHandlingFactory.swift b/novawallet/Common/Services/RemoteSubscription/OrmlAccountSubcriptionHandlingFactory.swift deleted file mode 100644 index ed2e2fcc2d..0000000000 --- a/novawallet/Common/Services/RemoteSubscription/OrmlAccountSubcriptionHandlingFactory.swift +++ /dev/null @@ -1,49 +0,0 @@ -import Foundation -import RobinHood - -final class OrmlAccountSubscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol { - let chainAssetId: ChainAssetId - let accountId: AccountId - let chainRegistry: ChainRegistryProtocol - let assetRepository: AnyDataProviderRepository - let eventCenter: EventCenterProtocol - let transactionSubscription: TransactionSubscription? - - init( - chainAssetId: ChainAssetId, - accountId: AccountId, - chainRegistry: ChainRegistryProtocol, - assetRepository: AnyDataProviderRepository, - eventCenter: EventCenterProtocol, - transactionSubscription: TransactionSubscription? - ) { - self.chainAssetId = chainAssetId - self.accountId = accountId - self.chainRegistry = chainRegistry - self.assetRepository = assetRepository - self.eventCenter = eventCenter - self.transactionSubscription = transactionSubscription - } - - func createHandler( - remoteStorageKey: Data, - localStorageKey: String, - storage: AnyDataProviderRepository, - operationManager: OperationManagerProtocol, - logger: LoggerProtocol - ) -> StorageChildSubscribing { - OrmlAccountSubscription( - chainAssetId: chainAssetId, - accountId: accountId, - chainRegistry: chainRegistry, - assetRepository: assetRepository, - remoteStorageKey: remoteStorageKey, - localStorageKey: localStorageKey, - storage: storage, - operationManager: operationManager, - logger: logger, - eventCenter: eventCenter, - transactionSubscription: transactionSubscription - ) - } -} diff --git a/novawallet/Common/Services/RemoteSubscription/OrmlTokenSubscriptionHandlingFactory.swift b/novawallet/Common/Services/RemoteSubscription/OrmlTokenSubscriptionHandlingFactory.swift new file mode 100644 index 0000000000..224307ef4d --- /dev/null +++ b/novawallet/Common/Services/RemoteSubscription/OrmlTokenSubscriptionHandlingFactory.swift @@ -0,0 +1,41 @@ +import RobinHood + +final class OrmlTokenSubscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol { + let accountLocalStorageKey: String + let locksLocalStorageKey: String + let factory: OrmlTokenSubscriptionFactoryProtocol + + init( + accountLocalStorageKey: String, + locksLocalStorageKey: String, + factory: OrmlTokenSubscriptionFactoryProtocol + ) { + self.accountLocalStorageKey = accountLocalStorageKey + self.locksLocalStorageKey = locksLocalStorageKey + self.factory = factory + } + + func createHandler( + remoteStorageKey: Data, + localStorageKey: String, + storage: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing { + if locksLocalStorageKey == localStorageKey { + return factory.createOrmLocksSubscription( + remoteStorageKey: remoteStorageKey, + operationManager: operationManager, + logger: logger + ) + } else { + return factory.createOrmlAccountSubscription( + remoteStorageKey: remoteStorageKey, + localStorageKey: localStorageKey, + storage: storage, + operationManager: operationManager, + logger: logger + ) + } + } +} diff --git a/novawallet/Common/Services/RemoteSubscription/TokenSubscriptionFactory.swift b/novawallet/Common/Services/RemoteSubscription/TokenSubscriptionFactory.swift new file mode 100644 index 0000000000..295fc61c0a --- /dev/null +++ b/novawallet/Common/Services/RemoteSubscription/TokenSubscriptionFactory.swift @@ -0,0 +1,145 @@ +import Foundation +import RobinHood +import SubstrateSdk + +protocol OrmlTokenSubscriptionFactoryProtocol { + func createOrmlAccountSubscription( + remoteStorageKey: Data, + localStorageKey: String, + storage: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing + + func createOrmLocksSubscription( + remoteStorageKey: Data, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing +} + +protocol NativeTokenSubscriptionFactoryProtocol { + func createAccountInfoSubscription( + remoteStorageKey: Data, + localStorageKey: String, + storage: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing + + func createBalanceLocksSubscription( + remoteStorageKey: Data, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing +} + +// MARK: - OrmlTokenSubscriptionFactoryProtocol + +final class TokenSubscriptionFactory: OrmlTokenSubscriptionFactoryProtocol { + let chainAssetId: ChainAssetId + let accountId: AccountId + let chainRegistry: ChainRegistryProtocol + let assetRepository: AnyDataProviderRepository + let eventCenter: EventCenterProtocol + let transactionSubscription: TransactionSubscription? + let locksRepository: AnyDataProviderRepository + + init( + chainAssetId: ChainAssetId, + accountId: AccountId, + chainRegistry: ChainRegistryProtocol, + assetRepository: AnyDataProviderRepository, + locksRepository: AnyDataProviderRepository, + eventCenter: EventCenterProtocol, + transactionSubscription: TransactionSubscription? + ) { + self.chainAssetId = chainAssetId + self.accountId = accountId + self.chainRegistry = chainRegistry + self.assetRepository = assetRepository + self.locksRepository = locksRepository + self.eventCenter = eventCenter + self.transactionSubscription = transactionSubscription + } + + func createOrmlAccountSubscription( + remoteStorageKey: Data, + localStorageKey: String, + storage: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing { + OrmlAccountSubscription( + chainAssetId: chainAssetId, + accountId: accountId, + chainRegistry: chainRegistry, + assetRepository: assetRepository, + remoteStorageKey: remoteStorageKey, + localStorageKey: localStorageKey, + storage: storage, + operationManager: operationManager, + logger: logger, + eventCenter: eventCenter, + transactionSubscription: transactionSubscription + ) + } + + func createOrmLocksSubscription( + remoteStorageKey: Data, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing { + OrmLocksSubscription( + remoteStorageKey: remoteStorageKey, + chainAssetId: chainAssetId, + accountId: accountId, + chainRegistry: chainRegistry, + repository: locksRepository, + operationManager: operationManager, + logger: logger + ) + } +} + +// MARK: - NativeTokenSubscriptionFactoryProtocol + +extension TokenSubscriptionFactory: NativeTokenSubscriptionFactoryProtocol { + func createAccountInfoSubscription( + remoteStorageKey: Data, + localStorageKey: String, + storage: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing { + AccountInfoSubscription( + chainAssetId: chainAssetId, + accountId: accountId, + chainRegistry: chainRegistry, + assetRepository: assetRepository, + transactionSubscription: transactionSubscription, + remoteStorageKey: remoteStorageKey, + localStorageKey: localStorageKey, + storage: storage, + operationManager: operationManager, + logger: logger, + eventCenter: eventCenter + ) + } + + func createBalanceLocksSubscription( + remoteStorageKey: Data, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing { + BalanceLocksSubscription( + remoteStorageKey: remoteStorageKey, + chainAssetId: chainAssetId, + accountId: accountId, + chainRegistry: chainRegistry, + repository: locksRepository, + operationManager: operationManager, + logger: logger + ) + } +} diff --git a/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionService.swift b/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionService.swift index 3653e4cf22..dd2647af1e 100644 --- a/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionService.swift +++ b/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionService.swift @@ -1,5 +1,6 @@ import Foundation import SubstrateSdk +import RobinHood protocol WalletRemoteSubscriptionServiceProtocol { // swiftlint:disable:next function_parameter_count @@ -9,7 +10,7 @@ protocol WalletRemoteSubscriptionServiceProtocol { chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, - subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol? + subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol ) -> UUID? func detachFromAccountInfo( @@ -48,7 +49,7 @@ protocol WalletRemoteSubscriptionServiceProtocol { chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, - subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol? + subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol ) -> UUID? // swiftlint:disable:next function_parameter_count @@ -70,46 +71,67 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, - subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol? + subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol ) -> UUID? { do { let storagePath = StorageCodingPath.account - let localKey = try LocalStorageKeyFactory().createFromStoragePath( + let storageKeyFactory = LocalStorageKeyFactory() + let accountLocalKey = try storageKeyFactory.createFromStoragePath( storagePath, accountId: accountId, chainId: chainId ) + let locksStoragePath = StorageCodingPath.balanceLocks + let locksLocalKey = try storageKeyFactory.createFromStoragePath( + locksStoragePath, + encodableElement: accountId, + chainId: chainId + ) + + let accountRequest: SubscriptionRequestProtocol + let locksRequest: SubscriptionRequestProtocol switch chainFormat { case .substrate: - let request = MapSubscriptionRequest( + accountRequest = MapSubscriptionRequest( storagePath: storagePath, - localKey: localKey + localKey: accountLocalKey ) { accountId } - - return attachToSubscription( - with: [request], - chainId: chainId, - cacheKey: localKey, - queue: queue, - closure: closure, - subscriptionHandlingFactory: subscriptionHandlingFactory + locksRequest = MapSubscriptionRequest( + storagePath: .balanceLocks, + localKey: locksLocalKey, + keyParamClosure: { + accountId + } ) case .ethereum: - let request = MapSubscriptionRequest( + accountRequest = MapSubscriptionRequest( storagePath: storagePath, - localKey: localKey + localKey: accountLocalKey ) { accountId.map { StringScaleMapper(value: $0) } } - return attachToSubscription( - with: [request], - chainId: chainId, - cacheKey: localKey, - queue: queue, - closure: closure, - subscriptionHandlingFactory: subscriptionHandlingFactory + locksRequest = MapSubscriptionRequest( + storagePath: .balanceLocks, + localKey: locksLocalKey, + keyParamClosure: { accountId.map { StringScaleMapper(value: $0) } } ) } + + let handlingFactory = AccountInfoSubscriptionHandlingFactory( + accountLocalStorageKey: accountLocalKey, + locksLocalStorageKey: locksLocalKey, + factory: subscriptionHandlingFactory + ) + + return attachToSubscription( + with: [accountRequest, locksRequest], + chainId: chainId, + cacheKey: accountLocalKey + locksLocalKey, + queue: queue, + closure: closure, + subscriptionHandlingFactory: handlingFactory + ) + } catch { callbackClosureIfProvided(closure, queue: queue, result: .failure(error)) return nil @@ -125,12 +147,21 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu ) { do { let storagePath = StorageCodingPath.account - let localKey = try LocalStorageKeyFactory().createFromStoragePath( + let storageKeyFactory = LocalStorageKeyFactory() + let accountLocalKey = try storageKeyFactory.createFromStoragePath( storagePath, accountId: accountId, chainId: chainId ) + let locksStoragePath = StorageCodingPath.balanceLocks + let locksLocalKey = try storageKeyFactory.createFromStoragePath( + locksStoragePath, + encodableElement: accountId, + chainId: chainId + ) + + let localKey = accountLocalKey + locksLocalKey detachFromSubscription(localKey, subscriptionId: subscriptionId, queue: queue, closure: closure) } catch { callbackClosureIfProvided(closure, queue: queue, result: .failure(error)) @@ -193,7 +224,6 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu closure: closure, subscriptionHandlingFactory: handlingFactory ) - } catch { callbackClosureIfProvided(closure, queue: queue, result: .failure(error)) @@ -232,32 +262,50 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, - subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol? + subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol ) -> UUID? { do { - let storagePath = StorageCodingPath.ormlTokenAccount + let storageKeyFactory = LocalStorageKeyFactory() + let accountLocalKey = try storageKeyFactory.createFromStoragePath( + .ormlTokenAccount, + encodableElement: accountId + currencyId, + chainId: chainId + ) - let localKey = try LocalStorageKeyFactory().createFromStoragePath( - storagePath, + let accountRequest = DoubleMapSubscriptionRequest( + storagePath: .ormlTokenAccount, + localKey: accountLocalKey, + keyParamClosure: { (accountId, currencyId) }, + param1Encoder: nil, + param2Encoder: { $0 } + ) + let locksLocalKey = try storageKeyFactory.createFromStoragePath( + .ormlTokenLocks, encodableElement: accountId + currencyId, chainId: chainId ) - let request = DoubleMapSubscriptionRequest( - storagePath: storagePath, - localKey: localKey, + let locksRequest = DoubleMapSubscriptionRequest( + storagePath: .ormlTokenLocks, + localKey: locksLocalKey, keyParamClosure: { (accountId, currencyId) }, param1Encoder: nil, param2Encoder: { $0 } ) + let handlingFactory = OrmlTokenSubscriptionHandlingFactory( + accountLocalStorageKey: accountLocalKey, + locksLocalStorageKey: locksLocalKey, + factory: subscriptionHandlingFactory + ) + return attachToSubscription( - with: [request], + with: [accountRequest, locksRequest], chainId: chainId, - cacheKey: localKey, + cacheKey: accountLocalKey + locksLocalKey, queue: queue, closure: closure, - subscriptionHandlingFactory: subscriptionHandlingFactory + subscriptionHandlingFactory: handlingFactory ) } catch { @@ -276,12 +324,18 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu closure: RemoteSubscriptionClosure? ) { do { - let storagePath = StorageCodingPath.ormlTokenAccount - let localKey = try LocalStorageKeyFactory().createFromStoragePath( - storagePath, + let storageKeyFactory = LocalStorageKeyFactory() + let accountLocalKey = try storageKeyFactory.createFromStoragePath( + .ormlTokenAccount, + encodableElement: accountId + currencyId, + chainId: chainId + ) + let locksLocalKey = try storageKeyFactory.createFromStoragePath( + .ormlTokenLocks, encodableElement: accountId + currencyId, chainId: chainId ) + let localKey = accountLocalKey + locksLocalKey detachFromSubscription(localKey, subscriptionId: subscriptionId, queue: queue, closure: closure) diff --git a/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionWrapper.swift b/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionWrapper.swift index 48c60a20f4..c9af1deb6b 100644 --- a/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionWrapper.swift +++ b/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionWrapper.swift @@ -80,11 +80,13 @@ final class WalletRemoteSubscriptionWrapper { completion: RemoteSubscriptionClosure? ) -> UUID? { let assetsRepository = repositoryFactory.createAssetBalanceRepository() - let subscriptionHandlingFactory = OrmlAccountSubscriptionHandlingFactory( + let locksRepository = repositoryFactory.createAssetLocksRepository(for: accountId, chainAssetId: chainAssetId) + let subscriptionHandlingFactory = TokenSubscriptionFactory( chainAssetId: chainAssetId, accountId: accountId, chainRegistry: chainRegistry, assetRepository: assetsRepository, + locksRepository: locksRepository, eventCenter: eventCenter, transactionSubscription: nil ) @@ -106,14 +108,16 @@ final class WalletRemoteSubscriptionWrapper { completion: RemoteSubscriptionClosure? ) -> UUID? { let assetRepository = repositoryFactory.createAssetBalanceRepository() + let locksRepository = repositoryFactory.createAssetLocksRepository(for: accountId, chainAssetId: chainAssetId) - let subscriptionHandlingFactory = AccountInfoSubscriptionHandlingFactory( + let subscriptionHandlingFactory = TokenSubscriptionFactory( chainAssetId: chainAssetId, accountId: accountId, chainRegistry: chainRegistry, assetRepository: assetRepository, - transactionSubscription: nil, - eventCenter: eventCenter + locksRepository: locksRepository, + eventCenter: eventCenter, + transactionSubscription: nil ) return remoteSubscriptionService.attachToAccountInfo( diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/LocksSubscription.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/LocksSubscription.swift new file mode 100644 index 0000000000..3f3601245b --- /dev/null +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/LocksSubscription.swift @@ -0,0 +1,182 @@ +import RobinHood + +final class OrmLocksSubscription: LocksSubscription { + init( + remoteStorageKey: Data, + chainAssetId: ChainAssetId, + accountId: AccountId, + chainRegistry: ChainRegistryProtocol, + repository: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) { + super.init( + storageCodingPath: .ormlTokenLocks, + remoteStorageKey: remoteStorageKey, + chainAssetId: chainAssetId, + accountId: accountId, + chainRegistry: chainRegistry, + repository: repository, + operationManager: operationManager, + logger: logger + ) + } +} + +final class BalanceLocksSubscription: LocksSubscription { + init( + remoteStorageKey: Data, + chainAssetId: ChainAssetId, + accountId: AccountId, + chainRegistry: ChainRegistryProtocol, + repository: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) { + super.init( + storageCodingPath: .balanceLocks, + remoteStorageKey: remoteStorageKey, + chainAssetId: chainAssetId, + accountId: accountId, + chainRegistry: chainRegistry, + repository: repository, + operationManager: operationManager, + logger: logger + ) + } +} + +class LocksSubscription: StorageChildSubscribing { + var remoteStorageKey: Data + + let chainAssetId: ChainAssetId + let accountId: AccountId + let chainRegistry: ChainRegistryProtocol + let repository: AnyDataProviderRepository + let operationManager: OperationManagerProtocol + let storageCodingPath: StorageCodingPath + let logger: LoggerProtocol + + init( + storageCodingPath: StorageCodingPath, + remoteStorageKey: Data, + chainAssetId: ChainAssetId, + accountId: AccountId, + chainRegistry: ChainRegistryProtocol, + repository: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) { + self.remoteStorageKey = remoteStorageKey + self.chainAssetId = chainAssetId + self.accountId = accountId + self.chainRegistry = chainRegistry + self.repository = repository + self.operationManager = operationManager + self.storageCodingPath = storageCodingPath + self.logger = logger + } + + func processUpdate(_ data: Data?, blockHash _: Data?) { + guard let data = data else { + return + } + logger.debug("Did receive locks update") + + let decodingWrapper = createDecodingOperationWrapper( + data: data, + chainAssetId: chainAssetId + ) + let changesWrapper = createChangesOperationWrapper( + dependingOn: decodingWrapper, + chainAssetId: chainAssetId, + accountId: accountId + ) + + let saveOperation = createSaveOperation(dependingOn: changesWrapper) + + changesWrapper.addDependency(wrapper: decodingWrapper) + saveOperation.addDependency(changesWrapper.targetOperation) + + let operations = decodingWrapper.allOperations + changesWrapper.allOperations + [saveOperation] + + operationManager.enqueue(operations: operations, in: .transient) + } + + private func createDecodingOperationWrapper( + data: Data, + chainAssetId: ChainAssetId + ) -> CompoundOperationWrapper<[BalanceLock]?> { + guard let runtimeProvider = chainRegistry.getRuntimeProvider(for: chainAssetId.chainId) else { + logger.error("Runtime metadata unavailable for chain: \(chainAssetId.chainId)") + return CompoundOperationWrapper.createWithError( + ChainRegistryError.runtimeMetadaUnavailable + ) + } + + let codingFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() + let decodingOperation = StorageFallbackDecodingOperation<[BalanceLock]>( + path: storageCodingPath, + data: data + ) + + decodingOperation.configurationBlock = { [weak self] in + do { + decodingOperation.codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + } catch { + self?.logger.error("Error occur while decoding data: \(error.localizedDescription)") + decodingOperation.result = .failure(error) + } + } + + decodingOperation.addDependency(codingFactoryOperation) + + return CompoundOperationWrapper( + targetOperation: decodingOperation, + dependencies: [codingFactoryOperation] + ) + } + + private func createChangesOperationWrapper( + dependingOn decodingWrapper: CompoundOperationWrapper<[BalanceLock]?>, + chainAssetId: ChainAssetId, + accountId: AccountId + ) -> CompoundOperationWrapper<[DataProviderChange]?> { + let fetchOperation = repository.fetchAllOperation(with: .init()) + + let changesOperation = ClosureOperation<[DataProviderChange]?> { + let locks = try decodingWrapper + .targetOperation + .extractNoCancellableResultData() ?? [] + + let remoteModels = locks.map { + AssetLock( + chainAssetId: chainAssetId, + accountId: accountId, + type: $0.identifier, + amount: $0.amount + ) + } + + return remoteModels.map(DataProviderChange.update) + } + + changesOperation.addDependency(fetchOperation) + + return CompoundOperationWrapper(targetOperation: changesOperation, dependencies: [fetchOperation]) + } + + private func createSaveOperation( + dependingOn operation: CompoundOperationWrapper<[DataProviderChange]?> + ) -> BaseOperation { + let replaceOperation = repository.replaceOperation { + guard let changes = try operation.targetOperation.extractNoCancellableResultData() else { + return [] + } + return changes.compactMap(\.item) + } + + replaceOperation.addDependency(operation.targetOperation) + return replaceOperation + } +} diff --git a/novawallet/Common/Storage/EntityToModel/AssetLockMapper.swift b/novawallet/Common/Storage/EntityToModel/AssetLockMapper.swift new file mode 100644 index 0000000000..f08c3469e1 --- /dev/null +++ b/novawallet/Common/Storage/EntityToModel/AssetLockMapper.swift @@ -0,0 +1,41 @@ +import Foundation +import RobinHood +import CoreData +import BigInt + +final class AssetLockMapper { + var entityIdentifierFieldName: String { #keyPath(CDAssetLock.identifier) } + + typealias DataProviderModel = AssetLock + typealias CoreDataEntity = CDAssetLock +} + +extension AssetLockMapper: CoreDataMapperProtocol { + func populate( + entity: CoreDataEntity, + from model: DataProviderModel, + using _: NSManagedObjectContext + ) throws { + entity.identifier = model.identifier + entity.chainAccountId = model.accountId.toHex() + entity.chainId = model.chainAssetId.chainId + entity.assetId = Int32(bitPattern: model.chainAssetId.assetId) + entity.amount = String(model.amount) + entity.type = model.type.toUTF8String()! + } + + func transform(entity: CoreDataEntity) throws -> DataProviderModel { + let accountId = try Data(hexString: entity.chainAccountId!) + let amount = entity.amount.map { BigUInt($0) ?? 0 } ?? 0 + let chainAssetId = ChainAssetId( + chainId: entity.chainId!, + assetId: UInt32(bitPattern: entity.assetId) + ) + return .init( + chainAssetId: chainAssetId, + accountId: accountId, + type: entity.type!.data(using: .utf8)!, + amount: amount + ) + } +} diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents index 7936e89864..158e8d3a0e 100644 --- a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -22,6 +22,14 @@ + + + + + + + + @@ -142,5 +150,6 @@ + \ No newline at end of file diff --git a/novawallet/Common/Substrate/Types/AssetLock.swift b/novawallet/Common/Substrate/Types/AssetLock.swift new file mode 100644 index 0000000000..4b983615f5 --- /dev/null +++ b/novawallet/Common/Substrate/Types/AssetLock.swift @@ -0,0 +1,47 @@ +import BigInt +import RobinHood + +struct AssetLock: Equatable { + let chainAssetId: ChainAssetId + let accountId: AccountId + let type: Data + let amount: BigUInt + + var lockType: LockType? { + guard let typeString = + String(data: type, encoding: .utf8)?.trimmingCharacters(in: .whitespaces) else { + return nil + } + return LockType(rawValue: typeString.lowercased()) + } +} + +extension AssetLock: Identifiable { + static func createIdentifier( + for chainAssetId: ChainAssetId, + accountId: AccountId, + type: Data + ) -> String { + let data = [ + chainAssetId.stringValue, + accountId.toHex(), + type.toUTF8String()! + ].joined(separator: "-").data(using: .utf8)! + return data.sha256().toHex() + } + + var identifier: String { + Self.createIdentifier(for: chainAssetId, accountId: accountId, type: type) + } +} + +extension AssetLock: CustomDebugStringConvertible { + var debugDescription: String { + [ + "ChainAsset: \(chainAssetId.stringValue)", + "AccountId: \(accountId.toHex())", + "Type: \(type.toUTF8String() ?? "")", + "Amount: \(amount)" + ].joined(separator: "\n") + } +} diff --git a/novawallet/Modules/AssetList/AssetListInteractor.swift b/novawallet/Modules/AssetList/AssetListInteractor.swift index 5a80b0a874..f2c250f3d2 100644 --- a/novawallet/Modules/AssetList/AssetListInteractor.swift +++ b/novawallet/Modules/AssetList/AssetListInteractor.swift @@ -21,6 +21,8 @@ final class AssetListInteractor: AssetListBaseInteractor { private var nftSubscription: StreamableProvider? private var nftChainIds: Set? + private var assetLocksSubscriptions: [AccountId: StreamableProvider] = [:] + private var locks: [ChainAssetId: [AssetLock]] = [:] init( selectedWalletSettings: SelectedWalletSettings, @@ -50,7 +52,7 @@ final class AssetListInteractor: AssetListBaseInteractor { private func resetWallet() { clearAccountSubscriptions() clearNftSubscription() - + clearLocksSubscription() guard let selectedMetaAccount = selectedWalletSettings.value else { return } @@ -66,8 +68,14 @@ final class AssetListInteractor: AssetListBaseInteractor { presenter?.didReceiveChainModelChanges(changes) updateAccountInfoSubscription(from: changes) - setupNftSubscription(from: Array(availableChains.values)) + updateLocksSubscription(from: changes) + } + + private func clearLocksSubscription() { + assetLocksSubscriptions.values.forEach { $0.removeObserver(self) } + assetLocksSubscriptions = [:] + locks = [:] } private func providerWalletInfo() { @@ -102,6 +110,7 @@ final class AssetListInteractor: AssetListBaseInteractor { updateConnectionStatus(from: allChanges) setupNftSubscription(from: Array(availableChains.values)) + updateLocksSubscription(from: allChanges) } private func updateConnectionStatus(from changes: [DataProviderChange]) { @@ -142,6 +151,28 @@ final class AssetListInteractor: AssetListBaseInteractor { eventCenter.add(observer: self, dispatchIn: .main) } + + private func updateLocksSubscription(from changes: [DataProviderChange]) { + guard let selectedMetaAccount = selectedWalletSettings.value else { + return + } + + assetLocksSubscriptions = changes.reduce( + intitial: assetLocksSubscriptions, + selectedMetaAccount: selectedMetaAccount + ) { [weak self] in + self?.subscribeToAllLocksProvider(for: $0) + } + } + + override func handleAccountLocks(result: Result<[DataProviderChange], Error>, accountId: AccountId) { + switch result { + case let .success(changes): + handleAccountLocksChanges(changes, accountId: accountId) + case let .failure(error): + presenter?.didReceiveLocks(result: .failure(error)) + } + } } extension AssetListInteractor: AssetListInteractorInputProtocol { @@ -173,6 +204,60 @@ extension AssetListInteractor: NftLocalStorageSubscriber, NftLocalSubscriptionHa } } +extension AssetListInteractor { + private func handleAccountLocksChanges( + _ changes: [DataProviderChange], + accountId: AccountId + ) { + let initialItems = assetBalanceIdMapping.values.reduce( + into: [ChainAssetId: [AssetLock]]() + ) { accum, assetBalanceId in + guard assetBalanceId.accountId == accountId else { + return + } + + let chainAssetId = ChainAssetId( + chainId: assetBalanceId.chainId, + assetId: assetBalanceId.assetId + ) + + accum[chainAssetId] = locks[chainAssetId] + } + + locks = changes.reduce( + into: initialItems + ) { accum, change in + switch change { + case let .insert(lock), let .update(lock): + let groupIdentifier = AssetBalance.createIdentifier( + for: lock.chainAssetId, + accountId: lock.accountId + ) + guard + let assetBalanceId = assetBalanceIdMapping[groupIdentifier], + assetBalanceId.accountId == accountId else { + return + } + + let chainAssetId = ChainAssetId( + chainId: assetBalanceId.chainId, + assetId: assetBalanceId.assetId + ) + + var items = accum[chainAssetId] ?? [] + items.addOrReplaceSingle(lock) + accum[chainAssetId] = items + case let .delete(deletedIdentifier): + for chainLocks in accum { + accum[chainLocks.key] = chainLocks.value.filter { $0.identifier != deletedIdentifier } + } + } + } + + presenter?.didReceiveLocks(result: .success(Array(locks.values.flatMap { $0 }))) + } +} + extension AssetListInteractor: ConnectionStateSubscription { func didReceive(state: WebSocketEngine.State, for chainId: ChainModel.Id) { presenter?.didReceive(state: state, for: chainId) diff --git a/novawallet/Modules/AssetList/AssetListPresenter.swift b/novawallet/Modules/AssetList/AssetListPresenter.swift index 081b2ab7ba..fc1e8b3fef 100644 --- a/novawallet/Modules/AssetList/AssetListPresenter.swift +++ b/novawallet/Modules/AssetList/AssetListPresenter.swift @@ -20,6 +20,7 @@ final class AssetListPresenter: AssetListBasePresenter { private var name: String? private var hidesZeroBalances: Bool? private(set) var connectionStates: [ChainModel.Id: WebSocketEngine.State] = [:] + private(set) var locksResult: Result<[AssetLock], Error>? private var scheduler: SchedulerProtocol? @@ -280,6 +281,11 @@ final class AssetListPresenter: AssetListBasePresenter { wireframe.showAssetDetails(from: view, chain: chain, asset: asset) } + override func resetStorages() { + super.resetStorages() + locksResult = nil + } + // MARK: Interactor Output overridings override func didReceivePrices(result: Result<[ChainAssetId: PriceData], Error>?) { @@ -296,7 +302,7 @@ final class AssetListPresenter: AssetListBasePresenter { updateAssetsView() } - override func didReceiveBalance(results: [ChainAssetId: Result]) { + override func didReceiveBalance(results: [ChainAssetId: Result]) { super.didReceiveBalance(results: results) updateAssetsView() @@ -337,6 +343,21 @@ extension AssetListPresenter: AssetListPresenterProtocol { wireframe.showAssetsSearch(from: view, initState: initState, delegate: self) } + + func didTapTotalBalance() { + guard let priceResult = priceResult, + let prices = try? priceResult.get(), + let locks = try? locksResult?.get() else { + return + } + wireframe.showBalanceBreakdown( + from: view, + prices: prices, + balances: balances.values.compactMap { try? $0.get() }, + chains: allChains, + locks: locks + ) + } } extension AssetListPresenter: AssetListInteractorOutputProtocol { @@ -382,6 +403,10 @@ extension AssetListPresenter: AssetListInteractorOutputProtocol { updateAssetsView() } + + func didReceiveLocks(result: Result<[AssetLock], Error>) { + locksResult = result + } } extension AssetListPresenter: Localizable { diff --git a/novawallet/Modules/AssetList/AssetListProtocols.swift b/novawallet/Modules/AssetList/AssetListProtocols.swift index 661e22ba5f..eb51d4462a 100644 --- a/novawallet/Modules/AssetList/AssetListProtocols.swift +++ b/novawallet/Modules/AssetList/AssetListProtocols.swift @@ -18,6 +18,7 @@ protocol AssetListPresenterProtocol: AnyObject { func refresh() func presentSettings() func presentSearch() + func didTapTotalBalance() } protocol AssetListInteractorInputProtocol: AssetListBaseInteractorInputProtocol { @@ -32,6 +33,7 @@ protocol AssetListInteractorOutputProtocol: AssetListBaseInteractorOutputProtoco func didReceive(state: WebSocketEngine.State, for chainId: ChainModel.Id) func didChange(name: String) func didReceive(hidesZeroBalances: Bool) + func didReceiveLocks(result: Result<[AssetLock], Error>) } protocol AssetListWireframeProtocol: AnyObject, WalletSwitchPresentable { @@ -45,4 +47,12 @@ protocol AssetListWireframeProtocol: AnyObject, WalletSwitchPresentable { ) func showNfts(from view: AssetListViewProtocol?) + + func showBalanceBreakdown( + from view: AssetListViewProtocol?, + prices: [ChainAssetId: PriceData], + balances: [AssetBalance], + chains: [ChainModel.Id: ChainModel], + locks: [AssetLock] + ) } diff --git a/novawallet/Modules/AssetList/AssetListViewController.swift b/novawallet/Modules/AssetList/AssetListViewController.swift index d5cdc01252..3d744730d7 100644 --- a/novawallet/Modules/AssetList/AssetListViewController.swift +++ b/novawallet/Modules/AssetList/AssetListViewController.swift @@ -119,7 +119,7 @@ extension AssetListViewController: UICollectionViewDelegateFlowLayout { let cellType = AssetListFlowLayout.CellType(indexPath: indexPath) switch cellType { - case .account, .totalBalance, .settings, .emptyState: + case .account, .settings, .emptyState: break case .asset: if let groupIndex = AssetListFlowLayout.SectionType.assetsGroupIndexFromSection( @@ -130,6 +130,8 @@ extension AssetListViewController: UICollectionViewDelegateFlowLayout { } case .yourNfts: presenter.selectNfts() + case .totalBalance: + presenter.didTapTotalBalance() } } diff --git a/novawallet/Modules/AssetList/AssetListWireframe.swift b/novawallet/Modules/AssetList/AssetListWireframe.swift index 3c6b70701b..5919768a1f 100644 --- a/novawallet/Modules/AssetList/AssetListWireframe.swift +++ b/novawallet/Modules/AssetList/AssetListWireframe.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import SoraUI final class AssetListWireframe: AssetListWireframeProtocol { let walletUpdater: WalletDetailsUpdating @@ -59,4 +60,28 @@ final class AssetListWireframe: AssetListWireframeProtocol { nftListView.controller.hidesBottomBarWhenPushed = true view?.controller.navigationController?.pushViewController(nftListView.controller, animated: true) } + + func showBalanceBreakdown( + from view: AssetListViewProtocol?, + prices: [ChainAssetId: PriceData], + balances: [AssetBalance], + chains: [ChainModel.Id: ChainModel], + locks: [AssetLock] + ) { + guard let viewController = LocksViewFactory.createView(input: + .init( + prices: prices, + balances: balances, + chains: chains, + locks: locks + )) else { + return + } + + let factory = ModalSheetPresentationFactory(configuration: ModalSheetPresentationConfiguration.fearless) + viewController.controller.modalTransitioningFactory = factory + viewController.controller.modalPresentationStyle = .custom + + view?.controller.present(viewController.controller, animated: true) + } } diff --git a/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift b/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift index 2dc29843bf..e6d7101c39 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift @@ -4,7 +4,7 @@ import SubstrateSdk import SoraKeystore import BigInt -class AssetListBaseInteractor { +class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { weak var basePresenter: AssetListBaseInteractorOutputProtocol? let selectedWalletSettings: SelectedWalletSettings @@ -113,22 +113,11 @@ class AssetListBaseInteractor { } } - assetBalanceSubscriptions = changes.reduce(into: assetBalanceSubscriptions) { result, change in - switch change { - case let .insert(chain), let .update(chain): - guard let accountId = selectedMetaAccount.fetch( - for: chain.accountRequest() - )?.accountId else { - return - } - - if result[accountId] == nil { - result[accountId] = subscribeToAccountBalanceProvider(for: accountId) - } - case .delete: - // we might have the same account id used in other - break - } + assetBalanceSubscriptions = changes.reduce( + intitial: assetBalanceSubscriptions, + selectedMetaAccount: selectedMetaAccount + ) { [weak self] in + self?.subscribeToAccountBalanceProvider(for: $0) } } @@ -226,14 +215,26 @@ class AssetListBaseInteractor { func setup() { subscribeChains() } -} -extension AssetListBaseInteractor: AssetListBaseInteractorInputProtocol {} + func handleAccountBalance( + result: Result<[DataProviderChange], Error>, + accountId: AccountId + ) { + switch result { + case let .success(changes): + handleAccountBalanceChanges(changes, accountId: accountId) + case let .failure(error): + handleAccountBalanceError(error, accountId: accountId) + } + } -extension AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { + func handleAccountLocks(result _: Result<[DataProviderChange], Error>, accountId _: AccountId) {} +} + +extension AssetListBaseInteractor { private func handleAccountBalanceError(_ error: Error, accountId: AccountId) { let results = assetBalanceIdMapping.values.reduce( - into: [ChainAssetId: Result]() + into: [ChainAssetId: Result]() ) { accum, assetBalanceId in guard assetBalanceId.accountId == accountId else { return @@ -256,7 +257,7 @@ extension AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubs ) { // prepopulate non existing balances with zeros let initialItems = assetBalanceIdMapping.values.reduce( - into: [ChainAssetId: Result]() + into: [ChainAssetId: Result]() ) { accum, assetBalanceId in guard assetBalanceId.accountId == accountId else { return @@ -286,7 +287,7 @@ extension AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubs assetId: assetBalanceId.assetId ) - accum[chainAssetId] = .success(balance.totalInPlank) + accum[chainAssetId] = .success(.init(balance: balance, total: balance.totalInPlank)) case let .delete(deletedIdentifier): guard let assetBalanceId = assetBalanceIdMapping[deletedIdentifier] else { return @@ -297,24 +298,12 @@ extension AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubs assetId: assetBalanceId.assetId ) - accum[chainAssetId] = .success(0) + accum[chainAssetId] = .success(.init(total: 0)) } } basePresenter?.didReceiveBalance(results: results) } - - func handleAccountBalance( - result: Result<[DataProviderChange], Error>, - accountId: AccountId - ) { - switch result { - case let .success(changes): - handleAccountBalanceChanges(changes, accountId: accountId) - case let .failure(error): - handleAccountBalanceError(error, accountId: accountId) - } - } } extension AssetListBaseInteractor: SelectedCurrencyDepending { @@ -326,3 +315,33 @@ extension AssetListBaseInteractor: SelectedCurrencyDepending { updatePriceProvider(for: Set(availableTokenPrice.values), currency: selectedCurrency) } } + +extension Array where Element == DataProviderChange { + func reduce( + intitial: [AccountId: StreamableProvider], + selectedMetaAccount: MetaAccountModel, + subscription: @escaping (AccountId) -> StreamableProvider? + ) -> [AccountId: StreamableProvider] { + reduce(into: intitial) { result, change in + switch change { + case let .insert(chain), let .update(chain): + guard let accountId = selectedMetaAccount.fetch( + for: chain.accountRequest() + )?.accountId else { + return + } + + if result[accountId] == nil { + result[accountId] = subscription(accountId) + } + case .delete: + break + } + } + } +} + +struct CalculatedAssetBalance { + var balance: AssetBalance? + var total: BigUInt +} diff --git a/novawallet/Modules/AssetList/Base/AssetListBaseInteractorProtocol.swift b/novawallet/Modules/AssetList/Base/AssetListBaseInteractorProtocol.swift index a94dd76dd7..3414620c27 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBaseInteractorProtocol.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBaseInteractorProtocol.swift @@ -8,6 +8,6 @@ protocol AssetListBaseInteractorInputProtocol: AnyObject { protocol AssetListBaseInteractorOutputProtocol: AnyObject { func didReceiveChainModelChanges(_ changes: [DataProviderChange]) - func didReceiveBalance(results: [ChainAssetId: Result]) + func didReceiveBalance(results: [ChainAssetId: Result]) func didReceivePrices(result: Result<[ChainAssetId: PriceData], Error>?) } diff --git a/novawallet/Modules/AssetList/Base/AssetListBasePresenter.swift b/novawallet/Modules/AssetList/Base/AssetListBasePresenter.swift index 4cd80731e2..9e49e8f329 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBasePresenter.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBasePresenter.swift @@ -8,6 +8,7 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { private(set) var priceResult: Result<[ChainAssetId: PriceData], Error>? private(set) var balanceResults: [ChainAssetId: Result] = [:] + private(set) var balances: [ChainAssetId: Result] = [:] private(set) var allChains: [ChainModel.Id: ChainModel] = [:] init() { @@ -17,7 +18,7 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { func resetStorages() { allChains = [:] balanceResults = [:] - + balances = [:] groups = Self.createGroupsDiffCalculator(from: []) groupLists = [:] } @@ -130,7 +131,7 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { groups.apply(changes: groupChanges) } - func didReceiveBalance(results: [ChainAssetId: Result]) { + func didReceiveBalance(results: [ChainAssetId: Result]) { var assetsChanges: [ChainModel.Id: [DataProviderChange]] = [:] var changedGroups: [ChainModel.Id: ChainModel] = [:] @@ -138,12 +139,16 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { switch result { case let .success(maybeAmount): if let amount = maybeAmount { - balanceResults[chainAssetId] = .success(amount) + balanceResults[chainAssetId] = .success(amount.total) + amount.balance.map { + balances[chainAssetId] = .success($0) + } } else if balanceResults[chainAssetId] == nil { balanceResults[chainAssetId] = .success(0) } case let .failure(error): balanceResults[chainAssetId] = .failure(error) + balances[chainAssetId] = .failure(error) } } diff --git a/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift b/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift index 5e64cc4c85..2c0cf6d19e 100644 --- a/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift +++ b/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift @@ -152,7 +152,7 @@ final class AssetsSearchPresenter: AssetListBasePresenter { filterAndUpdateView() } - override func didReceiveBalance(results: [ChainAssetId: Result]) { + override func didReceiveBalance(results: [ChainAssetId: Result]) { super.didReceiveBalance(results: results) filterAndUpdateView() diff --git a/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift new file mode 100644 index 0000000000..7c12b608e8 --- /dev/null +++ b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift @@ -0,0 +1,147 @@ +import BigInt +import Foundation + +protocol LocksBalanceViewModelFactoryProtocol { + func formatBalance( + balances: [AssetBalance], + chains: [ChainModel.Id: ChainModel], + prices: [ChainAssetId: PriceData], + locale: Locale + ) -> FormattedBalance + func formatPlankValue( + plank: BigUInt, + chainAssetId: ChainAssetId, + chains: [ChainModel.Id: ChainModel], + prices: [ChainAssetId: PriceData], + locale: Locale + ) -> FormattedPlank? +} + +struct FormattedBalance { + let total: String + let transferrable: String + let locks: String + + let totalPrice: Decimal + let transferrablePrice: Decimal + let locksPrice: Decimal +} + +struct FormattedPlank { + let amount: String + let price: Decimal +} + +final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { + let priceAssetInfoFactory: PriceAssetInfoFactoryProtocol + let assetFormatterFactory: AssetBalanceFormatterFactoryProtocol + let currencyManager: CurrencyManagerProtocol + + init( + priceAssetInfoFactory: PriceAssetInfoFactoryProtocol, + assetFormatterFactory: AssetBalanceFormatterFactoryProtocol, + currencyManager: CurrencyManagerProtocol + ) { + self.priceAssetInfoFactory = priceAssetInfoFactory + self.assetFormatterFactory = assetFormatterFactory + self.currencyManager = currencyManager + } + + func formatBalance( + balances: [AssetBalance], + chains: [ChainModel.Id: ChainModel], + prices: [ChainAssetId: PriceData], + locale: Locale + ) -> FormattedBalance { + var totalPrice: Decimal = 0 + var transferrablePrice: Decimal = 0 + var locksPrice: Decimal = 0 + var lastPriceData: PriceData? + + for balance in balances { + guard let priceData = prices[balance.chainAssetId] else { + continue + } + guard let assetPrecision = chains[balance.chainAssetId.chainId]? + .asset(for: balance.chainAssetId.assetId)? + .precision else { + continue + } + let rate = Decimal(string: priceData.price) ?? 0.0 + + totalPrice += calculateAmount( + from: balance.totalInPlank, + precision: assetPrecision, + rate: rate + ) + transferrablePrice += calculateAmount( + from: balance.transferable, + precision: assetPrecision, + rate: rate + ) + locksPrice += calculateAmount( + from: balance.frozenInPlank + balance.reservedInPlank, + precision: assetPrecision, + rate: rate + ) + + lastPriceData = priceData + } + + let formattedTotal = formatPrice(amount: totalPrice, priceData: lastPriceData, locale: locale) + let formattedTransferrable = formatPrice(amount: transferrablePrice, priceData: lastPriceData, locale: locale) + let formattedLocks = formatPrice(amount: locksPrice, priceData: lastPriceData, locale: locale) + return .init( + total: formattedTotal, + transferrable: formattedTransferrable, + locks: formattedLocks, + totalPrice: totalPrice, + transferrablePrice: transferrablePrice, + locksPrice: locksPrice + ) + } + + func formatPlankValue( + plank: BigUInt, + chainAssetId: ChainAssetId, + chains: [ChainModel.Id: ChainModel], + prices: [ChainAssetId: PriceData], + locale: Locale + ) -> FormattedPlank? { + guard let priceData = prices[chainAssetId] else { + return nil + } + guard let assetPrecision = chains[chainAssetId.chainId]?.asset(for: chainAssetId.assetId)?.precision else { + return nil + } + let rate = Decimal(string: priceData.price) ?? 0.0 + + let price = calculateAmount( + from: plank, + precision: assetPrecision, + rate: rate + ) + + guard price > 0 else { + return nil + } + + let formattedPrice = formatPrice(amount: price, priceData: priceData, locale: locale) + return .init(amount: formattedPrice, price: price) + } + + private func calculateAmount(from plank: BigUInt, precision: UInt16, rate: Decimal) -> Decimal { + let amount = Decimal.fromSubstrateAmount( + plank, + precision: Int16(precision) + ) ?? 0.0 + return amount * rate + } + + private 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.createTokenFormatter(for: assetDisplayInfo) + return priceFormatter.value(for: locale).stringFromDecimal(amount) ?? "" + } +} diff --git a/novawallet/Modules/Locks/LocksPresenter.swift b/novawallet/Modules/Locks/LocksPresenter.swift new file mode 100644 index 0000000000..00284d8a14 --- /dev/null +++ b/novawallet/Modules/Locks/LocksPresenter.swift @@ -0,0 +1,170 @@ +import Foundation +import BigInt +import SoraFoundation + +final class LocksPresenter { + weak var view: LocksViewProtocol? + let wireframe: LocksWireframeProtocol + let input: LocksViewInput + let priceViewModelFactory: LocksBalanceViewModelFactoryProtocol + lazy var formatter: NumberFormatter = { + let formatter = NumberFormatter.percent + formatter.roundingMode = .halfEven + return formatter + }() + + init( + input: LocksViewInput, + wireframe: LocksWireframeProtocol, + localizationManager: LocalizationManagerProtocol, + priceViewModelFactory: LocksBalanceViewModelFactoryProtocol + ) { + self.input = input + self.wireframe = wireframe + self.priceViewModelFactory = priceViewModelFactory + self.localizationManager = localizationManager + } + + private func updateView() { + let balanceModel = priceViewModelFactory.formatBalance( + balances: input.balances, + chains: input.chains, + prices: input.prices, + locale: selectedLocale + ) + + let header = R.string.localizable.walletSendBalanceTotal( + preferredLanguages: selectedLocale.rLanguages + ) + + view?.updateHeader(title: header, value: balanceModel.total) + view?.update(viewModel: [ + createTranferrableSection(balanceModel: balanceModel), + createLocksSection(balanceModel: balanceModel) + ]) + } + + private func createTranferrableSection(balanceModel: FormattedBalance) -> LocksViewSectionModel { + let percent = balanceModel.totalPrice > 0 ? + balanceModel.transferrablePrice / balanceModel.totalPrice : 0 + let displayPercent = formatter.stringFromDecimal(percent) ?? "" + return LocksViewSectionModel( + header: .init( + icon: R.image.iconTransferable(), + title: R.string.localizable.walletBalanceAvailable( + preferredLanguages: selectedLocale.rLanguages + ), + details: displayPercent, + value: balanceModel.transferrable + ), + cells: [] + ) + } + + private func createLocksSection(balanceModel: FormattedBalance) -> LocksViewSectionModel { + let percent = balanceModel.totalPrice > 0 ? + balanceModel.locksPrice / balanceModel.totalPrice : 0 + let displayPercent = formatter.stringFromDecimal(percent) ?? "" + let locksCells = createLocksCells().sorted { + $0.price > $1.price + } + + return LocksViewSectionModel( + header: .init( + icon: R.image.iconBrowserSecurity(), + title: R.string.localizable.walletBalanceLocked( + preferredLanguages: selectedLocale.rLanguages + ), + details: displayPercent, + value: balanceModel.locks + ), + cells: locksCells + ) + } + + private func createLocksCells() -> [LocksViewSectionModel.CellViewModel] { + let locksCells: [LocksViewSectionModel.CellViewModel] = input.locks.compactMap { + createCell( + amountInPlank: $0.amount, + chainAssetId: $0.chainAssetId, + title: $0.lockType.map { $0.displayType.value(for: selectedLocale) } ?? + String(data: $0.type, encoding: .utf8)?.capitalized ?? "", + identifier: $0.identifier + ) + } + + let reservedCells: [LocksViewSectionModel.CellViewModel] = input.balances.compactMap { + createCell( + amountInPlank: $0.reservedInPlank, + chainAssetId: $0.chainAssetId, + title: R.string.localizable.walletBalanceReserved( + preferredLanguages: selectedLocale.rLanguages + ), + identifier: $0.identifier + ) + } + + return locksCells + reservedCells + } + + private func createCell( + amountInPlank: BigUInt, + chainAssetId: ChainAssetId, + title: String, + identifier: String + ) -> LocksViewSectionModel.CellViewModel? { + guard let chain = input.chains[chainAssetId.chainId] else { + return nil + } + guard let asset = chain.asset(for: chainAssetId.assetId) else { + return nil + } + let title = [asset.symbol, title].joined(separator: " ") + + guard let value = priceViewModelFactory.formatPlankValue( + plank: amountInPlank, + chainAssetId: chainAssetId, + chains: input.chains, + prices: input.prices, + locale: selectedLocale + ) else { + return nil + } + + return LocksViewSectionModel.CellViewModel( + id: identifier, + title: title, + value: value.amount, + price: value.price + ) + } + + var contentHeight: CGFloat { + let reservedCellsCount = input.balances.filter { + $0.reservedInPlank > 0 && input.prices[$0.chainAssetId] != nil + }.count + let locksCellsCount = input.locks.filter { + $0.amount > 0 && input.prices[$0.chainAssetId] != nil + }.count + return view?.calculateEstimatedHeight(sections: 2, items: locksCellsCount + reservedCellsCount) ?? 0 + } +} + +extension LocksPresenter: LocksPresenterProtocol { + func setup() { + updateView() + } + + func didTapOnCell() { + wireframe.close(view: view) + } +} + +extension LocksPresenter: Localizable { + func applyLocalization() { + guard view?.isSetup == true else { + return + } + updateView() + } +} diff --git a/novawallet/Modules/Locks/LocksProtocols.swift b/novawallet/Modules/Locks/LocksProtocols.swift new file mode 100644 index 0000000000..eca08b3ecf --- /dev/null +++ b/novawallet/Modules/Locks/LocksProtocols.swift @@ -0,0 +1,35 @@ +import UIKit + +protocol LocksViewProtocol: ControllerBackedProtocol { + func update(viewModel: [LocksViewSectionModel]) + func updateHeader(title: String, value: String) + func calculateEstimatedHeight(sections: Int, items: Int) -> CGFloat +} + +protocol LocksPresenterProtocol: AnyObject { + func setup() + func didTapOnCell() +} + +protocol LocksWireframeProtocol: AnyObject { + func close(view: LocksViewProtocol?) +} + +struct LocksViewSectionModel: SectionProtocol, Hashable { + let header: HeaderViewModel + var cells: [CellViewModel] + + struct HeaderViewModel: Hashable { + let icon: UIImage? + let title: String + let details: String + let value: String + } + + struct CellViewModel: Hashable { + let id: String + let title: String + let value: String + let price: Decimal + } +} diff --git a/novawallet/Modules/Locks/LocksViewController.swift b/novawallet/Modules/Locks/LocksViewController.swift new file mode 100644 index 0000000000..0ca31ac287 --- /dev/null +++ b/novawallet/Modules/Locks/LocksViewController.swift @@ -0,0 +1,110 @@ +import UIKit + +final class LocksViewController: UIViewController, ViewHolder, ModalSheetCollectionViewProtocol { + typealias RootViewType = LocksViewLayout + typealias DataSource = + UICollectionViewDiffableDataSource + + let presenter: LocksPresenterProtocol + var collectionView: UICollectionView { + rootView.collectionView + } + + private lazy var dataSource = createDataSource() + private lazy var delegate = createDelegate() + private var viewModel: [LocksViewSectionModel] = [] + + init(presenter: LocksPresenterProtocol) { + 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 = LocksViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupCollectionView() + presenter.setup() + } + + private func setupCollectionView() { + rootView.collectionView.dataSource = dataSource + rootView.collectionView.delegate = delegate + + rootView.collectionView.registerCellClass(LockCollectionViewCell.self) + rootView.collectionView.registerClass( + LocksHeaderView.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader + ) + rootView.showHeader = { _ in true } + } + + private func createDataSource() -> DataSource { + let dataSource = DataSource( + collectionView: rootView.collectionView, + cellProvider: { collectionView, indexPath, model -> UICollectionViewCell? in + let cell: LockCollectionViewCell? = collectionView.dequeueReusableCell(for: indexPath) + cell?.bind(title: model.title, value: model.value) + return cell + } + ) + + dataSource.supplementaryViewProvider = { [weak self] collectionView, _, indexPath -> UICollectionReusableView? in + guard let headerModel = self? + .dataSource + .snapshot() + .sectionIdentifiers[indexPath.section] + .header else { + return nil + } + + let header: LocksHeaderView? = collectionView.dequeueReusableSupplementaryView( + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + for: indexPath + ) + header?.bind(viewModel: + .init( + icon: headerModel.icon, + title: headerModel.title, + details: headerModel.details, + value: headerModel.value + ) + ) + + return header + } + + return dataSource + } + + private func createDelegate() -> UICollectionViewDelegate { + ModalSheetCollectionViewDelegate { [weak self] _ in + self?.presenter.didTapOnCell() + } + } +} + +extension LocksViewController: LocksViewProtocol { + func update(viewModel: [LocksViewSectionModel]) { + self.viewModel = viewModel + + dataSource.apply(viewModel) + } + + func updateHeader(title: String, value: String) { + rootView.titleLabel.text = title + rootView.valueLabel.text = value + } + + func calculateEstimatedHeight(sections: Int, items: Int) -> CGFloat { + rootView.contentHeight(sections: sections, items: items) + } +} diff --git a/novawallet/Modules/Locks/LocksViewFactory.swift b/novawallet/Modules/Locks/LocksViewFactory.swift new file mode 100644 index 0000000000..0cbb70b3cb --- /dev/null +++ b/novawallet/Modules/Locks/LocksViewFactory.swift @@ -0,0 +1,36 @@ +import Foundation +import SoraUI +import SoraFoundation + +struct LocksViewFactory { + static func createView(input: LocksViewInput) -> LocksViewProtocol? { + guard let currencyManager = CurrencyManager.shared else { + return nil + } + let wireframe = LocksWireframe() + let viewModelFactory = LocksBalanceViewModelFactory( + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager), + assetFormatterFactory: AssetBalanceFormatterFactory(), + currencyManager: currencyManager + ) + let presenter = LocksPresenter( + input: input, + wireframe: wireframe, + localizationManager: LocalizationManager.shared, + priceViewModelFactory: viewModelFactory + ) + + let view = LocksViewController(presenter: presenter) + + presenter.view = view + let maxHeight = ModalSheetPresentationConfiguration.maximumContentHeight + let preferredContentSize = min(presenter.contentHeight, maxHeight) + + view.preferredContentSize = .init( + width: 0, + height: preferredContentSize + ) + + return view + } +} diff --git a/novawallet/Modules/Locks/LocksViewInput.swift b/novawallet/Modules/Locks/LocksViewInput.swift new file mode 100644 index 0000000000..99bf4f9ec1 --- /dev/null +++ b/novawallet/Modules/Locks/LocksViewInput.swift @@ -0,0 +1,6 @@ +struct LocksViewInput { + let prices: [ChainAssetId: PriceData] + let balances: [AssetBalance] + let chains: [ChainModel.Id: ChainModel] + let locks: [AssetLock] +} diff --git a/novawallet/Modules/Locks/LocksViewLayout.swift b/novawallet/Modules/Locks/LocksViewLayout.swift new file mode 100644 index 0000000000..46d92ed741 --- /dev/null +++ b/novawallet/Modules/Locks/LocksViewLayout.swift @@ -0,0 +1,22 @@ +import UIKit + +final class LocksViewLayout: GenericCollectionViewLayout> { + let titleLabel: UILabel = .create { + $0.font = .semiBoldBody + $0.textColor = R.color.colorWhite() + } + + let valueLabel: UILabel = .create { + $0.font = .regularSubheadline + $0.textColor = R.color.colorWhite() + } + + override init(frame _: CGRect = .zero) { + let settings = GenericCollectionViewLayoutSettings( + pinToVisibleBounds: false, + estimatedRowHeight: 48, + estimatedSectionHeaderHeight: 48 + ) + super.init(header: .init(titleView: titleLabel, valueView: valueLabel), settings: settings) + } +} diff --git a/novawallet/Modules/Locks/LocksWireframe.swift b/novawallet/Modules/Locks/LocksWireframe.swift new file mode 100644 index 0000000000..08ca80463f --- /dev/null +++ b/novawallet/Modules/Locks/LocksWireframe.swift @@ -0,0 +1,7 @@ +import Foundation + +final class LocksWireframe: LocksWireframeProtocol { + func close(view: LocksViewProtocol?) { + view?.controller.presentingViewController?.dismiss(animated: true, completion: nil) + } +} diff --git a/novawallet/Modules/Locks/View/LockCollectionViewCell.swift b/novawallet/Modules/Locks/View/LockCollectionViewCell.swift new file mode 100644 index 0000000000..099b72de57 --- /dev/null +++ b/novawallet/Modules/Locks/View/LockCollectionViewCell.swift @@ -0,0 +1,40 @@ +import UIKit + +final class LockCollectionViewCell: UICollectionViewCell { + lazy var view = GenericTitleValueView( + titleView: titleLabel, + valueView: valueLabel + ) + private let titleLabel: UILabel = .create { + $0.font = .regularSubheadline + $0.textColor = R.color.colorWhite48() + } + + private let valueLabel: UILabel = .create { + $0.font = .regularSubheadline + $0.textColor = R.color.colorWhite48() + } + + override init(frame: CGRect) { + super.init(frame: frame) + + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + contentView.addSubview(view) + view.snp.makeConstraints { + $0.edges.equalToSuperview().inset(UIEdgeInsets(top: 14, left: 24, bottom: 14, right: 0)) + } + } + + func bind(title: String, value: String) { + view.titleView.text = title + view.valueView.text = value + } +} diff --git a/novawallet/Modules/Locks/View/LocksHeaderView.swift b/novawallet/Modules/Locks/View/LocksHeaderView.swift new file mode 100644 index 0000000000..49549ea5c9 --- /dev/null +++ b/novawallet/Modules/Locks/View/LocksHeaderView.swift @@ -0,0 +1,47 @@ +import UIKit + +final class LocksHeaderView: UICollectionReusableView { + private typealias TitleView = IconDetailsGenericView> + private typealias PercentView = GenericTitleValueView + + private let view = GenericTitleValueView() + + struct ViewModel { + let icon: UIImage? + let title: String + let details: String + let value: String + } + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(view) + view.snp.makeConstraints { + $0.edges.equalToSuperview().inset(UIEdgeInsets(top: 14, left: 0, bottom: 14, right: 0)) + } + + setup() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setup() { + view.titleView.imageView.contentMode = .scaleAspectFill + view.titleView.imageView.tintColor = .white + view.titleView.detailsView.titleView.font = .regularSubheadline + view.titleView.detailsView.valueView.titleView.contentInsets = .init(top: 2, left: 8, bottom: 3, right: 8) + view.titleView.detailsView.valueView.titleView.titleLabel.textColor = R.color.colorWhite80() + view.valueView.font = .regularSubheadline + } + + func bind(viewModel: ViewModel) { + view.titleView.imageView.image = viewModel.icon?.withRenderingMode(.alwaysTemplate) + view.titleView.detailsView.titleView.text = viewModel.title + view.titleView.detailsView.valueView.titleView.titleLabel.text = viewModel.details + view.valueView.text = viewModel.value + } +} diff --git a/novawallet/Modules/YourWallets/CollectionViewDelegate.swift b/novawallet/Modules/YourWallets/CollectionViewDelegate.swift new file mode 100644 index 0000000000..63cc745c42 --- /dev/null +++ b/novawallet/Modules/YourWallets/CollectionViewDelegate.swift @@ -0,0 +1,15 @@ +import UIKit +import SoraUI + +class CollectionViewDelegate: NSObject, UICollectionViewDelegate { + private let selectItemClosure: ((IndexPath) -> Void)? + + init(selectItemClosure: ((IndexPath) -> Void)? = nil) { + self.selectItemClosure = selectItemClosure + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + collectionView.deselectItem(at: indexPath, animated: true) + selectItemClosure?(indexPath) + } +} diff --git a/novawallet/Modules/YourWallets/GenericCollectionViewLayout.swift b/novawallet/Modules/YourWallets/GenericCollectionViewLayout.swift new file mode 100644 index 0000000000..c8b4cf5cb8 --- /dev/null +++ b/novawallet/Modules/YourWallets/GenericCollectionViewLayout.swift @@ -0,0 +1,121 @@ +import UIKit + +class GenericCollectionViewLayout: UIView { + var header: THeaderView = .init() + + lazy var collectionView: UICollectionView = { + let view = UICollectionView(frame: .zero, collectionViewLayout: compositionalLayout) + view.backgroundColor = .clear + view.contentInsetAdjustmentBehavior = .always + view.contentInset = settings.collectionViewContentInset + return view + }() + + var showHeader: (Int) -> Bool = { _ in false } + + private var settings = GenericCollectionViewLayoutSettings() + private lazy var compositionalLayout: UICollectionViewCompositionalLayout = { + .init { [weak self] sectionIndex, _ -> NSCollectionLayoutSection? in + let showHeader = self?.showHeader(sectionIndex) ?? false + return self?.createCompositionalLayout(showHeader: showHeader) + } + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = R.color.color0x1D1D20() + setupLayout() + } + + init(header: THeaderView, settings: GenericCollectionViewLayoutSettings = .init()) { + super.init(frame: .zero) + + self.header = header + self.settings = settings + + backgroundColor = R.color.color0x1D1D20() + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + addSubview(header) + addSubview(collectionView) + + header.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide.snp.top).offset(settings.headerContentInsets.top) + $0.leading.trailing.equalToSuperview().inset(settings.horizontalInset) + } + + header.setContentHuggingPriority(.defaultLow, for: .vertical) + + collectionView.snp.makeConstraints { + $0.top.equalTo(header.snp.bottom).offset(settings.headerContentInsets.bottom) + $0.leading.trailing.bottom.equalToSuperview() + } + } + + private func createCompositionalLayout(showHeader: Bool) -> NSCollectionLayoutSection { + .createSectionLayoutWithFullWidthRow(settings: + .init( + estimatedRowHeight: settings.estimatedRowHeight, + estimatedHeaderHeight: settings.estimatedSectionHeaderHeight, + sectionContentInsets: settings.sectionContentInsets, + sectionInterGroupSpacing: settings.interGroupSpacing, + header: showHeader ? .init(pinToVisibleBounds: settings.pinToVisibleBounds) : nil + )) + } +} + +// MARK: - Settings + +struct GenericCollectionViewLayoutSettings { + var horizontalInset: CGFloat = UIConstants.horizontalInset + var pinToVisibleBounds: Bool = true + var estimatedHeaderHeight: CGFloat = 36 + var estimatedRowHeight: CGFloat = 56 + var estimatedSectionHeaderHeight: CGFloat = 46 + var sectionContentInsets = NSDirectionalEdgeInsets( + top: 0, + leading: 16, + bottom: 0, + trailing: 16 + ) + var interGroupSpacing: CGFloat = 0 + var collectionViewContentInset = UIEdgeInsets( + top: 0, + left: 0, + bottom: 0, + right: 0 + ) + var headerContentInsets = UIEdgeInsets( + top: 3, + left: 0, + bottom: 12, + right: 0 + ) +} + +// MARK: - ContentHeight + +extension GenericCollectionViewLayout { + func contentHeight(sections: Int, items: Int) -> CGFloat { + let itemHeight = settings.estimatedRowHeight + + let sectionsHeight = settings.estimatedSectionHeaderHeight + + settings.sectionContentInsets.top + + settings.sectionContentInsets.bottom + + let estimatedListHeight = settings.collectionViewContentInset.top + + CGFloat(items) * itemHeight + + CGFloat(sections) * sectionsHeight + + settings.collectionViewContentInset.bottom + + return settings.estimatedHeaderHeight + estimatedListHeight + } +} diff --git a/novawallet/Modules/YourWallets/ModalSheetCollectionViewProtocol.swift b/novawallet/Modules/YourWallets/ModalSheetCollectionViewProtocol.swift new file mode 100644 index 0000000000..7f37adc78e --- /dev/null +++ b/novawallet/Modules/YourWallets/ModalSheetCollectionViewProtocol.swift @@ -0,0 +1,19 @@ +import SoraUI +import UIKit + +protocol ModalSheetCollectionViewProtocol: ModalSheetPresenterDelegate { + var collectionView: UICollectionView { get } +} + +extension ModalSheetCollectionViewProtocol { + func presenterCanDrag(_: ModalPresenterProtocol) -> Bool { + let offset = collectionView.contentOffset.y + collectionView.contentInset.top + return offset == 0 + } +} + +final class ModalSheetCollectionViewDelegate: CollectionViewDelegate { + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + scrollView.bounces = scrollView.contentOffset.y > UIConstants.bouncesOffset + } +} diff --git a/novawallet/Modules/YourWallets/YourWalletsViewController.swift b/novawallet/Modules/YourWallets/YourWalletsViewController.swift index c9fe40e8de..84acfa9052 100644 --- a/novawallet/Modules/YourWallets/YourWalletsViewController.swift +++ b/novawallet/Modules/YourWallets/YourWalletsViewController.swift @@ -3,13 +3,18 @@ import SoraFoundation import SoraUI import SubstrateSdk -final class YourWalletsViewController: UIViewController, ViewHolder { +final class YourWalletsViewController: UIViewController, ViewHolder, ModalSheetCollectionViewProtocol { + var collectionView: UICollectionView { + rootView.collectionView + } + typealias RootViewType = YourWalletsViewLayout typealias DataSource = UICollectionViewDiffableDataSource let presenter: YourWalletsPresenterProtocol private lazy var dataSource = createDataSource() + private lazy var delegate = createDelegate() private var viewModel: [YourWalletsViewSectionModel] = [] init(presenter: YourWalletsPresenterProtocol) { @@ -41,7 +46,8 @@ final class YourWalletsViewController: UIViewController, ViewHolder { private func setupCollectionView() { rootView.collectionView.dataSource = dataSource - rootView.collectionView.delegate = self + rootView.collectionView.delegate = delegate + rootView.collectionView.registerCellClass(SelectableIconSubtitleCollectionViewCell.self) rootView.collectionView.registerClass( RoundedIconTitleCollectionHeaderView.self, @@ -89,6 +95,21 @@ final class YourWalletsViewController: UIViewController, ViewHolder { return dataSource } + private func createDelegate() -> UICollectionViewDelegate { + ModalSheetCollectionViewDelegate( + selectItemClosure: { [weak self] indexPath in + guard let self = self else { + return + } + guard let item = self.dataSource.itemIdentifier(for: indexPath), + case let .common(viewModel) = item else { + return + } + self.presenter.didSelect(viewModel: viewModel) + } + ) + } + private static func mapWarningModel(_ model: YourWalletsCellViewModel.WarningModel) -> SelectableIconSubtitleCollectionViewCell.Model { .init( @@ -120,13 +141,7 @@ extension YourWalletsViewController: YourWalletsViewProtocol { func update(viewModel: [YourWalletsViewSectionModel]) { self.viewModel = viewModel - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections(viewModel) - viewModel.forEach { section in - snapshot.appendItems(section.cells, toSection: section) - } - - dataSource.apply(snapshot) + dataSource.apply(viewModel) } func update(header: String) { @@ -134,33 +149,6 @@ extension YourWalletsViewController: YourWalletsViewProtocol { } func calculateEstimatedHeight(sections: Int, items: Int) -> CGFloat { - RootViewType.contentHeight(sections: sections, items: items) - } -} - -// MARK: - UICollectionViewDelegate - -extension YourWalletsViewController: UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - collectionView.deselectItem(at: indexPath, animated: true) - guard let item = dataSource.itemIdentifier(for: indexPath), - case let .common(viewModel) = item else { - return - } - - presenter.didSelect(viewModel: viewModel) - } - - func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - scrollView.bounces = scrollView.contentOffset.y > UIConstants.bouncesOffset - } -} - -// MARK: - ModalSheetPresenterDelegate - -extension YourWalletsViewController: ModalSheetPresenterDelegate { - func presenterCanDrag(_: ModalPresenterProtocol) -> Bool { - let offset = rootView.collectionView.contentOffset.y + rootView.collectionView.contentInset.top - return offset == 0 + rootView.contentHeight(sections: sections, items: items) } } diff --git a/novawallet/Modules/YourWallets/YourWalletsViewLayout.swift b/novawallet/Modules/YourWallets/YourWalletsViewLayout.swift index 5412b85da6..cf1abc4bdc 100644 --- a/novawallet/Modules/YourWallets/YourWalletsViewLayout.swift +++ b/novawallet/Modules/YourWallets/YourWalletsViewLayout.swift @@ -1,111 +1,12 @@ import UIKit -final class YourWalletsViewLayout: UIView { - lazy var header: UILabel = .create { +final class YourWalletsViewLayout: GenericCollectionViewLayout { + let titleLabel: UILabel = .create { $0.font = .semiBoldBody $0.textColor = R.color.colorWhite() } - lazy var collectionView: UICollectionView = { - let view = UICollectionView(frame: .zero, collectionViewLayout: compositionalLayout) - view.backgroundColor = .clear - view.contentInsetAdjustmentBehavior = .always - view.contentInset = Constants.collectionViewContentInset - return view - }() - - var showHeader: (Int) -> Bool = { _ in false } - - private lazy var compositionalLayout: UICollectionViewCompositionalLayout = { - .init { [weak self] sectionIndex, _ -> NSCollectionLayoutSection? in - let showHeader = self?.showHeader(sectionIndex) ?? false - return Self.createCompositionalLayout(showHeader: showHeader) - } - }() - - override init(frame: CGRect) { - super.init(frame: frame) - - backgroundColor = R.color.color0x1D1D20() - setupLayout() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupLayout() { - addSubview(header) - addSubview(collectionView) - - header.snp.makeConstraints { - $0.top.equalTo(safeAreaLayoutGuide.snp.top).offset(Constants.headerContentInsets.top) - $0.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) - } - - header.setContentHuggingPriority(.defaultLow, for: .vertical) - - collectionView.snp.makeConstraints { - $0.top.equalTo(header.snp.bottom).offset(Constants.headerContentInsets.bottom) - $0.leading.trailing.bottom.equalToSuperview() - } - } - - private static func createCompositionalLayout(showHeader: Bool) -> NSCollectionLayoutSection { - .createSectionLayoutWithFullWidthRow(settings: - .init( - estimatedRowHeight: Constants.estimatedRowHeight, - estimatedHeaderHeight: Constants.estimatedSectionHeaderHeight, - sectionContentInsets: Constants.sectionContentInsets, - sectionInterGroupSpacing: Constants.interGroupSpacing, - header: showHeader ? .init(pinToVisibleBounds: true) : nil - )) - } -} - -// MARK: - Constants - -extension YourWalletsViewLayout { - private enum Constants { - static let estimatedHeaderHeight: CGFloat = 36 - static let estimatedRowHeight: CGFloat = 56 - static let estimatedSectionHeaderHeight: CGFloat = 46 - static let sectionContentInsets = NSDirectionalEdgeInsets( - top: 0, - leading: 16, - bottom: 0, - trailing: 16 - ) - static let interGroupSpacing: CGFloat = 0 - static let collectionViewContentInset = UIEdgeInsets( - top: 0, - left: 0, - bottom: 16, - right: 0 - ) - static let headerContentInsets = UIEdgeInsets( - top: 3.0, - left: 0, - bottom: 12.0, - right: 0 - ) - } -} - -extension YourWalletsViewLayout { - static func contentHeight(sections: Int, items: Int) -> CGFloat { - let itemHeight = Constants.estimatedRowHeight - - let sectionsHeight = Constants.estimatedSectionHeaderHeight + - Constants.sectionContentInsets.top + - Constants.sectionContentInsets.bottom - - let estimatedListHeight = Constants.collectionViewContentInset.top + - CGFloat(items) * itemHeight + - CGFloat(sections) * sectionsHeight + - Constants.collectionViewContentInset.bottom - - return Constants.estimatedHeaderHeight + estimatedListHeight + override init(frame _: CGRect = .zero) { + super.init(header: titleLabel) } } From 7d29d642c71d93836c71c7fd7dc006de12120eec Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 16 Sep 2022 23:13:27 +0300 Subject: [PATCH 03/52] refresh yield boost on add observer --- .../Parachain/YieldBoost/ParaStkYieldBoostProviderFactory.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novawallet/Modules/Staking/Parachain/YieldBoost/ParaStkYieldBoostProviderFactory.swift b/novawallet/Modules/Staking/Parachain/YieldBoost/ParaStkYieldBoostProviderFactory.swift index a56677eda0..2c5e049dda 100644 --- a/novawallet/Modules/Staking/Parachain/YieldBoost/ParaStkYieldBoostProviderFactory.swift +++ b/novawallet/Modules/Staking/Parachain/YieldBoost/ParaStkYieldBoostProviderFactory.swift @@ -63,7 +63,7 @@ final class ParaStkYieldBoostProviderFactory: ParaStkYieldBoostProviderFactoryPr operationManager: OperationManager(operationQueue: operationQueue) ) - let wrapperTrigger: DataProviderEventTrigger = [.onInitialization] + let wrapperTrigger: DataProviderEventTrigger = [.onInitialization, .onAddObserver] let trigger = AccountAssetBalanceTrigger( chainAssetId: chainAssetId, eventCenter: eventCenter, From a2b60438689d4d98b72794c4edc48773362965c4 Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 16 Sep 2022 23:16:13 +0300 Subject: [PATCH 04/52] add alert on Yield Boost flow swipe down --- .../ParaStkYieldBoostSetupViewController.swift | 2 +- .../ParaStkYieldBoostStartViewController.swift | 2 +- .../ParaStkYieldBoostStopViewController.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostSetup/ParaStkYieldBoostSetupViewController.swift b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostSetup/ParaStkYieldBoostSetupViewController.swift index bbdc308ce0..1288c07ddb 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostSetup/ParaStkYieldBoostSetupViewController.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostSetup/ParaStkYieldBoostSetupViewController.swift @@ -2,7 +2,7 @@ import UIKit import CommonWallet import SoraFoundation -final class ParaStkYieldBoostSetupViewController: UIViewController, ViewHolder { +final class ParaStkYieldBoostSetupViewController: UIViewController, ViewHolder, ImportantViewProtocol { typealias RootViewType = ParaStkYieldBoostSetupViewLayout let presenter: ParaStkYieldBoostSetupPresenterProtocol diff --git a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStart/ParaStkYieldBoostStartViewController.swift b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStart/ParaStkYieldBoostStartViewController.swift index 4c4117c545..dbfca6af7a 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStart/ParaStkYieldBoostStartViewController.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStart/ParaStkYieldBoostStartViewController.swift @@ -1,7 +1,7 @@ import UIKit import SoraFoundation -final class ParaStkYieldBoostStartViewController: UIViewController, ViewHolder { +final class ParaStkYieldBoostStartViewController: UIViewController, ViewHolder, ImportantViewProtocol { typealias RootViewType = ParaStkYieldBoostStartViewLayout let presenter: ParaStkYieldBoostStartPresenterProtocol diff --git a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStop/ParaStkYieldBoostStopViewController.swift b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStop/ParaStkYieldBoostStopViewController.swift index b91f2a13da..3469d63462 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStop/ParaStkYieldBoostStopViewController.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStop/ParaStkYieldBoostStopViewController.swift @@ -1,7 +1,7 @@ import UIKit import SoraFoundation -final class ParaStkYieldBoostStopViewController: UIViewController, ViewHolder { +final class ParaStkYieldBoostStopViewController: UIViewController, ViewHolder, ImportantViewProtocol { typealias RootViewType = ParaStkYieldBoostStopViewLayout let presenter: ParaStkYieldBoostStopPresenterProtocol From 2f4692b344921a57ca6c6f14a027c6a1171aba36 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 19 Sep 2022 00:25:30 +0300 Subject: [PATCH 05/52] parse amount from events --- novawallet.xcodeproj/project.pbxproj | 16 ++++ .../Helpers/AccountIdCodingWrapper.swift | 16 ++++ .../ExtrinsicProcessor+Events.swift | 42 +++++++++ .../ExtrinsicProcessor+Matching.swift | 87 +++++++++++++++---- .../Types/BalancesTransferEvent.swift | 16 ++++ .../Substrate/Types/EventCodingPath.swift | 12 +++ .../Types/TokenTransferedEvent.swift | 22 +++++ 7 files changed, 194 insertions(+), 17 deletions(-) create mode 100644 novawallet/Common/Helpers/AccountIdCodingWrapper.swift create mode 100644 novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift create mode 100644 novawallet/Common/Substrate/Types/BalancesTransferEvent.swift create mode 100644 novawallet/Common/Substrate/Types/TokenTransferedEvent.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 6a04ee64a4..e33ec55e24 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -1123,6 +1123,10 @@ 847999B628894FE200D1BAD2 /* AccountInputViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847999B528894FE200D1BAD2 /* AccountInputViewDelegate.swift */; }; 847999B82889510C00D1BAD2 /* TextInputViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847999B72889510C00D1BAD2 /* TextInputViewDelegate.swift */; }; 8479F31426CD9A0E005D8D24 /* ChainRegistryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8479F31326CD9A0E005D8D24 /* ChainRegistryIntegrationTests.swift */; }; + 847A25B928D7BB1F006AC9F5 /* BalancesTransferEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A25B828D7BB1F006AC9F5 /* BalancesTransferEvent.swift */; }; + 847A25BB28D7BB92006AC9F5 /* ExtrinsicProcessor+Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A25BA28D7BB92006AC9F5 /* ExtrinsicProcessor+Events.swift */; }; + 847A25BD28D7C0E7006AC9F5 /* TokenTransferedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A25BC28D7C0E7006AC9F5 /* TokenTransferedEvent.swift */; }; + 847A25BF28D7C2A2006AC9F5 /* AccountIdCodingWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A25BE28D7C2A2006AC9F5 /* AccountIdCodingWrapper.swift */; }; 847A6C0928817DC700477F77 /* AssetListBaseInteractorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A6C0828817DC700477F77 /* AssetListBaseInteractorProtocol.swift */; }; 847A6C0B28817E4000477F77 /* AssetListBaseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A6C0A28817E4000477F77 /* AssetListBaseInteractor.swift */; }; 847ABE3128532E1B00851218 /* ConsesusType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847ABE3028532E1B00851218 /* ConsesusType.swift */; }; @@ -3836,6 +3840,10 @@ 847999B528894FE200D1BAD2 /* AccountInputViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInputViewDelegate.swift; sourceTree = ""; }; 847999B72889510C00D1BAD2 /* TextInputViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputViewDelegate.swift; sourceTree = ""; }; 8479F31326CD9A0E005D8D24 /* ChainRegistryIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainRegistryIntegrationTests.swift; sourceTree = ""; }; + 847A25B828D7BB1F006AC9F5 /* BalancesTransferEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalancesTransferEvent.swift; sourceTree = ""; }; + 847A25BA28D7BB92006AC9F5 /* ExtrinsicProcessor+Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ExtrinsicProcessor+Events.swift"; sourceTree = ""; }; + 847A25BC28D7C0E7006AC9F5 /* TokenTransferedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenTransferedEvent.swift; sourceTree = ""; }; + 847A25BE28D7C2A2006AC9F5 /* AccountIdCodingWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountIdCodingWrapper.swift; sourceTree = ""; }; 847A6C0828817DC700477F77 /* AssetListBaseInteractorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListBaseInteractorProtocol.swift; sourceTree = ""; }; 847A6C0A28817E4000477F77 /* AssetListBaseInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListBaseInteractor.swift; sourceTree = ""; }; 847ABE3028532E1B00851218 /* ConsesusType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsesusType.swift; sourceTree = ""; }; @@ -7015,6 +7023,8 @@ 84F1CB3F27CF6BEF0095D523 /* UniquesClassDetails.swift */, 8430D6C42800040A00FFB6AE /* EthereumExecuted.swift */, 84A3B8A12836DA2600DE2669 /* LastAccountIdKeyWrapper.swift */, + 847A25B828D7BB1F006AC9F5 /* BalancesTransferEvent.swift */, + 847A25BC28D7C0E7006AC9F5 /* TokenTransferedEvent.swift */, ); path = Types; sourceTree = ""; @@ -7905,6 +7915,7 @@ 8454C26E2632BBAA00657DAD /* ExtrinsicProcessing.swift */, 849B563227A70D71007D5528 /* ExtrinsicProcessor+Fee.swift */, 849B563427A70DDE007D5528 /* ExtrinsicProcessor+Matching.swift */, + 847A25BA28D7BB92006AC9F5 /* ExtrinsicProcessor+Events.swift */, 84452F9725D6728B00F47EC5 /* RuntimeVersionSubscription.swift */, 8470D6D1253E3382009E9A5D /* StorageSubscriptionContainer.swift */, 8470D6CF253E321C009E9A5D /* StorageSubscriptionProtocols.swift */, @@ -8721,6 +8732,7 @@ 848F8B1C2863616E00204BC4 /* ChainsStore.swift */, 849FA21528A26CB500F83EAA /* CountdownTimerMediator.swift */, 84466B3228B65B5B00FA1E0D /* MetaAccountModel+Identicon.swift */, + 847A25BE28D7C2A2006AC9F5 /* AccountIdCodingWrapper.swift */, ); path = Helpers; sourceTree = ""; @@ -13855,6 +13867,7 @@ 84D8F15F24D8179000AF43E9 /* TitleWithSubtitleViewModel.swift in Sources */, AEE5FB0126415E2A002B8FDC /* StakingRebondSetupInteractor.swift in Sources */, 84C1B98624F5424700FE5470 /* ChainAccountViewModelFactory.swift in Sources */, + 847A25B928D7BB1F006AC9F5 /* BalancesTransferEvent.swift in Sources */, 84E258A52893D27E00DC8A51 /* AddressScanPresentable.swift in Sources */, AEE5FB1026457806002B8FDC /* StakingRewardDestSetupViewFactory.swift in Sources */, 849B563327A70D71007D5528 /* ExtrinsicProcessor+Fee.swift in Sources */, @@ -14056,6 +14069,7 @@ F4F2296C260DBDCE00ACFDB8 /* StakingPayoutLabelTableCell.swift in Sources */, 844CB57826FA702700396E13 /* CrowdloansViewInfo.swift in Sources */, AEACD5F9265E94AB00A09892 /* StatusViewModel.swift in Sources */, + 847A25BF28D7C2A2006AC9F5 /* AccountIdCodingWrapper.swift in Sources */, 84CCBFBC2509709500180F4F /* UIBarButtonItem+Style.swift in Sources */, 8449660A25E15ECA00F2E9F5 /* RewardDestinationViewModel.swift in Sources */, F4223F102732D445003D8E4E /* AcalaStatementData.swift in Sources */, @@ -14094,6 +14108,7 @@ 849014DD24AA8F60008F705E /* MainTabBarViewFactory.swift in Sources */, AE7129C12608CAE7000AA3F5 /* NetworkStakingInfo.swift in Sources */, 849976C427B286AF00B14A6C /* MetamaskMessage.swift in Sources */, + 847A25BD28D7C0E7006AC9F5 /* TokenTransferedEvent.swift in Sources */, 8401F24F24E524900081D8F8 /* String+Helpers.swift in Sources */, F4113B3F260C77FF00DF4DBA /* StakingRewardPayoutsViewLayout.swift in Sources */, AEA0C8A8267B6B3200F9666F /* SelectedValidatorListPresenter.swift in Sources */, @@ -15725,6 +15740,7 @@ 09A6D92CE47636723DFC91F4 /* MessageSheetViewFactory.swift in Sources */, 4BC33C8DE172AE573AEEDA4F /* WalletsListProtocols.swift in Sources */, 049DA9A36A72CB6F8401769C /* WalletsListWireframe.swift in Sources */, + 847A25BB28D7BB92006AC9F5 /* ExtrinsicProcessor+Events.swift in Sources */, 1812D5012A1765CB38D32A4A /* WalletsListPresenter.swift in Sources */, A265CC9857E951EB71E5E831 /* WalletsListInteractor.swift in Sources */, 28B4C94DBAF461CBF18B1B63 /* WalletsListViewController.swift in Sources */, diff --git a/novawallet/Common/Helpers/AccountIdCodingWrapper.swift b/novawallet/Common/Helpers/AccountIdCodingWrapper.swift new file mode 100644 index 0000000000..df750f9b42 --- /dev/null +++ b/novawallet/Common/Helpers/AccountIdCodingWrapper.swift @@ -0,0 +1,16 @@ +import Foundation +import SubstrateSdk + +struct AccountIdCodingWrapper: Decodable { + let wrappedValue: AccountId + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if let rawAccountId = try? container.decode(AccountId.self) { + wrappedValue = rawAccountId + } else { + wrappedValue = try container.decode(BytesCodable.self).wrappedValue + } + } +} diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift new file mode 100644 index 0000000000..21fd3f0eb6 --- /dev/null +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift @@ -0,0 +1,42 @@ +import Foundation +import BigInt +import SubstrateSdk + +extension ExtrinsicProcessor { + func matchBalancesTransferAmount( + from eventRecords: [EventRecord], + metadata: RuntimeMetadataProtocol, + context: RuntimeJsonContext + ) throws -> BigUInt? { + try eventRecords.first { record in + if + let eventPath = metadata.createEventCodingPath(from: record.event), + eventPath == EventCodingPath.balancesTransfer { + return true + } else { + return false + } + }.map { eventRecord in + try eventRecord.event.params.map(to: BalancesTransferEvent.self, with: context.toRawContext()) + }?.amount + } + + func matchOrmlTransferAmount( + from eventRecords: [EventRecord], + metadata: RuntimeMetadataProtocol, + context: RuntimeJsonContext + ) throws -> BigUInt? { + let eventPaths: [EventCodingPath] = [.tokensTransfer, .currenciesTransfer] + return try eventRecords.first { record in + if + let eventPath = metadata.createEventCodingPath(from: record.event), + eventPaths.contains(eventPath) { + return true + } else { + return false + } + }.map { eventRecord in + try eventRecord.event.params.map(to: TokenTransferedEvent.self, with: context.toRawContext()) + }?.amount + } +} diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Matching.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Matching.swift index f971a53660..44b825cb80 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Matching.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Matching.swift @@ -56,7 +56,14 @@ extension ExtrinsicProcessor { with: context.toRawContext() ).accountId - let result = try parseOrmlExtrinsic(extrinsic, sender: maybeSender, context: context) + let eventRecords = eventRecords.filter { $0.extrinsicIndex == extrinsicIndex } + let result = try parseOrmlExtrinsic( + extrinsic, + eventRecords: eventRecords, + metadata: metadata, + sender: maybeSender, + context: context + ) guard result.callPath.isTokensTransfer, result.isAccountMatched, let sender = maybeSender else { return nil @@ -131,19 +138,41 @@ extension ExtrinsicProcessor { private func parseOrmlExtrinsic( _ extrinsic: Extrinsic, + eventRecords: [EventRecord], + metadata: RuntimeMetadataProtocol, sender: AccountId?, context: RuntimeJsonContext ) throws -> OrmlParsingResult { - let call = try extrinsic.call.map( - to: RuntimeCall.self, - with: context.toRawContext() - ) - let callAccountId = call.args.dest.accountId - let callPath = CallCodingPath(moduleName: call.moduleName, callName: call.callName) - let isAccountMatched = accountId == sender || accountId == callAccountId - let currencyId = call.args.currencyId + if + let call = try? extrinsic.call.map( + to: RuntimeCall.self, + with: context.toRawContext() + ) { + let callAccountId = call.args.dest.accountId + let callPath = CallCodingPath(moduleName: call.moduleName, callName: call.callName) + let isAccountMatched = accountId == sender || accountId == callAccountId + let currencyId = call.args.currencyId + + return (callPath, isAccountMatched, callAccountId, currencyId, call.args.amount) + } else { + let call = try extrinsic.call.map( + to: RuntimeCall.self, + with: context.toRawContext() + ) + + let callAccountId = call.args.dest.accountId + let callPath = CallCodingPath(moduleName: call.moduleName, callName: call.callName) + let isAccountMatched = accountId == sender || accountId == callAccountId + let currencyId = call.args.currencyId - return (callPath, isAccountMatched, callAccountId, currencyId, call.args.amount) + let amount = try? matchOrmlTransferAmount( + from: eventRecords, + metadata: metadata, + context: context + ) + + return (callPath, isAccountMatched, callAccountId, currencyId, amount ?? 0) + } } private func parseEthereumTransact( @@ -361,7 +390,14 @@ extension ExtrinsicProcessor { with: context.toRawContext() ).accountId - let result = try parseBalancesExtrinsic(extrinsic, sender: maybeSender, context: context) + let extrinsicEventRecords = eventRecords.filter { $0.extrinsicIndex == extrinsicIndex } + let result = try parseBalancesExtrinsic( + extrinsic, + eventRecords: extrinsicEventRecords, + metadata: metadata, + sender: maybeSender, + context: context + ) guard result.callPath.isBalancesTransfer, @@ -408,18 +444,35 @@ extension ExtrinsicProcessor { private func parseBalancesExtrinsic( _ extrinsic: Extrinsic, + eventRecords: [EventRecord], + metadata: RuntimeMetadataProtocol, sender: AccountId?, context: RuntimeJsonContext ) throws -> BalancesParsingResult { - let call = try extrinsic.call.map( + if let call = try? extrinsic.call.map( to: RuntimeCall.self, with: context.toRawContext() - ) - let callAccountId = call.args.dest.accountId - let callPath = CallCodingPath(moduleName: call.moduleName, callName: call.callName) - let isAccountMatched = accountId == sender || accountId == callAccountId + ) { + let callAccountId = call.args.dest.accountId + let callPath = CallCodingPath(moduleName: call.moduleName, callName: call.callName) + let isAccountMatched = accountId == sender || accountId == callAccountId + + return (callPath, isAccountMatched, callAccountId, call.args.value) + } else { + let call = try extrinsic.call.map(to: RuntimeCall.self, with: context.toRawContext()) + + let callAccountId = call.args.dest.accountId + let callPath = CallCodingPath(moduleName: call.moduleName, callName: call.callName) + let isAccountMatched = accountId == sender || accountId == callAccountId - return (callPath, isAccountMatched, callAccountId, call.args.value) + let amount = try? matchBalancesTransferAmount( + from: eventRecords, + metadata: metadata, + context: context + ) + + return (callPath, isAccountMatched, callAccountId, amount ?? 0) + } } func matchExtrinsic( diff --git a/novawallet/Common/Substrate/Types/BalancesTransferEvent.swift b/novawallet/Common/Substrate/Types/BalancesTransferEvent.swift new file mode 100644 index 0000000000..e4b817ca32 --- /dev/null +++ b/novawallet/Common/Substrate/Types/BalancesTransferEvent.swift @@ -0,0 +1,16 @@ +import Foundation +import SubstrateSdk +import BigInt + +struct BalancesTransferEvent: Decodable { + let accountId: AccountId + let amount: BigUInt + + init(from decoder: Decoder) throws { + var unkeyedContainer = try decoder.unkeyedContainer() + + accountId = try unkeyedContainer.decode(AccountIdCodingWrapper.self).wrappedValue + + amount = try unkeyedContainer.decode(StringScaleMapper.self).value + } +} diff --git a/novawallet/Common/Substrate/Types/EventCodingPath.swift b/novawallet/Common/Substrate/Types/EventCodingPath.swift index 02dc28267d..344c625b02 100644 --- a/novawallet/Common/Substrate/Types/EventCodingPath.swift +++ b/novawallet/Common/Substrate/Types/EventCodingPath.swift @@ -32,6 +32,18 @@ extension EventCodingPath { EventCodingPath(moduleName: "Balances", eventName: "Withdraw") } + static var balancesTransfer: EventCodingPath { + EventCodingPath(moduleName: "Balances", eventName: "Transfer") + } + + static var tokensTransfer: EventCodingPath { + EventCodingPath(moduleName: "Tokens", eventName: "Transfered") + } + + static var currenciesTransfer: EventCodingPath { + EventCodingPath(moduleName: "Currencies", eventName: "Transfered") + } + static var ethereumExecuted: EventCodingPath { EventCodingPath(moduleName: "Ethereum", eventName: "Executed") } diff --git a/novawallet/Common/Substrate/Types/TokenTransferedEvent.swift b/novawallet/Common/Substrate/Types/TokenTransferedEvent.swift new file mode 100644 index 0000000000..4fbdc0f257 --- /dev/null +++ b/novawallet/Common/Substrate/Types/TokenTransferedEvent.swift @@ -0,0 +1,22 @@ +import Foundation +import SubstrateSdk +import BigInt + +struct TokenTransferedEvent: Decodable { + let currencyId: JSON + let sender: AccountId + let receiver: AccountId + let amount: BigUInt + + init(from decoder: Decoder) throws { + var unkeyedContainer = try decoder.unkeyedContainer() + + currencyId = try unkeyedContainer.decode(JSON.self) + + sender = try unkeyedContainer.decode(AccountIdCodingWrapper.self).wrappedValue + + receiver = try unkeyedContainer.decode(AccountIdCodingWrapper.self).wrappedValue + + amount = try unkeyedContainer.decode(StringScaleMapper.self).value + } +} From dc9e0fd972b3ab58a75c9ea9b27b87632af70659 Mon Sep 17 00:00:00 2001 From: Gulnaz <666lynx666@mail.ru> Date: Tue, 20 Sep 2022 12:20:55 +0400 Subject: [PATCH 06/52] Crowdloan contributions in total breakdown screen (#403) * init * added balances locks * remove dynamic key creation * added balances locks * added module, bugfix subscription * convert prices * ui * bugfix * bugfix * use formatter for percent value * renaming * clean up * bugfix * added check for localization setup * PR fixes * CollectionView delegate for ModalSheet * fix formatter, bugfix locks * fix nil in locks subscription * bugfixes * added filter for repository * move locks from base to assetlist * buildfix * remove sorting from subscription * rename ext * added crowdloans onchain subscription * small improvements * fix filter for locks * added crowdloans to total breakdown screen * removed useless factory, added sync * added crowdloans to total * removed orig file * fixes for queue * PR fixes * added complete for syncing, clear sync wrapper * added logger, setup base service * OffChain crowdloan contributions in total breakdown screen (#409) * added offchain contributions sync service, added locks * fix for layout * changed layout logic * fixes for layout * fixed locks * cleanup * small improvement * new name for Acala source --- novawallet.xcodeproj/project.pbxproj | 54 +++++- ...ContributionLocalSubscriptionFactory.swift | 173 +++++++++++++++++ .../CrowdloansLocalStorageSubscriber.swift | 73 +++++++ .../DataProviderChange+Identifier.swift | 12 ++ .../Foundation/NSPredicate+Filter.swift | 31 +++ .../Helpers/SubstrateRepositoryFactory.swift | 25 +++ .../Common/Services/BaseSyncService.swift | 10 +- .../CrowdloanContributionData.swift | 46 +++++ ...rowdloanContributionStreamableSource.swift | 46 +++++ .../CrowdloanOffChainSyncService.swift | 118 ++++++++++++ .../CrowdloanOnChainSyncService.swift | 179 ++++++++++++++++++ .../RemoteCrowdloanContribution.swift | 4 + .../CrowdloanContributionDataMapper.swift | 40 ++++ .../SubstrateDataModel.xcdatamodel/contents | 11 +- .../AssetList/AssetListInteractor.swift | 106 +++++++++++ .../AssetList/AssetListPresenter.swift | 178 ++++++++++++----- .../AssetList/AssetListProtocols.swift | 4 +- .../AssetList/AssetListViewController.swift | 8 +- .../AssetList/AssetListViewFactory.swift | 5 +- .../AssetList/AssetListViewLayout.swift | 6 +- .../AssetList/AssetListWireframe.swift | 6 +- .../Modules/AssetList/Models/Either.swift | 16 ++ .../LoadableViewModelState+Addition.swift | 12 ++ .../AssetList/View/AssetListFlowLayout.swift | 50 +++-- .../View/AssetListTotalBalanceCell.swift | 32 +++- .../ViewModel/AssetListViewModel.swift | 1 + .../ViewModel/AssetListViewModelFactory.swift | 4 + .../AcalaContributionSource.swift | 5 +- .../ExternalContributionSource.swift | 1 + .../ParallelContributionSource.swift | 5 +- .../Locks/LocksBalanceViewModelFactory.swift | 38 +++- novawallet/Modules/Locks/LocksPresenter.swift | 32 +++- novawallet/Modules/Locks/LocksViewInput.swift | 1 + 33 files changed, 1228 insertions(+), 104 deletions(-) create mode 100644 novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift create mode 100644 novawallet/Common/DataProvider/Subscription/CrowdloansLocalStorageSubscriber.swift create mode 100644 novawallet/Common/Extension/Foundation/DataProviderChange+Identifier.swift create mode 100644 novawallet/Common/Services/CrowdloanService/CrowdloanContributionData.swift create mode 100644 novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift create mode 100644 novawallet/Common/Services/CrowdloanService/CrowdloanOffChainSyncService.swift create mode 100644 novawallet/Common/Services/CrowdloanService/CrowdloanOnChainSyncService.swift create mode 100644 novawallet/Common/Services/CrowdloanService/RemoteCrowdloanContribution.swift create mode 100644 novawallet/Common/Storage/EntityToModel/CrowdloanContributionDataMapper.swift create mode 100644 novawallet/Modules/AssetList/Models/Either.swift create mode 100644 novawallet/Modules/AssetList/Models/LoadableViewModelState+Addition.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index d4afd9543b..aa36d0e3d1 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -2090,6 +2090,13 @@ 86EB789787B731691B36C827 /* OnChainTransferSetupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1A2F7E5E278FDCC89FE097 /* OnChainTransferSetupPresenter.swift */; }; 87F7556E02F6F5BB6F1B1AEA /* ParitySignerTxQrViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A5DCA28ABF42D342BBDF9A /* ParitySignerTxQrViewLayout.swift */; }; 880855ED28D062A9004255E7 /* Array+AddOrReplace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855EC28D062A9004255E7 /* Array+AddOrReplace.swift */; }; + 880855F028D099F2004255E7 /* CrowdloanOnChainSyncService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855EF28D099F2004255E7 /* CrowdloanOnChainSyncService.swift */; }; + 880855F228D09A0B004255E7 /* CrowdloanContributionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855F128D09A0B004255E7 /* CrowdloanContributionData.swift */; }; + 880855F428D09A26004255E7 /* RemoteCrowdloanContribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855F328D09A26004255E7 /* RemoteCrowdloanContribution.swift */; }; + 880855F628D09A3C004255E7 /* CrowdloanContributionStreamableSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855F528D09A3C004255E7 /* CrowdloanContributionStreamableSource.swift */; }; + 880855F828D09DA8004255E7 /* CrowdloanContributionDataMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855F728D09DA8004255E7 /* CrowdloanContributionDataMapper.swift */; }; + 880855FA28D0BAA2004255E7 /* CrowdloanContributionLocalSubscriptionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855F928D0BAA2004255E7 /* CrowdloanContributionLocalSubscriptionFactory.swift */; }; + 880855FC28D0C3DF004255E7 /* CrowdloansLocalStorageSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880855FB28D0C3DF004255E7 /* CrowdloansLocalStorageSubscriber.swift */; }; 8828C05828B4A67000555CB6 /* Prism.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8828C05728B4A67000555CB6 /* Prism.swift */; }; 8828C05A28B4A6A800555CB6 /* Samples.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8828C05928B4A6A800555CB6 /* Samples.swift */; }; 8828F4F328AD2734009E0B7C /* CrowdloansCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8828F4F228AD2734009E0B7C /* CrowdloansCalculator.swift */; }; @@ -2131,6 +2138,7 @@ 8887813C28B62B0A00E7290F /* FlexibleSpaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8887813B28B62B0A00E7290F /* FlexibleSpaceView.swift */; }; 8887813E28B7AA3100E7290F /* RoundedIconTitleCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8887813D28B7AA3100E7290F /* RoundedIconTitleCollectionHeaderView.swift */; }; 8887814028B7AAB700E7290F /* RoundedIconTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8887813F28B7AAB700E7290F /* RoundedIconTitleView.swift */; }; + 88A0C52128D49A090083A524 /* CrowdloanOffChainSyncService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A0C52028D49A090083A524 /* CrowdloanOffChainSyncService.swift */; }; 88A0E0FF28A284C700A9C940 /* SelectedCurrencyDepending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A0E0FC28A284C700A9C940 /* SelectedCurrencyDepending.swift */; }; 88A0E10028A284C700A9C940 /* CurrencyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A0E0FD28A284C700A9C940 /* CurrencyManager.swift */; }; 88A0E10128A284C800A9C940 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A0E0FE28A284C700A9C940 /* Observable.swift */; }; @@ -2144,6 +2152,7 @@ 88AC186328CA3F0000892A9B /* GenericCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AC186228CA3F0000892A9B /* GenericCollectionViewLayout.swift */; }; 88AC186528CA461F00892A9B /* ModalSheetCollectionViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AC186428CA461F00892A9B /* ModalSheetCollectionViewProtocol.swift */; }; 88AF35DE28C21D28003730DA /* LocksSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AF35DD28C21D28003730DA /* LocksSubscription.swift */; }; + 88BB21A028D34C660019C6B4 /* DataProviderChange+Identifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88BB219F28D34C660019C6B4 /* DataProviderChange+Identifier.swift */; }; 88C017E628C60A65003B2D28 /* AssetLockMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C017E528C60A65003B2D28 /* AssetLockMapper.swift */; }; 88C7165428C894510015D1E9 /* CollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C7165328C894510015D1E9 /* CollectionViewDelegate.swift */; }; 88C7165628C8CD050015D1E9 /* UICollectionViewDiffableDataSource+apply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C7165528C8CD050015D1E9 /* UICollectionViewDiffableDataSource+apply.swift */; }; @@ -2154,6 +2163,8 @@ 88D997B228ABC90E006135A5 /* AboutCrowdloansView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D997B128ABC90E006135A5 /* AboutCrowdloansView.swift */; }; 88E1E896289C021F00C123A8 /* CurrencyCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E1E895289C021F00C123A8 /* CurrencyCollectionViewCell.swift */; }; 88E1E898289C024400C123A8 /* UIView+Create.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E1E897289C024400C123A8 /* UIView+Create.swift */; }; + 88F19DDE28D8D0A100F6E459 /* Either.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F19DDD28D8D0A100F6E459 /* Either.swift */; }; + 88F19DE028D8D0F600F6E459 /* LoadableViewModelState+Addition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F19DDF28D8D0F600F6E459 /* LoadableViewModelState+Addition.swift */; }; 88F3A9FB9CEA464275F1115E /* ExportMnemonicViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47759907380BE9300E54DC78 /* ExportMnemonicViewFactory.swift */; }; 88F7716028BEA589008C028A /* YourWalletsIconDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F7715F28BEA589008C028A /* YourWalletsIconDetailsView.swift */; }; 88F7716428BF6B59008C028A /* GenericMultiValueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F7716328BF6B59008C028A /* GenericMultiValueView.swift */; }; @@ -4831,6 +4842,13 @@ 86F9063B2DF46E7B65B5248E /* Pods_novawalletAll_novawalletIntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_novawalletAll_novawalletIntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 87CD8C618D61C78EA8C58532 /* ParitySignerTxScanViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxScanViewLayout.swift; sourceTree = ""; }; 880855EC28D062A9004255E7 /* Array+AddOrReplace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+AddOrReplace.swift"; sourceTree = ""; }; + 880855EF28D099F2004255E7 /* CrowdloanOnChainSyncService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanOnChainSyncService.swift; sourceTree = ""; }; + 880855F128D09A0B004255E7 /* CrowdloanContributionData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionData.swift; sourceTree = ""; }; + 880855F328D09A26004255E7 /* RemoteCrowdloanContribution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteCrowdloanContribution.swift; sourceTree = ""; }; + 880855F528D09A3C004255E7 /* CrowdloanContributionStreamableSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionStreamableSource.swift; sourceTree = ""; }; + 880855F728D09DA8004255E7 /* CrowdloanContributionDataMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionDataMapper.swift; sourceTree = ""; }; + 880855F928D0BAA2004255E7 /* CrowdloanContributionLocalSubscriptionFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionLocalSubscriptionFactory.swift; sourceTree = ""; }; + 880855FB28D0C3DF004255E7 /* CrowdloansLocalStorageSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloansLocalStorageSubscriber.swift; sourceTree = ""; }; 8821119C96944A0E3526E93A /* StakingRedeemViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRedeemViewFactory.swift; sourceTree = ""; }; 8828C05728B4A67000555CB6 /* Prism.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Prism.swift; sourceTree = ""; }; 8828C05928B4A6A800555CB6 /* Samples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Samples.swift; sourceTree = ""; }; @@ -4872,6 +4890,7 @@ 8887813D28B7AA3100E7290F /* RoundedIconTitleCollectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedIconTitleCollectionHeaderView.swift; sourceTree = ""; }; 8887813F28B7AAB700E7290F /* RoundedIconTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedIconTitleView.swift; sourceTree = ""; }; 889A825F58F5CB54118A9D35 /* SelectValidatorsStartWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartWireframe.swift; sourceTree = ""; }; + 88A0C52028D49A090083A524 /* CrowdloanOffChainSyncService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanOffChainSyncService.swift; sourceTree = ""; }; 88A0E0FC28A284C700A9C940 /* SelectedCurrencyDepending.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectedCurrencyDepending.swift; sourceTree = ""; }; 88A0E0FD28A284C700A9C940 /* CurrencyManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyManager.swift; sourceTree = ""; }; 88A0E0FE28A284C700A9C940 /* Observable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; @@ -4885,6 +4904,7 @@ 88AC186228CA3F0000892A9B /* GenericCollectionViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericCollectionViewLayout.swift; sourceTree = ""; }; 88AC186428CA461F00892A9B /* ModalSheetCollectionViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalSheetCollectionViewProtocol.swift; sourceTree = ""; }; 88AF35DD28C21D28003730DA /* LocksSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocksSubscription.swift; sourceTree = ""; }; + 88BB219F28D34C660019C6B4 /* DataProviderChange+Identifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataProviderChange+Identifier.swift"; sourceTree = ""; }; 88C017E528C60A65003B2D28 /* AssetLockMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetLockMapper.swift; sourceTree = ""; }; 88C7165328C894510015D1E9 /* CollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewDelegate.swift; sourceTree = ""; }; 88C7165528C8CD050015D1E9 /* UICollectionViewDiffableDataSource+apply.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionViewDiffableDataSource+apply.swift"; sourceTree = ""; }; @@ -4895,6 +4915,8 @@ 88D997B128ABC90E006135A5 /* AboutCrowdloansView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutCrowdloansView.swift; sourceTree = ""; }; 88E1E895289C021F00C123A8 /* CurrencyCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyCollectionViewCell.swift; sourceTree = ""; }; 88E1E897289C024400C123A8 /* UIView+Create.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Create.swift"; sourceTree = ""; }; + 88F19DDD28D8D0A100F6E459 /* Either.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Either.swift; sourceTree = ""; }; + 88F19DDF28D8D0F600F6E459 /* LoadableViewModelState+Addition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoadableViewModelState+Addition.swift"; sourceTree = ""; }; 88F7715F28BEA589008C028A /* YourWalletsIconDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourWalletsIconDetailsView.swift; sourceTree = ""; }; 88F7716328BF6B59008C028A /* GenericMultiValueView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericMultiValueView.swift; sourceTree = ""; }; 899686C7351A2600FFA08371 /* TransferConfirmOnChainViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferConfirmOnChainViewFactory.swift; sourceTree = ""; }; @@ -5242,8 +5264,8 @@ 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 = ""; }; - F3D4D2E89D40718677685CE1 /* LocksViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocksViewController.swift; sourceTree = ""; }; F31A3D4E3894582CB49013F0 /* ParaStkYieldBoostStartViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStartViewLayout.swift; sourceTree = ""; }; + F3D4D2E89D40718677685CE1 /* LocksViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LocksViewController.swift; sourceTree = ""; }; F400A7C1260CE1670061D576 /* StakingRewardStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardStatus.swift; sourceTree = ""; }; F402BC82273ACDC30075F803 /* AstarBonusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AstarBonusService.swift; sourceTree = ""; }; F402BC8A273AD20D0075F803 /* AstarBonusServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AstarBonusServiceError.swift; sourceTree = ""; }; @@ -6366,6 +6388,7 @@ 84155DE8253980D700A27058 /* Services */ = { isa = PBXGroup; children = ( + 880855EE28D099BD004255E7 /* CrowdloanService */, 8411707D285B15C8006F4DFB /* XcmService */, 8466781427EC9B6C007935D3 /* PersistExtrinsicService */, 841AAC2B26F7311200F0A25E /* RemoteSubscription */, @@ -7478,6 +7501,7 @@ 845B822026EF8F1A00D25C72 /* ManagedMetaAccountMapper.swift */, 849A4EF9279ABC8800AB6709 /* AssetBalanceMapper.swift */, 88C017E528C60A65003B2D28 /* AssetLockMapper.swift */, + 880855F728D09DA8004255E7 /* CrowdloanContributionDataMapper.swift */, 8499FECB27BF8F4A00712589 /* NftModelMapper.swift */, 84F3B27727F4179A00D64CF5 /* PhishingSiteMapper.swift */, 849E07F5284A114B00DE0440 /* ParaStkScheduledRequestsMapper.swift */, @@ -7598,6 +7622,7 @@ 848CCB432832EE9B00A1FD00 /* GeneralStorageSubscriptionFactory.swift */, 842643BC28785A940031B5B5 /* TuringStakingLocalSubscriptionFactory.swift */, 84EF8D38288FCE8100265346 /* WalletListLocalSubscriptionFactory.swift */, + 880855F928D0BAA2004255E7 /* CrowdloanContributionLocalSubscriptionFactory.swift */, ); path = DataProvider; sourceTree = ""; @@ -8118,6 +8143,8 @@ children = ( 847F2D4C27AA68DD00AFD476 /* AssetListAssetModel.swift */, 847F2D4E27AA695F00AFD476 /* AssetListGroupModel.swift */, + 88F19DDD28D8D0A100F6E459 /* Either.swift */, + 88F19DDF28D8D0F600F6E459 /* LoadableViewModelState+Addition.swift */, ); path = Models; sourceTree = ""; @@ -8804,6 +8831,7 @@ 844DAAE028AD106B008E11DA /* UInt+Serialization.swift */, 84D9C8F228ADA42F007FB23B /* Data+Chunk.swift */, 880855EC28D062A9004255E7 /* Array+AddOrReplace.swift */, + 88BB219F28D34C660019C6B4 /* DataProviderChange+Identifier.swift */, ); path = Foundation; sourceTree = ""; @@ -10562,6 +10590,7 @@ 848CCB472832EF4400A1FD00 /* GeneralLocalStorageHandler.swift */, 84EF8D3D288FDA2100265346 /* WalletListLocalStorageSubscriber.swift */, 84EF8D3F288FDA7700265346 /* WalletListLocalStorageSubscriptionHandler.swift */, + 880855FB28D0C3DF004255E7 /* CrowdloansLocalStorageSubscriber.swift */, ); path = Subscription; sourceTree = ""; @@ -11440,6 +11469,18 @@ path = ParaStkUnstakeConfirm; sourceTree = ""; }; + 880855EE28D099BD004255E7 /* CrowdloanService */ = { + isa = PBXGroup; + children = ( + 880855EF28D099F2004255E7 /* CrowdloanOnChainSyncService.swift */, + 880855F128D09A0B004255E7 /* CrowdloanContributionData.swift */, + 880855F328D09A26004255E7 /* RemoteCrowdloanContribution.swift */, + 880855F528D09A3C004255E7 /* CrowdloanContributionStreamableSource.swift */, + 88A0C52028D49A090083A524 /* CrowdloanOffChainSyncService.swift */, + ); + path = CrowdloanService; + sourceTree = ""; + }; 8836AF4228AA290300A94EDD /* Currency */ = { isa = PBXGroup; children = ( @@ -13480,6 +13521,7 @@ F40966B726B297D6008CD244 /* AnalyticsContainerViewController.swift in Sources */, F4CE0FC727344F600094CD8A /* AcalaContributionConfirmProtocols.swift in Sources */, 8490145224A93FD1008F705E /* FearlessLoadingViewPresenter.swift in Sources */, + 880855F828D09DA8004255E7 /* CrowdloanContributionDataMapper.swift in Sources */, 882AA13028AE64DC0093BC63 /* CrowdloanYourContributionsTotalCell.swift in Sources */, 849976B827B24BCB00B14A6C /* DAppPolkadotExtensionTransport.swift in Sources */, 844DBC71274E83F5009F8351 /* OnboardingMainBaseWireframe.swift in Sources */, @@ -13889,6 +13931,7 @@ 8401AE8D2641EF7B000B03E3 /* NetworkFeeConfirmView.swift in Sources */, 841AAC2F26F73E0C00F0A25E /* LocalStorageKeyFactory.swift in Sources */, 843C49DF24DF3CB300B71DDA /* AccountImportMetadata.swift in Sources */, + 88A0C52128D49A090083A524 /* CrowdloanOffChainSyncService.swift in Sources */, 8436E94226C840C8003D4EA7 /* RuntimeCoderEvents.swift in Sources */, 8461CC8A26BD2F07007460E4 /* RuntimeProviderPool.swift in Sources */, 8489A6DC27FE9F570040C066 /* StakingUnbondingItemViewModel.swift in Sources */, @@ -13922,6 +13965,7 @@ 84F43C0F25DF016600AEDA56 /* DispatchQueueHelper.swift in Sources */, 8490147824A94A37008F705E /* RootPresenterFactory.swift in Sources */, 849014C224AA87E4008F705E /* LocalAuthPresenter.swift in Sources */, + 88BB21A028D34C660019C6B4 /* DataProviderChange+Identifier.swift in Sources */, 8446F5F6281916D300B7A86C /* StakingRewardsHeaderCell.swift in Sources */, 84CA68D126BE99ED003B9453 /* RuntimeProviderFactory.swift in Sources */, 848F8B1928635A5600204BC4 /* TransferSetupPresenter.swift in Sources */, @@ -13965,6 +14009,7 @@ 8401620B25E144D50087A5F3 /* AmountInputAccessoryView.swift in Sources */, 84490E7427F2CE2C00941837 /* TransferMetadataContext.swift in Sources */, 8490151324AB8A3A008F705E /* WalletEmptyStateDataSource.swift in Sources */, + 88F19DE028D8D0F600F6E459 /* LoadableViewModelState+Addition.swift in Sources */, 84DF21AB25363C9F005454AE /* Chain+Info.swift in Sources */, 84A3B8A22836DA2600DE2669 /* LastAccountIdKeyWrapper.swift in Sources */, 84243095265B1888003E07EC /* CrowdloanMetadata.swift in Sources */, @@ -14034,6 +14079,7 @@ 2A84E87825D425750006FE9C /* AlertControllerFactory.swift in Sources */, 843461CB26E2590200DCE0CD /* SubscanHistoryOperationFactory.swift in Sources */, 840DFF5128940D0C001B11EA /* ChainAddressDetailsViewModel.swift in Sources */, + 880855FA28D0BAA2004255E7 /* CrowdloanContributionLocalSubscriptionFactory.swift in Sources */, 84CFF1F226526FBC00DB7CF7 /* StakingBondMoreConfirmationVC.swift in Sources */, 8446F5F82819235B00B7A86C /* AssetIconView+Style.swift in Sources */, 84786E1F25FA6C390089DFF7 /* CDStashItem+CoreDataCodable.swift in Sources */, @@ -14079,6 +14125,7 @@ 8463A72D25E3A8E1003B8160 /* ChainStorageDecodedItem.swift in Sources */, 84DD5F30263D84F300425ACF /* RuntimeConstantFetching.swift in Sources */, 84B64E412704569D00914E88 /* StakingLocalSubscriptionHandler.swift in Sources */, + 880855F428D09A26004255E7 /* RemoteCrowdloanContribution.swift in Sources */, 848FFE9525E6DF2200652AA5 /* PagedKeysRequest.swift in Sources */, 8428766B24ADF51D00D91AD8 /* UIViewController+Modal.swift in Sources */, 84468A09286662E000BCBE00 /* CrossChainTransferSetupInteractor.swift in Sources */, @@ -14358,6 +14405,7 @@ 84D2F19D2771E5610040C680 /* ExtrinsicBuilder+Signing.swift in Sources */, 8430AAFB260230C5005B1066 /* ValidatorState.swift in Sources */, 8424308D265B1814003E07EC /* CrowdloanOperationFactory.swift in Sources */, + 880855F628D09A3C004255E7 /* CrowdloanContributionStreamableSource.swift in Sources */, 847999B82889510C00D1BAD2 /* TextInputViewDelegate.swift in Sources */, F4EF24D026BA7A4400F28B4E /* AnalyticsViewModelFactoryBase.swift in Sources */, 842A738427DDF55A006EE1EA /* OperationIdOptionsPresentable.swift in Sources */, @@ -14822,6 +14870,7 @@ 84DF21B12536DDC1005454AE /* TransferConfirmCommand.swift in Sources */, AEA0C8BC2681140700F9666F /* YourValidatorList+CustomList.swift in Sources */, 84E83AA428632AF50000B418 /* XcmPalletTransfer.swift in Sources */, + 880855FC28D0C3DF004255E7 /* CrowdloansLocalStorageSubscriber.swift in Sources */, 8499FE6827BD1EA600712589 /* DistributedStorageFactory.swift in Sources */, 8428228A289B1E5C00163031 /* TableHeaderLayoutUpdatable.swift in Sources */, 84EE2FAF2891215200A98816 /* WalletManageTableViewCell.swift in Sources */, @@ -15226,6 +15275,7 @@ 84D1ABE227E1E8060073C631 /* NewAmountInputView.swift in Sources */, 84100F3C26A60E4C00A5054E /* YourValidatorListWarningSectionView.swift in Sources */, 58F693958EF69F59D7C9760E /* StakingRewardPayoutsInteractor.swift in Sources */, + 88F19DDE28D8D0A100F6E459 /* Either.swift in Sources */, 50758C9BBB27AE5732FF78BA /* StakingRewardPayoutsViewController.swift in Sources */, AEF507AF262423FD0098574D /* HmacSigner.swift in Sources */, 3229E306230161AA99B14BDD /* StakingRewardPayoutsViewFactory.swift in Sources */, @@ -15392,6 +15442,7 @@ 84D9C8EF28AD97E7007FB23B /* SupportedLedgerApps.swift in Sources */, 8482F62A280C4B770006C3A0 /* DAppAuthSettingsViewModel.swift in Sources */, 840DC8422880AF440039A054 /* AssetSelectionViewController.swift in Sources */, + 880855F228D09A0B004255E7 /* CrowdloanContributionData.swift in Sources */, B51AD1836313CE26F369ED3F /* CustomValidatorListWireframe.swift in Sources */, D565DB5ED3B8B4D9BCFB4C21 /* CustomValidatorListPresenter.swift in Sources */, 8AEF593AFE8F59F7DC0A5753 /* CustomValidatorListInteractor.swift in Sources */, @@ -15462,6 +15513,7 @@ F4433D7A26C16D070002A91E /* AnalyticsValidatorsViewModel.swift in Sources */, 340AC2484415B10F247C135E /* AnalyticsValidatorsPresenter.swift in Sources */, C9A608AFCFF4030D63D1FB4F /* AnalyticsValidatorsInteractor.swift in Sources */, + 880855F028D099F2004255E7 /* CrowdloanOnChainSyncService.swift in Sources */, 8BF525D6B5DFB7CF6C03B015 /* AnalyticsValidatorsViewController.swift in Sources */, 237AD34CD1C2778834D7B330 /* AnalyticsValidatorsViewFactory.swift in Sources */, 843CE3A627D2098100436F4E /* NftDetailsLabel.swift in Sources */, diff --git a/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift new file mode 100644 index 0000000000..2afbb55b37 --- /dev/null +++ b/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift @@ -0,0 +1,173 @@ +import SubstrateSdk +import RobinHood + +protocol CrowdloanContributionLocalSubscriptionFactoryProtocol { + func getCrowdloanContributionDataProvider( + for accountId: AccountId, + chain: ChainModel + ) -> StreamableProvider? +} + +final class CrowdloanContributionLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, + CrowdloanContributionLocalSubscriptionFactoryProtocol { + let operationFactory: CrowdloanOperationFactoryProtocol + let paraIdOperationFactory: ParaIdOperationFactoryProtocol + + init( + operationFactory: CrowdloanOperationFactoryProtocol, + operationManager: OperationManagerProtocol, + chainRegistry: ChainRegistryProtocol, + storageFacade: StorageFacadeProtocol, + paraIdOperationFactory: ParaIdOperationFactoryProtocol, + logger: LoggerProtocol + ) { + self.operationFactory = operationFactory + self.paraIdOperationFactory = paraIdOperationFactory + + super.init( + chainRegistry: chainRegistry, + storageFacade: storageFacade, + operationManager: operationManager, + logger: logger + ) + } + + func getCrowdloanContributionDataProvider( + for accountId: AccountId, + chain: ChainModel + ) -> StreamableProvider? { + let cacheKey = "crowdloanContributions-\(accountId.toHex())-\(chain.chainId)" + + if let provider = getProvider(for: cacheKey) as? StreamableProvider { + return provider + } + + let offchainSources: [ExternalContributionSourceProtocol] = [ + ParallelContributionSource(), + AcalaContributionSource( + paraIdOperationFactory: paraIdOperationFactory, + acalaChainId: KnowChainId.acala + ) + ] + + let onChainSyncService = createOnChainSyncService(chainId: chain.chainId, accountId: accountId) + let offChainSyncServices = createOffChainSyncServices( + from: offchainSources, + chain: chain, + accountId: accountId + ) + + let syncServices = [onChainSyncService] + offChainSyncServices + + let source = CrowdloanContributionStreamableSource(syncServices: syncServices) + + let crowdloansFilter = NSPredicate.crowdloanContribution( + for: chain.chainId, + accountId: accountId + ) + + let mapper = CrowdloanContributionDataMapper() + let repository = storageFacade.createRepository( + filter: crowdloansFilter, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + + let observable = CoreDataContextObservable( + service: storageFacade.databaseService, + mapper: AnyCoreDataMapper(mapper), + predicate: { entity in + accountId.toHex() == entity.chainAccountId && + chain.chainId == entity.chainId + } + ) + + observable.start { [weak self] error in + if let error = error { + self?.logger.error("Did receive error: \(error)") + } + } + + let provider = StreamableProvider( + source: AnyStreamableSource(source), + repository: AnyDataProviderRepository(repository), + observable: AnyDataProviderRepositoryObservable(observable), + operationManager: operationManager + ) + + saveProvider(provider, for: cacheKey) + + return provider + } + + private func createOnChainSyncService(chainId: ChainModel.Id, accountId: AccountId) -> SyncServiceProtocol { + let mapper = CrowdloanContributionDataMapper() + let onChainFilter = NSPredicate.crowdloanContribution( + for: chainId, + accountId: accountId, + source: nil + ) + let onChainCrowdloansRepository = storageFacade.createRepository( + filter: onChainFilter, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + return CrowdloanOnChainSyncService( + operationFactory: operationFactory, + chainRegistry: chainRegistry, + repository: AnyDataProviderRepository(onChainCrowdloansRepository), + accountId: accountId, + chainId: chainId, + operationManager: operationManager, + logger: logger + ) + } + + private func createOffChainSyncServices( + from sources: [ExternalContributionSourceProtocol], + chain: ChainModel, + accountId: AccountId + ) -> [SyncServiceProtocol] { + let mapper = CrowdloanContributionDataMapper() + + return sources.map { source in + let chainFilter = NSPredicate.crowdloanContribution( + for: chain.chainId, + accountId: accountId, + source: source.sourceName + ) + let serviceRepository = storageFacade.createRepository( + filter: chainFilter, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + return CrowdloanOffChainSyncService( + source: source, + chain: chain, + accountId: accountId, + operationManager: operationManager, + repository: AnyDataProviderRepository(serviceRepository), + logger: logger + ) + } + } +} + +extension CrowdloanContributionLocalSubscriptionFactory { + static let operationManager = OperationManagerFacade.sharedManager + + static let shared = CrowdloanContributionLocalSubscriptionFactory( + operationFactory: CrowdloanOperationFactory( + requestOperationFactory: StorageRequestFactory( + remoteFactory: StorageKeyFactory(), + operationManager: operationManager + ), + operationManager: operationManager + ), + operationManager: operationManager, + chainRegistry: ChainRegistryFacade.sharedRegistry, + storageFacade: SubstrateDataStorageFacade.shared, + paraIdOperationFactory: ParaIdOperationFactory.shared, + logger: Logger.shared + ) +} diff --git a/novawallet/Common/DataProvider/Subscription/CrowdloansLocalStorageSubscriber.swift b/novawallet/Common/DataProvider/Subscription/CrowdloansLocalStorageSubscriber.swift new file mode 100644 index 0000000000..e11be7a2f1 --- /dev/null +++ b/novawallet/Common/DataProvider/Subscription/CrowdloansLocalStorageSubscriber.swift @@ -0,0 +1,73 @@ +import Foundation +import RobinHood + +protocol CrowdloanContributionLocalSubscriptionHandler: AnyObject { + func handleCrowdloans( + result: Result<[DataProviderChange], Error>, + accountId: AccountId, + chain: ChainModel + ) +} + +protocol CrowdloansLocalStorageSubscriber: AnyObject { + var crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol { get } + var crowdloansLocalSubscriptionHandler: CrowdloanContributionLocalSubscriptionHandler { get } + + func subscribeToCrowdloansProvider( + for account: AccountId, + chain: ChainModel + ) -> StreamableProvider? +} + +extension CrowdloansLocalStorageSubscriber { + func subscribeToCrowdloansProvider( + for accountId: AccountId, + chain: ChainModel + ) -> StreamableProvider? { + guard let provider = crowdloansLocalSubscriptionFactory.getCrowdloanContributionDataProvider( + for: accountId, + chain: chain + ) else { + return nil + } + + let updateClosure = { [weak self] (changes: [DataProviderChange]) in + self?.crowdloansLocalSubscriptionHandler.handleCrowdloans( + result: .success(changes), + accountId: accountId, + chain: chain + ) + return + } + + let failureClosure = { [weak self] (error: Error) in + self?.crowdloansLocalSubscriptionHandler.handleCrowdloans( + result: .failure(error), + accountId: accountId, + chain: chain + ) + return + } + + let options = StreamableProviderObserverOptions( + alwaysNotifyOnRefresh: true, + waitsInProgressSyncOnAdd: false, + initialSize: 0, + refreshWhenEmpty: false + ) + + provider.addObserver( + self, + deliverOn: .main, + executing: updateClosure, + failing: failureClosure, + options: options + ) + + return provider + } +} + +extension CrowdloansLocalStorageSubscriber where Self: CrowdloanContributionLocalSubscriptionHandler { + var crowdloansLocalSubscriptionHandler: CrowdloanContributionLocalSubscriptionHandler { self } +} diff --git a/novawallet/Common/Extension/Foundation/DataProviderChange+Identifier.swift b/novawallet/Common/Extension/Foundation/DataProviderChange+Identifier.swift new file mode 100644 index 0000000000..923f5b8332 --- /dev/null +++ b/novawallet/Common/Extension/Foundation/DataProviderChange+Identifier.swift @@ -0,0 +1,12 @@ +import RobinHood + +extension DataProviderChange where T: Identifiable { + var identifier: String { + switch self { + case let .insert(newItem), let .update(newItem): + return newItem.identifier + case let .delete(deletedIdentifier): + return deletedIdentifier + } + } +} diff --git a/novawallet/Common/Extension/Foundation/NSPredicate+Filter.swift b/novawallet/Common/Extension/Foundation/NSPredicate+Filter.swift index c153dd2bbb..793a0d6d32 100644 --- a/novawallet/Common/Extension/Foundation/NSPredicate+Filter.swift +++ b/novawallet/Common/Extension/Foundation/NSPredicate+Filter.swift @@ -278,4 +278,35 @@ extension NSPredicate { static func filterAuthorizedDApps(by metaId: String) -> NSPredicate { NSPredicate(format: "%K == %@", #keyPath(CDDAppSettings.metaId), metaId) } + + static func crowdloanContribution( + for chainId: ChainModel.Id, + accountId: AccountId, + source: String? + ) -> NSPredicate { + let accountChainPredicate = crowdloanContribution(for: chainId, accountId: accountId) + let sourcePredicate = source.map { + NSPredicate(format: "%K == %@", #keyPath(CDCrowdloanContribution.source), $0) + } ?? NSPredicate(format: "%K = nil", #keyPath(CDCrowdloanContribution.source)) + + return NSCompoundPredicate(andPredicateWithSubpredicates: [accountChainPredicate, sourcePredicate]) + } + + static func crowdloanContribution( + for chainId: ChainModel.Id, + accountId: AccountId + ) -> NSPredicate { + let chainPredicate = NSPredicate( + format: "%K == %@", + #keyPath(CDCrowdloanContribution.chainId), + chainId + ) + let accountPredicate = NSPredicate( + format: "%K == %@", + #keyPath(CDCrowdloanContribution.chainAccountId), + accountId.toHex() + ) + + return NSCompoundPredicate(andPredicateWithSubpredicates: [chainPredicate, accountPredicate]) + } } diff --git a/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift b/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift index f64449a2ea..07859386da 100644 --- a/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift +++ b/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift @@ -40,6 +40,12 @@ protocol SubstrateRepositoryFactoryProtocol { func createPhishingSitesRepositoryWithPredicate( _ filter: NSPredicate ) -> AnyDataProviderRepository + + func createCrowdloanContributionRepository( + accountId: AccountId, + chainId: ChainModel.Id, + source: String? + ) -> AnyDataProviderRepository } final class SubstrateRepositoryFactory: SubstrateRepositoryFactoryProtocol { @@ -203,4 +209,23 @@ final class SubstrateRepositoryFactory: SubstrateRepositoryFactoryProtocol { ) return AnyDataProviderRepository(repository) } + + func createCrowdloanContributionRepository( + accountId: AccountId, + chainId: ChainModel.Id, + source: String? + ) -> AnyDataProviderRepository { + let filter = NSPredicate.crowdloanContribution( + for: chainId, + accountId: accountId, + source: source + ) + let mapper = CrowdloanContributionDataMapper() + let repository = storageFacade.createRepository( + filter: filter, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + return AnyDataProviderRepository(repository) + } } diff --git a/novawallet/Common/Services/BaseSyncService.swift b/novawallet/Common/Services/BaseSyncService.swift index e1d5efc2ed..97560a8398 100644 --- a/novawallet/Common/Services/BaseSyncService.swift +++ b/novawallet/Common/Services/BaseSyncService.swift @@ -2,7 +2,15 @@ import Foundation import RobinHood import SubstrateSdk -class BaseSyncService { +protocol SyncServiceProtocol { + func performSyncUp() + func stopSyncUp() + func setup() + + var isActive: Bool { get } +} + +class BaseSyncService: SyncServiceProtocol { let retryStrategy: ReconnectionStrategyProtocol let logger: LoggerProtocol? diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionData.swift b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionData.swift new file mode 100644 index 0000000000..0a809aa4c0 --- /dev/null +++ b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionData.swift @@ -0,0 +1,46 @@ +import BigInt +import RobinHood + +struct CrowdloanContributionData { + let accountId: AccountId + let chainId: ChainModel.Id + let paraId: ParaId + let source: String? + let amount: BigUInt + + var type: SourceType { + if let source = source, !source.isEmpty { + return .offChain + } else { + return .onChain + } + } + + enum SourceType: String { + case onChain + case offChain + } +} + +extension CrowdloanContributionData: Identifiable { + var identifier: String { + Self.createIdentifier(for: chainId, accountId: accountId, paraId: paraId, source: source) + } + + static func createIdentifier( + for chainId: ChainModel.Id, + accountId: AccountId, + paraId: ParaId, + source: String? + ) -> String { + let data = [ + chainId, + accountId.toHex(), + paraId.toHex(), + source + ].compactMap { $0 } + .joined(separator: "-") + .data(using: .utf8)! + return data.sha256().toHex() + } +} diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift new file mode 100644 index 0000000000..9df80c5184 --- /dev/null +++ b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift @@ -0,0 +1,46 @@ +import Foundation +import RobinHood + +final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { + typealias Model = CrowdloanContributionData + + let syncServices: [SyncServiceProtocol] + + init(syncServices: [SyncServiceProtocol]) { + self.syncServices = syncServices + } + + func fetchHistory( + runningIn queue: DispatchQueue?, + commitNotificationBlock: ((Result?) -> Void)? + ) { + guard let closure = commitNotificationBlock else { + return + } + + let result: Result = Result.success(0) + + dispatchInQueueWhenPossible(queue) { + closure(result) + } + } + + func refresh( + runningIn queue: DispatchQueue?, + commitNotificationBlock: ((Result?) -> Void)? + ) { + syncServices.forEach { + $0.isActive ? $0.performSyncUp() : $0.setup() + } + + guard let closure = commitNotificationBlock else { + return + } + + let result: Result = Result.success(0) + + dispatchInQueueWhenPossible(queue) { + closure(result) + } + } +} diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanOffChainSyncService.swift b/novawallet/Common/Services/CrowdloanService/CrowdloanOffChainSyncService.swift new file mode 100644 index 0000000000..1bfced4173 --- /dev/null +++ b/novawallet/Common/Services/CrowdloanService/CrowdloanOffChainSyncService.swift @@ -0,0 +1,118 @@ +import RobinHood + +final class CrowdloanOffChainSyncService: BaseSyncService { + private let source: ExternalContributionSourceProtocol + private let operationManager: OperationManagerProtocol + private let repository: AnyDataProviderRepository + private var syncOperationWrapper: CompoundOperationWrapper? + private let chain: ChainModel + private let accountId: AccountId + + init( + source: ExternalContributionSourceProtocol, + chain: ChainModel, + accountId: AccountId, + operationManager: OperationManagerProtocol, + repository: AnyDataProviderRepository, + logger: LoggerProtocol? + ) { + self.source = source + self.operationManager = operationManager + self.repository = repository + self.chain = chain + self.accountId = accountId + + super.init(logger: logger) + } + + private func contributionsFetchOperation( + accountId: AccountId, + chain: ChainModel + ) -> CompoundOperationWrapper<[ExternalContribution]> { + source.getContributions(accountId: accountId, chain: chain) + } + + private func createChangesOperationWrapper( + dependingOn contributionsOperation: CompoundOperationWrapper<[ExternalContribution]>, + chainId: ChainModel.Id, + accountId: AccountId + ) -> BaseOperation<[DataProviderChange]?> { + let changesOperation = ClosureOperation<[DataProviderChange]?> { + let contributions = try contributionsOperation.targetOperation.extractNoCancellableResultData() + + let remoteModels: [CrowdloanContributionData] = contributions.compactMap { + CrowdloanContributionData( + accountId: accountId, + chainId: chainId, + paraId: $0.paraId, + source: $0.source, + amount: $0.amount + ) + } + + return remoteModels.map(DataProviderChange.update) + } + + changesOperation.addDependency(contributionsOperation.targetOperation) + + return changesOperation + } + + private func createSaveOperation( + dependingOn operation: BaseOperation<[DataProviderChange]?> + ) -> BaseOperation { + let replaceOperation = repository.replaceOperation { + guard let changes = try operation.extractNoCancellableResultData() else { + return [] + } + return changes.compactMap(\.item) + } + + replaceOperation.addDependency(operation) + return replaceOperation + } + + override func performSyncUp() { + let contributionsFetchOperation = contributionsFetchOperation( + accountId: accountId, + chain: chain + ) + + let changesWrapper = createChangesOperationWrapper( + dependingOn: contributionsFetchOperation, + chainId: chain.chainId, + accountId: accountId + ) + let saveOperation = createSaveOperation(dependingOn: changesWrapper) + + saveOperation.completionBlock = { + guard !saveOperation.isCancelled else { + return + } + + do { + try saveOperation.extractNoCancellableResultData() + self.syncOperationWrapper = nil + self.complete(nil) + } catch { + self.syncOperationWrapper = nil + self.complete(error) + } + } + + let operations = contributionsFetchOperation.allOperations + [changesWrapper] + + let syncWrapper = CompoundOperationWrapper( + targetOperation: saveOperation, + dependencies: operations + ) + + syncOperationWrapper = syncWrapper + operationManager.enqueue(operations: syncWrapper.allOperations, in: .transient) + } + + override func stopSyncUp() { + syncOperationWrapper?.cancel() + syncOperationWrapper = nil + } +} diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanOnChainSyncService.swift b/novawallet/Common/Services/CrowdloanService/CrowdloanOnChainSyncService.swift new file mode 100644 index 0000000000..319c66498e --- /dev/null +++ b/novawallet/Common/Services/CrowdloanService/CrowdloanOnChainSyncService.swift @@ -0,0 +1,179 @@ +import SubstrateSdk +import RobinHood + +final class CrowdloanOnChainSyncService: BaseSyncService { + private let operationFactory: CrowdloanOperationFactoryProtocol + private let chainRegistry: ChainRegistryProtocol + private let operationManager: OperationManagerProtocol + private let accountId: AccountId + private let chainId: ChainModel.Id + private let repository: AnyDataProviderRepository + private var syncOperationWrapper: CompoundOperationWrapper? + + init( + operationFactory: CrowdloanOperationFactoryProtocol, + chainRegistry: ChainRegistryProtocol, + repository: AnyDataProviderRepository, + accountId: AccountId, + chainId: ChainModel.Id, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol? + ) { + self.operationFactory = operationFactory + self.chainRegistry = chainRegistry + self.repository = repository + self.accountId = accountId + self.chainId = chainId + self.operationManager = operationManager + + super.init(logger: logger) + } + + private func contributionsFetchOperation( + dependingOn fetchCrowdloansOperation: CompoundOperationWrapper<[Crowdloan]>, + connection: ChainConnection, + runtimeService: RuntimeProviderProtocol, + accountId: AccountId + ) -> BaseOperation<[RemoteCrowdloanContribution]> { + let contributionsOperation: BaseOperation<[RemoteCrowdloanContribution]> = + OperationCombiningService(operationManager: operationManager) { [weak self] in + guard let self = self else { + return [] + } + + let crowdloans = try fetchCrowdloansOperation.targetOperation.extractNoCancellableResultData() + + return crowdloans.map { crowdloan in + let fetchOperation = self.operationFactory.fetchContributionOperation( + connection: connection, + runtimeService: runtimeService, + accountId: accountId, + index: crowdloan.fundInfo.index + ) + + let mapOperation = ClosureOperation { + let contributionResponse = try fetchOperation.targetOperation.extractNoCancellableResultData() + + return RemoteCrowdloanContribution( + crowdloan: crowdloan, + contribution: contributionResponse.contribution + ) + } + + mapOperation.addDependency(fetchOperation.targetOperation) + + return CompoundOperationWrapper( + targetOperation: mapOperation, + dependencies: fetchOperation.allOperations + ) + } + }.longrunOperation() + + contributionsOperation.addDependency(fetchCrowdloansOperation.targetOperation) + + return contributionsOperation + } + + private func createChangesOperationWrapper( + dependingOn contributionsOperation: BaseOperation<[RemoteCrowdloanContribution]>, + chainId: ChainModel.Id, + accountId: AccountId + ) -> BaseOperation<[DataProviderChange]?> { + let changesOperation = ClosureOperation<[DataProviderChange]?> { + let contributions = try contributionsOperation + .extractNoCancellableResultData() + + let remoteModels: [CrowdloanContributionData] = contributions.compactMap { + guard let contribution = $0.contribution else { + return nil + } + return CrowdloanContributionData( + accountId: accountId, + chainId: chainId, + paraId: $0.crowdloan.paraId, + source: nil, + amount: contribution.balance + ) + } + + return remoteModels.map(DataProviderChange.update) + } + + changesOperation.addDependency(contributionsOperation) + + return changesOperation + } + + private func createSaveOperation( + dependingOn operation: BaseOperation<[DataProviderChange]?> + ) -> BaseOperation { + let replaceOperation = repository.replaceOperation { + guard let changes = try operation.extractNoCancellableResultData() else { + return [] + } + return changes.compactMap(\.item) + } + + replaceOperation.addDependency(operation) + return replaceOperation + } + + override func performSyncUp() { + guard let connection = chainRegistry.getConnection(for: chainId) else { + logger?.error("Connection for chainId: \(chainId) is unavailable") + complete(ChainRegistryError.connectionUnavailable) + return + } + guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + logger?.error("Runtime metadata for chainId: \(chainId) is unavailable") + complete(ChainRegistryError.runtimeMetadaUnavailable) + return + } + + let fetchCrowdloansOperation = operationFactory.fetchCrowdloansOperation( + connection: connection, + runtimeService: runtimeService + ) + let contributionsFetchOperation = contributionsFetchOperation( + dependingOn: fetchCrowdloansOperation, + connection: connection, + runtimeService: runtimeService, + accountId: accountId + ) + let changesWrapper = createChangesOperationWrapper( + dependingOn: contributionsFetchOperation, + chainId: chainId, + accountId: accountId + ) + let saveOperation = createSaveOperation(dependingOn: changesWrapper) + + saveOperation.completionBlock = { + guard !saveOperation.isCancelled else { + return + } + + do { + try saveOperation.extractNoCancellableResultData() + self.syncOperationWrapper = nil + self.complete(nil) + } catch { + self.syncOperationWrapper = nil + self.complete(error) + } + } + + let operations = fetchCrowdloansOperation.allOperations + [contributionsFetchOperation, changesWrapper] + + let syncWrapper = CompoundOperationWrapper( + targetOperation: saveOperation, + dependencies: operations + ) + syncOperationWrapper = syncWrapper + operationManager.enqueue(operations: syncWrapper.allOperations, in: .transient) + } + + override func stopSyncUp() { + syncOperationWrapper?.cancel() + syncOperationWrapper = nil + } +} diff --git a/novawallet/Common/Services/CrowdloanService/RemoteCrowdloanContribution.swift b/novawallet/Common/Services/CrowdloanService/RemoteCrowdloanContribution.swift new file mode 100644 index 0000000000..a04bfdf35f --- /dev/null +++ b/novawallet/Common/Services/CrowdloanService/RemoteCrowdloanContribution.swift @@ -0,0 +1,4 @@ +struct RemoteCrowdloanContribution { + let crowdloan: Crowdloan + let contribution: CrowdloanContribution? +} diff --git a/novawallet/Common/Storage/EntityToModel/CrowdloanContributionDataMapper.swift b/novawallet/Common/Storage/EntityToModel/CrowdloanContributionDataMapper.swift new file mode 100644 index 0000000000..b3737bf704 --- /dev/null +++ b/novawallet/Common/Storage/EntityToModel/CrowdloanContributionDataMapper.swift @@ -0,0 +1,40 @@ +import Foundation +import RobinHood +import CoreData +import BigInt + +final class CrowdloanContributionDataMapper { + var entityIdentifierFieldName: String { #keyPath(CDCrowdloanContribution.identifier) } + + typealias DataProviderModel = CrowdloanContributionData + typealias CoreDataEntity = CDCrowdloanContribution +} + +extension CrowdloanContributionDataMapper: CoreDataMapperProtocol { + func populate( + entity: CoreDataEntity, + from model: DataProviderModel, + using _: NSManagedObjectContext + ) throws { + entity.identifier = model.identifier + entity.chainId = model.chainId + entity.paraId = Int32(model.paraId) + entity.source = model.source + entity.chainAccountId = model.accountId.toHex() + entity.amount = String(model.amount) + } + + func transform(entity: CoreDataEntity) throws -> DataProviderModel { + let accountId = try Data(hexString: entity.chainAccountId!) + let amount = entity.amount.map { BigUInt($0) ?? 0 } ?? 0 + let paraId = UInt32(entity.paraId) + + return .init( + accountId: accountId, + chainId: entity.chainId!, + paraId: paraId, + source: entity.source, + amount: amount + ) + } +} diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents index 158e8d3a0e..27afd0afe8 100644 --- a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents @@ -83,6 +83,14 @@ + + + + + + + + @@ -139,6 +147,7 @@ + @@ -150,6 +159,6 @@ - + \ No newline at end of file diff --git a/novawallet/Modules/AssetList/AssetListInteractor.swift b/novawallet/Modules/AssetList/AssetListInteractor.swift index f2c250f3d2..7661f84182 100644 --- a/novawallet/Modules/AssetList/AssetListInteractor.swift +++ b/novawallet/Modules/AssetList/AssetListInteractor.swift @@ -16,19 +16,24 @@ final class AssetListInteractor: AssetListBaseInteractor { } let nftLocalSubscriptionFactory: NftLocalSubscriptionFactoryProtocol + let crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol let eventCenter: EventCenterProtocol let settingsManager: SettingsManagerProtocol private var nftSubscription: StreamableProvider? private var nftChainIds: Set? + private var crowdloanChainIds = Set() private var assetLocksSubscriptions: [AccountId: StreamableProvider] = [:] private var locks: [ChainAssetId: [AssetLock]] = [:] + private var crowdloansSubscriptions: [ChainModel.Id: StreamableProvider] = [:] + private var crowdloans: [ChainModel.Id: [CrowdloanContributionData]] = [:] init( selectedWalletSettings: SelectedWalletSettings, chainRegistry: ChainRegistryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, nftLocalSubscriptionFactory: NftLocalSubscriptionFactoryProtocol, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, eventCenter: EventCenterProtocol, settingsManager: SettingsManagerProtocol, @@ -38,6 +43,7 @@ final class AssetListInteractor: AssetListBaseInteractor { self.nftLocalSubscriptionFactory = nftLocalSubscriptionFactory self.eventCenter = eventCenter self.settingsManager = settingsManager + self.crowdloansLocalSubscriptionFactory = crowdloansLocalSubscriptionFactory super.init( selectedWalletSettings: selectedWalletSettings, @@ -53,6 +59,7 @@ final class AssetListInteractor: AssetListBaseInteractor { clearAccountSubscriptions() clearNftSubscription() clearLocksSubscription() + clearCrowdloansSubscription() guard let selectedMetaAccount = selectedWalletSettings.value else { return } @@ -70,6 +77,7 @@ final class AssetListInteractor: AssetListBaseInteractor { updateAccountInfoSubscription(from: changes) setupNftSubscription(from: Array(availableChains.values)) updateLocksSubscription(from: changes) + setupCrowdloansSubscription(from: Array(availableChains.values)) } private func clearLocksSubscription() { @@ -102,6 +110,13 @@ final class AssetListInteractor: AssetListBaseInteractor { nftChainIds = nil } + private func clearCrowdloansSubscription() { + crowdloansSubscriptions.values.forEach { $0.removeObserver(self) } + crowdloansSubscriptions = [:] + crowdloans = [:] + crowdloanChainIds = .init() + } + override func applyChanges( allChanges: [DataProviderChange], accountDependentChanges: [DataProviderChange] @@ -111,6 +126,7 @@ final class AssetListInteractor: AssetListBaseInteractor { updateConnectionStatus(from: allChanges) setupNftSubscription(from: Array(availableChains.values)) updateLocksSubscription(from: allChanges) + setupCrowdloansSubscription(from: Array(availableChains.values)) } private func updateConnectionStatus(from changes: [DataProviderChange]) { @@ -143,6 +159,32 @@ final class AssetListInteractor: AssetListBaseInteractor { nftSubscription?.refresh() } + private func setupCrowdloansSubscription(from allChains: [ChainModel]) { + guard let selectedMetaAccount = selectedWalletSettings.value else { + return + } + let crowdloanChains = allChains.filter { $0.hasCrowdloans } + let newCrowdloanChainIds = Set(crowdloanChains.map(\.chainId)) + + guard !crowdloanChains.isEmpty, crowdloanChainIds != newCrowdloanChainIds else { + return + } + + clearCrowdloansSubscription() + crowdloanChainIds = newCrowdloanChainIds + + for chain in crowdloanChains { + guard let accountId = selectedMetaAccount.fetch( + for: chain.accountRequest() + )?.accountId else { + return + } + crowdloansSubscriptions[chain.identifier] = subscribeToCrowdloansProvider(for: accountId, chain: chain) + crowdloansSubscriptions[chain.identifier]?.refresh() + logger?.debug("Crowdloans for chain: \(chain.name) will refresh") + } + } + override func setup() { provideHidesZeroBalances() providerWalletInfo() @@ -165,6 +207,32 @@ final class AssetListInteractor: AssetListBaseInteractor { } } + override func handleAccountBalance( + result: Result<[DataProviderChange], Error>, + accountId: AccountId + ) { + super.handleAccountBalance(result: result, accountId: accountId) + + switch result { + case let .failure(error): + logger?.error(error.localizedDescription) + case let .success(changes): + var updatingChains = Set() + updatingChains = changes.reduce(into: updatingChains) { accum, change in + if let assetBalanceId = assetBalanceIdMapping[change.identifier], + crowdloanChainIds.contains(assetBalanceId.chainId), + assetBalanceId.accountId == accountId { + accum.insert(assetBalanceId.chainId) + } + } + + updatingChains.forEach { chainId in + crowdloansSubscriptions[chainId]?.refresh() + logger?.debug("Crowdloans for chain: \(chainId) will refresh") + } + } + } + override func handleAccountLocks(result: Result<[DataProviderChange], Error>, accountId: AccountId) { switch result { case let .success(changes): @@ -285,3 +353,41 @@ extension AssetListInteractor: EventVisitorProtocol { provideHidesZeroBalances() } } + +extension AssetListInteractor: CrowdloanContributionLocalSubscriptionHandler, CrowdloansLocalStorageSubscriber { + func handleCrowdloans( + result: Result<[DataProviderChange], Error>, + accountId: AccountId, + chain: ChainModel + ) { + guard let selectedMetaAccount = selectedWalletSettings.value else { + return + } + guard let chainAccountId = selectedMetaAccount.fetch( + for: chain.accountRequest() + )?.accountId, chainAccountId == accountId else { + logger?.warning("Crowdloans updates can't be handled because account for selected wallet for chain: \(chain.name) is different") + return + } + + switch result { + case let .failure(error): + presenter?.didReceiveCrowdloans(result: .failure(error)) + case let .success(changes): + crowdloans = changes.reduce( + into: crowdloans + ) { result, change in + switch change { + case let .insert(crowdloan), let .update(crowdloan): + var items = result[chain.chainId] ?? [] + items.addOrReplaceSingle(crowdloan) + result[chain.chainId] = items + case let .delete(deletedIdentifier): + result[chain.chainId]?.removeAll(where: { $0.identifier == deletedIdentifier }) + } + } + + presenter?.didReceiveCrowdloans(result: .success(crowdloans)) + } + } +} diff --git a/novawallet/Modules/AssetList/AssetListPresenter.swift b/novawallet/Modules/AssetList/AssetListPresenter.swift index fc1e8b3fef..208295cb33 100644 --- a/novawallet/Modules/AssetList/AssetListPresenter.swift +++ b/novawallet/Modules/AssetList/AssetListPresenter.swift @@ -21,6 +21,7 @@ final class AssetListPresenter: AssetListBasePresenter { private var hidesZeroBalances: Bool? private(set) var connectionStates: [ChainModel.Id: WebSocketEngine.State] = [:] private(set) var locksResult: Result<[AssetLock], Error>? + private(set) var crowdloansResult: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>? private var scheduler: SchedulerProtocol? @@ -55,6 +56,7 @@ final class AssetListPresenter: AssetListBasePresenter { walletIdenticon: walletIdenticon, walletType: walletType, prices: nil, + locks: nil, locale: selectedLocale ) @@ -70,75 +72,121 @@ final class AssetListPresenter: AssetListBasePresenter { ) } + typealias SuccessAssetListAssetAccountPrice = AssetListAssetAccountPrice + typealias FailedAssetListAssetAccountPrice = AssetListAssetAccountPrice + private func createAssetAccountPrice( + chainAssetId: ChainAssetId, + priceData: PriceData + ) -> Either? { + let chainId = chainAssetId.chainId + let assetId = chainAssetId.assetId + + guard let chain = allChains[chainId], + let asset = chain.assets.first(where: { $0.assetId == assetId }) else { + return nil + } + + guard case let .success(assetBalance) = balances[chainAssetId] else { + return .right( + AssetListAssetAccountPrice( + assetInfo: asset.displayInfo, + balance: 0, + price: priceData + ) + ) + } + + return .left( + AssetListAssetAccountPrice( + assetInfo: asset.displayInfo, + balance: assetBalance.totalInPlank, + price: priceData + )) + } + + private func createAssetAccountPriceLock( + chainAssetId: ChainAssetId, + priceData: PriceData + ) -> AssetListAssetAccountPrice? { + let chainId = chainAssetId.chainId + let assetId = chainAssetId.assetId + + guard let chain = allChains[chainId], + let asset = chain.assets.first(where: { $0.assetId == assetId }) else { + return nil + } + + guard case let .success(assetBalance) = balances[chainAssetId] else { + return nil + } + + return AssetListAssetAccountPrice( + assetInfo: asset.displayInfo, + balance: assetBalance.frozenInPlank, + price: priceData + ) + } + private func provideHeaderViewModel( with priceMapping: [ChainAssetId: PriceData], walletIdenticon: Data?, walletType: MetaAccountModelType, name: String ) { - let priceState: LoadableViewModelState<[AssetListAssetAccountPrice]> = priceMapping.reduce( - LoadableViewModelState.loaded(value: []) - ) { result, keyValue in - let chainAssetId = keyValue.key - let chainId = chainAssetId.chainId - let assetId = chainAssetId.assetId - switch result { + var locks: [AssetListAssetAccountPrice] = [] + var priceState: LoadableViewModelState<[AssetListAssetAccountPrice]> = .loaded(value: []) + + for (chainAssetId, priceData) in priceMapping { + switch priceState { case .loading: - return .loading + priceState = .loading case let .cached(items): - guard - let chain = allChains[chainId], - let asset = chain.assets.first(where: { $0.assetId == assetId }) else { - return .cached(value: items) + guard let newItem = createAssetAccountPrice( + chainAssetId: chainAssetId, + priceData: priceData + ) else { + priceState = .cached(value: items) + continue } - - let totalBalance: BigUInt - - if case let .success(assetBalance) = balanceResults[chainAssetId] { - totalBalance = assetBalance - } else { - totalBalance = 0 + priceState = .cached(value: items + [newItem.value]) + createAssetAccountPriceLock( + chainAssetId: chainAssetId, + priceData: priceData + ).map { + locks.append($0) } - - let newItem = AssetListAssetAccountPrice( - assetInfo: asset.displayInfo, - balance: totalBalance, - price: keyValue.value - ) - - return .cached(value: items + [newItem]) case let .loaded(items): - guard - let chain = allChains[chainId], - let asset = chain.assets.first(where: { $0.assetId == assetId }) else { - return .cached(value: items) + guard let newItem = createAssetAccountPrice( + chainAssetId: chainAssetId, + priceData: priceData + ) else { + priceState = .cached(value: items) + continue } - if case let .success(assetBalance) = balanceResults[chainAssetId] { - let newItem = AssetListAssetAccountPrice( - assetInfo: asset.displayInfo, - balance: assetBalance, - price: keyValue.value - ) - - return .loaded(value: items + [newItem]) - } else { - let newItem = AssetListAssetAccountPrice( - assetInfo: asset.displayInfo, - balance: 0, - price: keyValue.value - ) - - return .cached(value: items + [newItem]) + switch newItem { + case let .left(item): + priceState = .loaded(value: items + [item]) + case let .right(item): + priceState = .cached(value: items + [item]) + } + createAssetAccountPriceLock( + chainAssetId: chainAssetId, + priceData: priceData + ).map { + locks.append($0) } } } + let crowdloans = crowdloansModel(prices: priceMapping) + let totalLocks = locks + crowdloans let viewModel = viewModelFactory.createHeaderViewModel( from: name, walletIdenticon: walletIdenticon, walletType: walletType, - prices: priceState, + prices: priceState + crowdloans, + locks: totalLocks.isEmpty ? nil : totalLocks, locale: selectedLocale ) @@ -182,6 +230,30 @@ final class AssetListPresenter: AssetListBasePresenter { } } + private func crowdloansModel(prices: [ChainAssetId: PriceData]) -> [AssetListAssetAccountPrice] { + switch crowdloansResult { + case .failure, .none: + return [] + case let .success(crowdloans): + return crowdloans.compactMap { chainId, chainCrowdloans in + guard let chain = allChains[chainId] else { + return nil + } + guard let asset = chain.utilityAsset() else { + return nil + } + let chainAssetId = ChainAssetId(chainId: chainId, assetId: asset.assetId) + let price = prices[chainAssetId] ?? .zero() + + return AssetListAssetAccountPrice( + assetInfo: asset.displayInfo, + balance: chainCrowdloans.reduce(0) { $0 + $1.amount }, + price: price + ) + } + } + } + private func createGroupViewModel( from groupModel: AssetListGroupModel, maybePrices: [ChainAssetId: PriceData]?, @@ -347,7 +419,8 @@ extension AssetListPresenter: AssetListPresenterProtocol { func didTapTotalBalance() { guard let priceResult = priceResult, let prices = try? priceResult.get(), - let locks = try? locksResult?.get() else { + let locks = try? locksResult?.get(), + let crowdloans = try? crowdloansResult?.get() else { return } wireframe.showBalanceBreakdown( @@ -355,7 +428,8 @@ extension AssetListPresenter: AssetListPresenterProtocol { prices: prices, balances: balances.values.compactMap { try? $0.get() }, chains: allChains, - locks: locks + locks: locks, + crowdloans: crowdloans ) } } @@ -407,6 +481,10 @@ extension AssetListPresenter: AssetListInteractorOutputProtocol { func didReceiveLocks(result: Result<[AssetLock], Error>) { locksResult = result } + + func didReceiveCrowdloans(result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) { + crowdloansResult = result + } } extension AssetListPresenter: Localizable { diff --git a/novawallet/Modules/AssetList/AssetListProtocols.swift b/novawallet/Modules/AssetList/AssetListProtocols.swift index eb51d4462a..e8e22d41b7 100644 --- a/novawallet/Modules/AssetList/AssetListProtocols.swift +++ b/novawallet/Modules/AssetList/AssetListProtocols.swift @@ -34,6 +34,7 @@ protocol AssetListInteractorOutputProtocol: AssetListBaseInteractorOutputProtoco func didChange(name: String) func didReceive(hidesZeroBalances: Bool) func didReceiveLocks(result: Result<[AssetLock], Error>) + func didReceiveCrowdloans(result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) } protocol AssetListWireframeProtocol: AnyObject, WalletSwitchPresentable { @@ -53,6 +54,7 @@ protocol AssetListWireframeProtocol: AnyObject, WalletSwitchPresentable { prices: [ChainAssetId: PriceData], balances: [AssetBalance], chains: [ChainModel.Id: ChainModel], - locks: [AssetLock] + locks: [AssetLock], + crowdloans: [ChainModel.Id: [CrowdloanContributionData]] ) } diff --git a/novawallet/Modules/AssetList/AssetListViewController.swift b/novawallet/Modules/AssetList/AssetListViewController.swift index 3d744730d7..aea837afc2 100644 --- a/novawallet/Modules/AssetList/AssetListViewController.swift +++ b/novawallet/Modules/AssetList/AssetListViewController.swift @@ -93,7 +93,8 @@ extension AssetListViewController: UICollectionViewDelegateFlowLayout { sizeForItemAt indexPath: IndexPath ) -> CGSize { let cellType = AssetListFlowLayout.CellType(indexPath: indexPath) - return CGSize(width: collectionView.frame.width, height: cellType.height) + let cellHeight = rootView.collectionViewLayout.cellHeight(for: cellType) + return CGSize(width: collectionView.bounds.width, height: cellHeight) } func collectionView( @@ -350,6 +351,11 @@ extension AssetListViewController: AssetListViewProtocol { headerViewModel = viewModel rootView.collectionView.reloadData() + + let cellHeight = viewModel.locksAmount == nil ? + AssetListMeasurement.totalBalanceHeight : AssetListMeasurement.totalBalanceWithLocksHeight + + rootView.collectionViewLayout.updateTotalBalanceHeight(cellHeight) } func didReceiveGroups(state: AssetListGroupState) { diff --git a/novawallet/Modules/AssetList/AssetListViewFactory.swift b/novawallet/Modules/AssetList/AssetListViewFactory.swift index 099c9d2a57..2dbe4f4e2c 100644 --- a/novawallet/Modules/AssetList/AssetListViewFactory.swift +++ b/novawallet/Modules/AssetList/AssetListViewFactory.swift @@ -7,15 +7,18 @@ struct AssetListViewFactory { guard let currencyManager = CurrencyManager.shared else { return nil } + let interactor = AssetListInteractor( selectedWalletSettings: SelectedWalletSettings.shared, chainRegistry: ChainRegistryFacade.sharedRegistry, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, nftLocalSubscriptionFactory: NftLocalSubscriptionFactory.shared, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, eventCenter: EventCenter.shared, settingsManager: SettingsManager.shared, - currencyManager: currencyManager + currencyManager: currencyManager, + logger: Logger.shared ) let wireframe = AssetListWireframe(walletUpdater: WalletDetailsUpdater.shared) diff --git a/novawallet/Modules/AssetList/AssetListViewLayout.swift b/novawallet/Modules/AssetList/AssetListViewLayout.swift index 0983d5d723..f927aa2660 100644 --- a/novawallet/Modules/AssetList/AssetListViewLayout.swift +++ b/novawallet/Modules/AssetList/AssetListViewLayout.swift @@ -3,8 +3,10 @@ import UIKit final class AssetListViewLayout: UIView { let backgroundView = MultigradientView.background - let collectionView: UICollectionView = { - let flowLayout = AssetListFlowLayout() + let collectionViewLayout = AssetListFlowLayout() + + lazy var collectionView: UICollectionView = { + let flowLayout = collectionViewLayout flowLayout.scrollDirection = .vertical flowLayout.minimumLineSpacing = 0 flowLayout.minimumInteritemSpacing = 0 diff --git a/novawallet/Modules/AssetList/AssetListWireframe.swift b/novawallet/Modules/AssetList/AssetListWireframe.swift index 5919768a1f..ba1899b3d8 100644 --- a/novawallet/Modules/AssetList/AssetListWireframe.swift +++ b/novawallet/Modules/AssetList/AssetListWireframe.swift @@ -66,14 +66,16 @@ final class AssetListWireframe: AssetListWireframeProtocol { prices: [ChainAssetId: PriceData], balances: [AssetBalance], chains: [ChainModel.Id: ChainModel], - locks: [AssetLock] + locks: [AssetLock], + crowdloans: [ChainModel.Id: [CrowdloanContributionData]] ) { guard let viewController = LocksViewFactory.createView(input: .init( prices: prices, balances: balances, chains: chains, - locks: locks + locks: locks, + crowdloans: crowdloans )) else { return } diff --git a/novawallet/Modules/AssetList/Models/Either.swift b/novawallet/Modules/AssetList/Models/Either.swift new file mode 100644 index 0000000000..ed329dfd60 --- /dev/null +++ b/novawallet/Modules/AssetList/Models/Either.swift @@ -0,0 +1,16 @@ +// https://github.com/apple/swift/blob/main/stdlib/public/core/EitherSequence.swift +enum Either { + case left(Left) + case right(Right) +} + +extension Either where Left == Right { + var value: Left { + switch self { + case let .left(left): + return left + case let .right(right): + return right + } + } +} diff --git a/novawallet/Modules/AssetList/Models/LoadableViewModelState+Addition.swift b/novawallet/Modules/AssetList/Models/LoadableViewModelState+Addition.swift new file mode 100644 index 0000000000..9824de6c7b --- /dev/null +++ b/novawallet/Modules/AssetList/Models/LoadableViewModelState+Addition.swift @@ -0,0 +1,12 @@ +extension LoadableViewModelState { + static func + (lhs: LoadableViewModelState<[T]>, rhs: [T]) -> LoadableViewModelState<[T]> { + switch lhs { + case let .cached(items): + return .cached(value: items + rhs) + case let .loaded(items): + return .loaded(value: items + rhs) + case .loading: + return lhs + } + } +} diff --git a/novawallet/Modules/AssetList/View/AssetListFlowLayout.swift b/novawallet/Modules/AssetList/View/AssetListFlowLayout.swift index cb8e6bd6a5..d74d30064e 100644 --- a/novawallet/Modules/AssetList/View/AssetListFlowLayout.swift +++ b/novawallet/Modules/AssetList/View/AssetListFlowLayout.swift @@ -2,7 +2,8 @@ import UIKit enum AssetListMeasurement { static let accountHeight: CGFloat = 56.0 - static let totalBalanceHeight: CGFloat = 96.0 + static let totalBalanceHeight: CGFloat = 103.0 + static let totalBalanceWithLocksHeight: CGFloat = 133.0 static let settingsHeight: CGFloat = 56.0 static let nftsHeight = 56.0 static let assetHeight: CGFloat = 56.0 @@ -13,6 +14,7 @@ enum AssetListMeasurement { final class AssetListFlowLayout: UICollectionViewFlowLayout { static let assetGroupDecoration = "assetGroupDecoration" + private var totalBalanceHeight: CGFloat = AssetListMeasurement.totalBalanceHeight enum SectionType: CaseIterable { case summary @@ -123,23 +125,6 @@ final class AssetListFlowLayout: UICollectionViewFlowLayout { return IndexPath(item: itemIndex, section: sectionIndex) } } - - var height: CGFloat { - switch self { - case .account: - return AssetListMeasurement.accountHeight - case .totalBalance: - return AssetListMeasurement.totalBalanceHeight - case .yourNfts: - return AssetListMeasurement.nftsHeight - case .settings: - return AssetListMeasurement.settingsHeight - case .emptyState: - return AssetListMeasurement.emptyStateCellHeight - case .asset: - return AssetListMeasurement.assetHeight - } - } } private var itemsDecorationAttributes: [UICollectionViewLayoutAttributes] = [] @@ -193,7 +178,7 @@ final class AssetListFlowLayout: UICollectionViewFlowLayout { if hasSummarySection { groupY = AssetListMeasurement.accountHeight + SectionType.summary.cellSpacing + - AssetListMeasurement.totalBalanceHeight + totalBalanceHeight } groupY += SectionType.summary.insets.top + SectionType.summary.insets.bottom @@ -203,7 +188,7 @@ final class AssetListFlowLayout: UICollectionViewFlowLayout { let hasNfts = collectionView.numberOfItems(inSection: SectionType.nfts.index) > 0 if hasNfts { - groupY += CellType.yourNfts.height + groupY += AssetListMeasurement.nftsHeight } groupY += SectionType.settings.insets.top + AssetListMeasurement.settingsHeight + @@ -245,4 +230,29 @@ final class AssetListFlowLayout: UICollectionViewFlowLayout { itemsDecorationAttributes = attributes } + + func updateTotalBalanceHeight(_ height: CGFloat) { + guard height != totalBalanceHeight else { + return + } + totalBalanceHeight = height + invalidateLayout() + } + + func cellHeight(for type: CellType) -> CGFloat { + switch type { + case .account: + return AssetListMeasurement.accountHeight + case .totalBalance: + return totalBalanceHeight + case .yourNfts: + return AssetListMeasurement.nftsHeight + case .settings: + return AssetListMeasurement.settingsHeight + case .emptyState: + return AssetListMeasurement.emptyStateCellHeight + case .asset: + return AssetListMeasurement.assetHeight + } + } } diff --git a/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift b/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift index 9239aee052..0e24f4fd89 100644 --- a/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift +++ b/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift @@ -3,7 +3,7 @@ import SoraUI final class AssetListTotalBalanceCell: UICollectionViewCell { private enum Constants { - static let bottomInset: CGFloat = 16.0 + static let bottomInset: CGFloat = 20.0 } let backgroundBlurView: TriangularedBlurView = { @@ -37,6 +37,16 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { return view }() + let locksView: BorderedIconLabelView = .create { + let color = R.color.colorWhite64()! + $0.iconDetailsView.imageView.image = R.image.iconBrowserSecurity()?.withTintColor(color) + $0.iconDetailsView.detailsLabel.font = .regularFootnote + $0.iconDetailsView.detailsLabel.textColor = color + $0.iconDetailsView.spacing = 4.0 + $0.contentInsets = UIEdgeInsets(top: 2, left: 6, bottom: 2, right: 6) + $0.isHidden = true + } + private var skeletonView: SkrullableView? var locale = Locale.current { @@ -71,11 +81,12 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { switch viewModel.amount { case let .loaded(value), let .cached(value): amountLabel.text = value - + locksView.iconDetailsView.detailsLabel.text = viewModel.locksAmount + locksView.isHidden = viewModel.locksAmount == nil stopLoadingIfNeeded() case .loading: amountLabel.text = "" - + locksView.isHidden = true startLoadingIfNeeded() } } @@ -96,11 +107,20 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { contentView.addSubview(titleView) titleView.snp.makeConstraints { make in make.centerX.equalToSuperview() - make.top.equalTo(backgroundBlurView.snp.top).offset(16.0) + make.top.equalTo(backgroundBlurView.snp.top).offset(20.0) } - contentView.addSubview(amountLabel) - amountLabel.snp.makeConstraints { make in + let amountView = UIStackView(arrangedSubviews: [ + amountLabel, + locksView + ]) + amountView.spacing = 8.0 + amountView.axis = .vertical + amountView.alignment = .center + + contentView.addSubview(amountView) + amountView.snp.makeConstraints { make in + make.top.greaterThanOrEqualTo(titleView.snp.bottom).offset(3) make.leading.equalTo(backgroundBlurView).offset(8.0) make.trailing.equalTo(backgroundBlurView).offset(-8.0) make.bottom.equalToSuperview().inset(Constants.bottomInset) diff --git a/novawallet/Modules/AssetList/ViewModel/AssetListViewModel.swift b/novawallet/Modules/AssetList/ViewModel/AssetListViewModel.swift index 12b2f0045f..42749adc7e 100644 --- a/novawallet/Modules/AssetList/ViewModel/AssetListViewModel.swift +++ b/novawallet/Modules/AssetList/ViewModel/AssetListViewModel.swift @@ -15,6 +15,7 @@ enum ValueDirection { struct AssetListHeaderViewModel { let title: String let amount: LoadableViewModelState + let locksAmount: String? let walletSwitch: WalletSwitchViewModel } diff --git a/novawallet/Modules/AssetList/ViewModel/AssetListViewModelFactory.swift b/novawallet/Modules/AssetList/ViewModel/AssetListViewModelFactory.swift index 347d947b75..f345dd1865 100644 --- a/novawallet/Modules/AssetList/ViewModel/AssetListViewModelFactory.swift +++ b/novawallet/Modules/AssetList/ViewModel/AssetListViewModelFactory.swift @@ -15,6 +15,7 @@ protocol AssetListViewModelFactoryProtocol: AssetListAssetViewModelFactoryProtoc walletIdenticon: Data?, walletType: MetaAccountModelType, prices: LoadableViewModelState<[AssetListAssetAccountPrice]>?, + locks: [AssetListAssetAccountPrice]?, locale: Locale ) -> AssetListHeaderViewModel @@ -84,6 +85,7 @@ extension AssetListViewModelFactory: AssetListViewModelFactoryProtocol { walletIdenticon: Data?, walletType: MetaAccountModelType, prices: LoadableViewModelState<[AssetListAssetAccountPrice]>?, + locks: [AssetListAssetAccountPrice]?, locale: Locale ) -> AssetListHeaderViewModel { let icon = walletIdenticon.flatMap { try? iconGenerator.generateFromAccountId($0) } @@ -97,12 +99,14 @@ extension AssetListViewModelFactory: AssetListViewModelFactoryProtocol { return AssetListHeaderViewModel( title: title, amount: totalPrice, + locksAmount: locks.map { formatTotalPrice(from: $0, locale: locale) }, walletSwitch: walletSwitch ) } else { return AssetListHeaderViewModel( title: title, amount: .loading, + locksAmount: nil, walletSwitch: walletSwitch ) } diff --git a/novawallet/Modules/Crowdloan/Operation/ExternalContibution/AcalaContributionSource.swift b/novawallet/Modules/Crowdloan/Operation/ExternalContibution/AcalaContributionSource.swift index ada8bc2e8b..838d2ee047 100644 --- a/novawallet/Modules/Crowdloan/Operation/ExternalContibution/AcalaContributionSource.swift +++ b/novawallet/Modules/Crowdloan/Operation/ExternalContibution/AcalaContributionSource.swift @@ -5,6 +5,7 @@ import BigInt final class AcalaContributionSource: ExternalContributionSourceProtocol { static let baseUrl = URL(string: "https://crowdloan.aca-api.network")! static let apiContribution = "/contribution" + var sourceName: String { "Acala Liquid" } let paraIdOperationFactory: ParaIdOperationFactoryProtocol let acalaChainId: ChainModel.Id @@ -37,7 +38,7 @@ final class AcalaContributionSource: ExternalContributionSourceProtocol { let paraIdWrapper = paraIdOperationFactory.createParaIdOperation(for: acalaChainId) - let mergeOperation = ClosureOperation<[ExternalContribution]> { + let mergeOperation = ClosureOperation<[ExternalContribution]> { [sourceName] in let response = try networkOperation.extractNoCancellableResultData() let paraId = try paraIdWrapper.targetOperation.extractNoCancellableResultData() @@ -45,7 +46,7 @@ final class AcalaContributionSource: ExternalContributionSourceProtocol { throw CrowdloanBonusServiceError.internalError } - return [ExternalContribution(source: "Liquid", amount: amount, paraId: paraId)] + return [ExternalContribution(source: sourceName, amount: amount, paraId: paraId)] } let dependencies = [networkOperation] + paraIdWrapper.allOperations diff --git a/novawallet/Modules/Crowdloan/Operation/ExternalContibution/ExternalContributionSource.swift b/novawallet/Modules/Crowdloan/Operation/ExternalContibution/ExternalContributionSource.swift index 0ea5ea9e5d..99f8635ab9 100644 --- a/novawallet/Modules/Crowdloan/Operation/ExternalContibution/ExternalContributionSource.swift +++ b/novawallet/Modules/Crowdloan/Operation/ExternalContibution/ExternalContributionSource.swift @@ -3,6 +3,7 @@ import RobinHood import SoraKeystore protocol ExternalContributionSourceProtocol { + var sourceName: String { get } func getContributions(accountId: AccountId, chain: ChainModel) -> CompoundOperationWrapper<[ExternalContribution]> } diff --git a/novawallet/Modules/Crowdloan/Operation/ExternalContibution/ParallelContributionSource.swift b/novawallet/Modules/Crowdloan/Operation/ExternalContibution/ParallelContributionSource.swift index f5ac1b2902..3556438b81 100644 --- a/novawallet/Modules/Crowdloan/Operation/ExternalContibution/ParallelContributionSource.swift +++ b/novawallet/Modules/Crowdloan/Operation/ExternalContibution/ParallelContributionSource.swift @@ -3,6 +3,7 @@ import RobinHood final class ParallelContributionSource: ExternalContributionSourceProtocol { static let baseURL = URL(string: "https://auction-service-prod.parallel.fi/crowdloan/rewards")! + var sourceName: String { "Parallel" } func getContributions(accountId: AccountId, chain: ChainModel) -> CompoundOperationWrapper<[ExternalContribution]> { guard let accountAddress = try? accountId.toAddress(using: chain.chainFormat) else { @@ -19,13 +20,13 @@ final class ParallelContributionSource: ExternalContributionSourceProtocol { return request } - let resultFactory = AnyNetworkResultFactory<[ExternalContribution]> { data in + let resultFactory = AnyNetworkResultFactory<[ExternalContribution]> { [sourceName] data in let resultData = try JSONDecoder().decode( [ParallelContributionResponse].self, from: data ) - return resultData.map { ExternalContribution(source: "Parallel", amount: $0.amount, paraId: $0.paraId) } + return resultData.map { ExternalContribution(source: sourceName, amount: $0.amount, paraId: $0.paraId) } } let operation = NetworkOperation(requestFactory: requestFactory, resultFactory: resultFactory) diff --git a/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift index 7c12b608e8..a884f324b3 100644 --- a/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift +++ b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift @@ -6,6 +6,7 @@ protocol LocksBalanceViewModelFactoryProtocol { balances: [AssetBalance], chains: [ChainModel.Id: ChainModel], prices: [ChainAssetId: PriceData], + crowdloans: [ChainModel.Id: [CrowdloanContributionData]], locale: Locale ) -> FormattedBalance func formatPlankValue( @@ -51,6 +52,7 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { balances: [AssetBalance], chains: [ChainModel.Id: ChainModel], prices: [ChainAssetId: PriceData], + crowdloans: [ChainModel.Id: [CrowdloanContributionData]], locale: Locale ) -> FormattedBalance { var totalPrice: Decimal = 0 @@ -88,9 +90,30 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { lastPriceData = priceData } - let formattedTotal = formatPrice(amount: totalPrice, priceData: lastPriceData, locale: locale) + let crowdloansTotalPrice: Decimal = crowdloans.reduce(0) { result, crowdloan in + guard let asset = chains[crowdloan.key]?.utilityAsset() else { + return result + } + let priceData = prices[.init(chainId: crowdloan.key, assetId: asset.assetId)] + let rate = priceData.map { Decimal(string: $0.price) ?? 0 } ?? 0 + return result + calculateAmount( + from: crowdloan.value.reduce(0) { $0 + $1.amount }, + precision: asset.precision, + rate: rate + ) + } + + let formattedTotal = formatPrice( + amount: totalPrice + crowdloansTotalPrice, + priceData: lastPriceData, + locale: locale + ) let formattedTransferrable = formatPrice(amount: transferrablePrice, priceData: lastPriceData, locale: locale) - let formattedLocks = formatPrice(amount: locksPrice, priceData: lastPriceData, locale: locale) + let formattedLocks = formatPrice( + amount: locksPrice + crowdloansTotalPrice, + priceData: lastPriceData, + locale: locale + ) return .init( total: formattedTotal, transferrable: formattedTransferrable, @@ -108,13 +131,12 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { prices: [ChainAssetId: PriceData], locale: Locale ) -> FormattedPlank? { - guard let priceData = prices[chainAssetId] else { - return nil - } guard let assetPrecision = chains[chainAssetId.chainId]?.asset(for: chainAssetId.assetId)?.precision else { return nil } - let rate = Decimal(string: priceData.price) ?? 0.0 + let priceData = prices[chainAssetId] + + let rate = priceData.map { Decimal(string: $0.price) ?? 0 } ?? 0 let price = calculateAmount( from: plank, @@ -122,10 +144,6 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { rate: rate ) - guard price > 0 else { - return nil - } - let formattedPrice = formatPrice(amount: price, priceData: priceData, locale: locale) return .init(amount: formattedPrice, price: price) } diff --git a/novawallet/Modules/Locks/LocksPresenter.swift b/novawallet/Modules/Locks/LocksPresenter.swift index 00284d8a14..bc96b0d947 100644 --- a/novawallet/Modules/Locks/LocksPresenter.swift +++ b/novawallet/Modules/Locks/LocksPresenter.swift @@ -30,6 +30,7 @@ final class LocksPresenter { balances: input.balances, chains: input.chains, prices: input.prices, + crowdloans: input.crowdloans, locale: selectedLocale ) @@ -104,7 +105,21 @@ final class LocksPresenter { ) } - return locksCells + reservedCells + let crowdloanCells: [LocksViewSectionModel.CellViewModel] = input.crowdloans.compactMap { + guard let utilityAsset = input.chains[$0.key]?.utilityAsset() else { + return nil + } + return createCell( + amountInPlank: $0.value.reduce(0) { $0 + $1.amount }, + chainAssetId: ChainAssetId(chainId: $0.key, assetId: utilityAsset.assetId), + title: R.string.localizable.tabbarCrowdloanTitle( + preferredLanguages: selectedLocale.rLanguages + ), + identifier: $0.key + ) + } + + return locksCells + reservedCells + crowdloanCells } private func createCell( @@ -113,6 +128,9 @@ final class LocksPresenter { title: String, identifier: String ) -> LocksViewSectionModel.CellViewModel? { + guard amountInPlank > 0 else { + return nil + } guard let chain = input.chains[chainAssetId.chainId] else { return nil } @@ -141,12 +159,18 @@ final class LocksPresenter { var contentHeight: CGFloat { let reservedCellsCount = input.balances.filter { - $0.reservedInPlank > 0 && input.prices[$0.chainAssetId] != nil + $0.reservedInPlank > 0 }.count let locksCellsCount = input.locks.filter { - $0.amount > 0 && input.prices[$0.chainAssetId] != nil + $0.amount > 0 + }.count + let crowdloanCellsCount = input.crowdloans.filter { crowdloan in + crowdloan.value.first(where: { $0.amount > 0 }) != nil }.count - return view?.calculateEstimatedHeight(sections: 2, items: locksCellsCount + reservedCellsCount) ?? 0 + return view?.calculateEstimatedHeight( + sections: 2, + items: locksCellsCount + reservedCellsCount + crowdloanCellsCount + ) ?? 0 } } diff --git a/novawallet/Modules/Locks/LocksViewInput.swift b/novawallet/Modules/Locks/LocksViewInput.swift index 99bf4f9ec1..8154f2c3ea 100644 --- a/novawallet/Modules/Locks/LocksViewInput.swift +++ b/novawallet/Modules/Locks/LocksViewInput.swift @@ -3,4 +3,5 @@ struct LocksViewInput { let balances: [AssetBalance] let chains: [ChainModel.Id: ChainModel] let locks: [AssetLock] + let crowdloans: [ChainModel.Id: [CrowdloanContributionData]] } From c2f6c3b0a279d81a47c7827d891511d2c535ffc2 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 20 Sep 2022 11:49:02 +0400 Subject: [PATCH 07/52] update ui --- .../NSCollectionLayoutSection+create.swift | 6 ++- .../Currency/View/CurrencyViewLayout.swift | 1 + .../Locks/LocksBalanceViewModelFactory.swift | 40 ++++++++++++++++--- novawallet/Modules/Locks/LocksPresenter.swift | 7 ++-- novawallet/Modules/Locks/LocksProtocols.swift | 5 ++- .../Modules/Locks/LocksViewController.swift | 7 +++- .../Modules/Locks/LocksViewLayout.swift | 4 +- .../Locks/View/LockCollectionViewCell.swift | 32 ++++++++++----- .../GenericCollectionViewLayout.swift | 2 + 9 files changed, 81 insertions(+), 23 deletions(-) diff --git a/novawallet/Common/Extension/UIKit/NSCollectionLayoutSection+create.swift b/novawallet/Common/Extension/UIKit/NSCollectionLayoutSection+create.swift index ccf4840a38..e55674ee7e 100644 --- a/novawallet/Common/Extension/UIKit/NSCollectionLayoutSection+create.swift +++ b/novawallet/Common/Extension/UIKit/NSCollectionLayoutSection+create.swift @@ -3,6 +3,7 @@ import UIKit extension NSCollectionLayoutSection { struct Settings { let estimatedRowHeight: CGFloat + let absoluteHeaderHeight: CGFloat? let estimatedHeaderHeight: CGFloat let sectionContentInsets: NSDirectionalEdgeInsets let sectionInterGroupSpacing: CGFloat @@ -28,7 +29,10 @@ extension NSCollectionLayoutSection { let headerSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), - heightDimension: .estimated(settings.estimatedHeaderHeight) + heightDimension: + settings.absoluteHeaderHeight.map { + .absolute($0) + } ?? .estimated(settings.estimatedHeaderHeight) ) let section = NSCollectionLayoutSection(group: group) section.contentInsets = settings.sectionContentInsets diff --git a/novawallet/Modules/Currency/View/CurrencyViewLayout.swift b/novawallet/Modules/Currency/View/CurrencyViewLayout.swift index a8f0b4dfe5..453a73ac0b 100644 --- a/novawallet/Modules/Currency/View/CurrencyViewLayout.swift +++ b/novawallet/Modules/Currency/View/CurrencyViewLayout.swift @@ -34,6 +34,7 @@ final class CurrencyViewLayout: UIView { private func createCompositionalLayout() -> UICollectionViewCompositionalLayout { let settings = NSCollectionLayoutSection.Settings( estimatedRowHeight: Constants.estimatedRowHeight, + absoluteHeaderHeight: nil, estimatedHeaderHeight: Constants.estimatedHeaderHeight, sectionContentInsets: Constants.sectionContentInsets, sectionInterGroupSpacing: Constants.interGroupSpacing, diff --git a/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift index a884f324b3..0d91feebd8 100644 --- a/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift +++ b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift @@ -30,7 +30,8 @@ struct FormattedBalance { struct FormattedPlank { let amount: String - let price: Decimal + let price: String? + let priceValue: Decimal } final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { @@ -131,9 +132,11 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { prices: [ChainAssetId: PriceData], locale: Locale ) -> FormattedPlank? { - guard let assetPrecision = chains[chainAssetId.chainId]?.asset(for: chainAssetId.assetId)?.precision else { + guard let assetPrecision = chains[chainAssetId.chainId]?.asset(for: chainAssetId.assetId)?.precision, + let utilityAsset = chains[chainAssetId.chainId]?.utilityAsset() else { return nil } + let priceData = prices[chainAssetId] let rate = priceData.map { Decimal(string: $0.price) ?? 0 } ?? 0 @@ -144,16 +147,34 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { rate: rate ) + let amount = calculateAmount( + from: plank, + precision: utilityAsset.precision, + rate: nil + ) + let formattedAmount = formatAmount( + amount, + assetDisplayInfo: utilityAsset.displayInfo, + locale: locale + ) + let formattedPrice = formatPrice(amount: price, priceData: priceData, locale: locale) - return .init(amount: formattedPrice, price: price) + return .init( + amount: formattedAmount, + price: formattedPrice, + priceValue: price + ) } - private func calculateAmount(from plank: BigUInt, precision: UInt16, rate: Decimal) -> Decimal { + private func calculateAmount(from plank: BigUInt, precision: UInt16, rate: Decimal?) -> Decimal { let amount = Decimal.fromSubstrateAmount( plank, precision: Int16(precision) ) ?? 0.0 - return amount * rate + + return rate.map { + amount * $0 + } ?? amount } private func formatPrice(amount: Decimal, priceData: PriceData?, locale: Locale) -> String { @@ -162,4 +183,13 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { let priceFormatter = assetFormatterFactory.createTokenFormatter(for: assetDisplayInfo) return priceFormatter.value(for: locale).stringFromDecimal(amount) ?? "" } + + private func formatAmount( + _ amount: Decimal, + assetDisplayInfo: AssetBalanceDisplayInfo, + locale: Locale + ) -> String { + let priceFormatter = assetFormatterFactory.createTokenFormatter(for: assetDisplayInfo) + return priceFormatter.value(for: locale).stringFromDecimal(amount) ?? "" + } } diff --git a/novawallet/Modules/Locks/LocksPresenter.swift b/novawallet/Modules/Locks/LocksPresenter.swift index bc96b0d947..e01553c186 100644 --- a/novawallet/Modules/Locks/LocksPresenter.swift +++ b/novawallet/Modules/Locks/LocksPresenter.swift @@ -67,7 +67,7 @@ final class LocksPresenter { balanceModel.locksPrice / balanceModel.totalPrice : 0 let displayPercent = formatter.stringFromDecimal(percent) ?? "" let locksCells = createLocksCells().sorted { - $0.price > $1.price + $0.priceValue > $1.priceValue } return LocksViewSectionModel( @@ -152,8 +152,9 @@ final class LocksPresenter { return LocksViewSectionModel.CellViewModel( id: identifier, title: title, - value: value.amount, - price: value.price + amount: value.amount, + price: value.price, + priceValue: value.priceValue ) } diff --git a/novawallet/Modules/Locks/LocksProtocols.swift b/novawallet/Modules/Locks/LocksProtocols.swift index eca08b3ecf..0a4cbef99a 100644 --- a/novawallet/Modules/Locks/LocksProtocols.swift +++ b/novawallet/Modules/Locks/LocksProtocols.swift @@ -29,7 +29,8 @@ struct LocksViewSectionModel: SectionProtocol, Hashable { struct CellViewModel: Hashable { let id: String let title: String - let value: String - let price: Decimal + let amount: String + let price: String? + let priceValue: Decimal } } diff --git a/novawallet/Modules/Locks/LocksViewController.swift b/novawallet/Modules/Locks/LocksViewController.swift index 0ca31ac287..5fe10c082c 100644 --- a/novawallet/Modules/Locks/LocksViewController.swift +++ b/novawallet/Modules/Locks/LocksViewController.swift @@ -52,7 +52,12 @@ final class LocksViewController: UIViewController, ViewHolder, ModalSheetCollect collectionView: rootView.collectionView, cellProvider: { collectionView, indexPath, model -> UICollectionViewCell? in let cell: LockCollectionViewCell? = collectionView.dequeueReusableCell(for: indexPath) - cell?.bind(title: model.title, value: model.value) + cell?.bind(viewModel: .init( + title: model.title, + amount: model.amount, + price: model.price + ) + ) return cell } ) diff --git a/novawallet/Modules/Locks/LocksViewLayout.swift b/novawallet/Modules/Locks/LocksViewLayout.swift index 46d92ed741..292a82779b 100644 --- a/novawallet/Modules/Locks/LocksViewLayout.swift +++ b/novawallet/Modules/Locks/LocksViewLayout.swift @@ -14,8 +14,8 @@ final class LocksViewLayout: GenericCollectionViewLayout( + lazy var view = GenericTitleValueView( titleView: titleLabel, valueView: valueLabel ) private let titleLabel: UILabel = .create { - $0.font = .regularSubheadline - $0.textColor = R.color.colorWhite48() + $0.font = .regularFootnote + $0.textColor = R.color.colorWhite64() } - private let valueLabel: UILabel = .create { - $0.font = .regularSubheadline - $0.textColor = R.color.colorWhite48() + private let valueLabel: MultiValueView = .create { + $0.valueTop.font = .regularFootnote + $0.valueBottom.font = .caption1 + $0.valueTop.textColor = R.color.colorWhite64() + $0.valueBottom.textColor = R.color.colorWhite64() } override init(frame: CGRect) { @@ -26,15 +28,27 @@ final class LockCollectionViewCell: UICollectionViewCell { fatalError("init(coder:) has not been implemented") } + override var intrinsicContentSize: CGSize { + CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) + } + private func setupLayout() { contentView.addSubview(view) view.snp.makeConstraints { $0.edges.equalToSuperview().inset(UIEdgeInsets(top: 14, left: 24, bottom: 14, right: 0)) } } +} + +extension LockCollectionViewCell { + struct Model { + let title: String + let amount: String + let price: String? + } - func bind(title: String, value: String) { - view.titleView.text = title - view.valueView.text = value + func bind(viewModel: Model) { + view.titleView.text = viewModel.title + view.valueView.bind(topValue: viewModel.amount, bottomValue: viewModel.price) } } diff --git a/novawallet/Modules/YourWallets/GenericCollectionViewLayout.swift b/novawallet/Modules/YourWallets/GenericCollectionViewLayout.swift index c8b4cf5cb8..6e24c6852f 100644 --- a/novawallet/Modules/YourWallets/GenericCollectionViewLayout.swift +++ b/novawallet/Modules/YourWallets/GenericCollectionViewLayout.swift @@ -64,6 +64,7 @@ class GenericCollectionViewLayout: UIView { .createSectionLayoutWithFullWidthRow(settings: .init( estimatedRowHeight: settings.estimatedRowHeight, + absoluteHeaderHeight: settings.absoluteHeaderHeight, estimatedHeaderHeight: settings.estimatedSectionHeaderHeight, sectionContentInsets: settings.sectionContentInsets, sectionInterGroupSpacing: settings.interGroupSpacing, @@ -79,6 +80,7 @@ struct GenericCollectionViewLayoutSettings { var pinToVisibleBounds: Bool = true var estimatedHeaderHeight: CGFloat = 36 var estimatedRowHeight: CGFloat = 56 + var absoluteHeaderHeight: CGFloat? var estimatedSectionHeaderHeight: CGFloat = 46 var sectionContentInsets = NSDirectionalEdgeInsets( top: 0, From 9602a00bfbccfe0cf98903539d1fecc6838dc1ad Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 20 Sep 2022 15:52:33 +0400 Subject: [PATCH 08/52] move sync to service --- ...ContributionLocalSubscriptionFactory.swift | 15 +++- .../Common/Services/BaseSyncService.swift | 26 +++++-- ...rowdloanContributionStreamableSource.swift | 76 +++++++++++++++++-- .../AssetList/AssetListInteractor.swift | 2 - 4 files changed, 105 insertions(+), 14 deletions(-) diff --git a/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift index 2afbb55b37..fee63a31d4 100644 --- a/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift +++ b/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift @@ -12,6 +12,7 @@ final class CrowdloanContributionLocalSubscriptionFactory: SubstrateLocalSubscri CrowdloanContributionLocalSubscriptionFactoryProtocol { let operationFactory: CrowdloanOperationFactoryProtocol let paraIdOperationFactory: ParaIdOperationFactoryProtocol + let eventCenter: EventCenterProtocol init( operationFactory: CrowdloanOperationFactoryProtocol, @@ -19,10 +20,12 @@ final class CrowdloanContributionLocalSubscriptionFactory: SubstrateLocalSubscri chainRegistry: ChainRegistryProtocol, storageFacade: StorageFacadeProtocol, paraIdOperationFactory: ParaIdOperationFactoryProtocol, + eventCenter: EventCenterProtocol, logger: LoggerProtocol ) { self.operationFactory = operationFactory self.paraIdOperationFactory = paraIdOperationFactory + self.eventCenter = eventCenter super.init( chainRegistry: chainRegistry, @@ -59,7 +62,14 @@ final class CrowdloanContributionLocalSubscriptionFactory: SubstrateLocalSubscri let syncServices = [onChainSyncService] + offChainSyncServices - let source = CrowdloanContributionStreamableSource(syncServices: syncServices) + let source = CrowdloanContributionStreamableSource( + syncServices: syncServices, + chainId: chain.chainId, + accountId: accountId, + eventCenter: eventCenter + ) + + let selfUpdatingSource = CrowdloanContributionStreamableSourceWrapper(source: source) let crowdloansFilter = NSPredicate.crowdloanContribution( for: chain.chainId, @@ -89,7 +99,7 @@ final class CrowdloanContributionLocalSubscriptionFactory: SubstrateLocalSubscri } let provider = StreamableProvider( - source: AnyStreamableSource(source), + source: AnyStreamableSource(selfUpdatingSource), repository: AnyDataProviderRepository(repository), observable: AnyDataProviderRepositoryObservable(observable), operationManager: operationManager @@ -168,6 +178,7 @@ extension CrowdloanContributionLocalSubscriptionFactory { chainRegistry: ChainRegistryFacade.sharedRegistry, storageFacade: SubstrateDataStorageFacade.shared, paraIdOperationFactory: ParaIdOperationFactory.shared, + eventCenter: EventCenter.shared, logger: Logger.shared ) } diff --git a/novawallet/Common/Services/BaseSyncService.swift b/novawallet/Common/Services/BaseSyncService.swift index 97560a8398..3fd5f78d10 100644 --- a/novawallet/Common/Services/BaseSyncService.swift +++ b/novawallet/Common/Services/BaseSyncService.swift @@ -3,14 +3,11 @@ import RobinHood import SubstrateSdk protocol SyncServiceProtocol { - func performSyncUp() + func syncUp() func stopSyncUp() - func setup() - - var isActive: Bool { get } } -class BaseSyncService: SyncServiceProtocol { +class BaseSyncService { let retryStrategy: ReconnectionStrategyProtocol let logger: LoggerProtocol? @@ -132,3 +129,22 @@ extension BaseSyncService: SchedulerDelegate { performSyncUp() } } + +extension BaseSyncService: SyncServiceProtocol { + func syncUp() { + mutex.lock() + + defer { + mutex.unlock() + } + + guard !isSyncing else { + return + } + + isActive = true + isSyncing = true + + performSyncUp() + } +} diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift index 9df80c5184..682c8a94af 100644 --- a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift +++ b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift @@ -1,18 +1,41 @@ import Foundation import RobinHood +protocol CrowdloanContributionStreamableSourceDelegate: AnyObject { + func didRefresh(result: Result?) +} + final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { typealias Model = CrowdloanContributionData + typealias CommitNotificationBlock = ((Result?) -> Void) let syncServices: [SyncServiceProtocol] + let chainId: ChainModel.Id + let accountId: AccountId + let eventCenter: EventCenterProtocol + weak var delegate: CrowdloanContributionStreamableSourceDelegate? - init(syncServices: [SyncServiceProtocol]) { + init( + syncServices: [SyncServiceProtocol], + chainId: ChainModel.Id, + accountId: AccountId, + eventCenter: EventCenterProtocol + ) { self.syncServices = syncServices + self.eventCenter = eventCenter + self.chainId = chainId + self.accountId = accountId + + self.eventCenter.add(observer: self) + } + + deinit { + self.eventCenter.remove(observer: self) } func fetchHistory( runningIn queue: DispatchQueue?, - commitNotificationBlock: ((Result?) -> Void)? + commitNotificationBlock: CommitNotificationBlock? ) { guard let closure = commitNotificationBlock else { return @@ -27,10 +50,10 @@ final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { func refresh( runningIn queue: DispatchQueue?, - commitNotificationBlock: ((Result?) -> Void)? + commitNotificationBlock: CommitNotificationBlock? ) { syncServices.forEach { - $0.isActive ? $0.performSyncUp() : $0.setup() + $0.syncUp() } guard let closure = commitNotificationBlock else { @@ -39,8 +62,51 @@ final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { let result: Result = Result.success(0) - dispatchInQueueWhenPossible(queue) { + dispatchInQueueWhenPossible(queue) { [weak delegate] in closure(result) + delegate?.didRefresh(result: result) } } } + +extension CrowdloanContributionStreamableSource: EventVisitorProtocol { + func processAssetBalanceChanged(event: AssetBalanceChanged) { + guard event.accountId == accountId, event.chainAssetId.chainId == chainId else { + return + } + refresh(runningIn: nil, commitNotificationBlock: nil) + } +} + +final class CrowdloanContributionStreamableSourceWrapper: StreamableSourceProtocol { + typealias Model = CrowdloanContributionData + typealias CommitNotificationBlock = CrowdloanContributionStreamableSource.CommitNotificationBlock + + private let source: CrowdloanContributionStreamableSource + private var refreshResult: Result? + + init(source: CrowdloanContributionStreamableSource) { + self.source = source + self.source.delegate = self + } + + func refresh( + runningIn _: DispatchQueue?, + commitNotificationBlock: CommitNotificationBlock? + ) { + commitNotificationBlock?(refreshResult) + } + + func fetchHistory( + runningIn queue: DispatchQueue?, + commitNotificationBlock: CommitNotificationBlock? + ) { + source.fetchHistory(runningIn: queue, commitNotificationBlock: commitNotificationBlock) + } +} + +extension CrowdloanContributionStreamableSourceWrapper: CrowdloanContributionStreamableSourceDelegate { + func didRefresh(result: Result?) { + refreshResult = result + } +} diff --git a/novawallet/Modules/AssetList/AssetListInteractor.swift b/novawallet/Modules/AssetList/AssetListInteractor.swift index 7661f84182..88c0af5ed3 100644 --- a/novawallet/Modules/AssetList/AssetListInteractor.swift +++ b/novawallet/Modules/AssetList/AssetListInteractor.swift @@ -180,8 +180,6 @@ final class AssetListInteractor: AssetListBaseInteractor { return } crowdloansSubscriptions[chain.identifier] = subscribeToCrowdloansProvider(for: accountId, chain: chain) - crowdloansSubscriptions[chain.identifier]?.refresh() - logger?.debug("Crowdloans for chain: \(chain.name) will refresh") } } From 2b730160765ae75c79aa97eefeb79378e6b13743 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 20 Sep 2022 15:56:54 +0400 Subject: [PATCH 09/52] remove intrinsicContentSize for cell --- novawallet/Modules/Locks/View/LockCollectionViewCell.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/novawallet/Modules/Locks/View/LockCollectionViewCell.swift b/novawallet/Modules/Locks/View/LockCollectionViewCell.swift index a371d0fc96..fd99959ae2 100644 --- a/novawallet/Modules/Locks/View/LockCollectionViewCell.swift +++ b/novawallet/Modules/Locks/View/LockCollectionViewCell.swift @@ -28,10 +28,6 @@ final class LockCollectionViewCell: UICollectionViewCell { fatalError("init(coder:) has not been implemented") } - override var intrinsicContentSize: CGSize { - CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) - } - private func setupLayout() { contentView.addSubview(view) view.snp.makeConstraints { From 2b028df0c3b958507123ffc971e0fac60a676939 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 20 Sep 2022 16:04:30 +0400 Subject: [PATCH 10/52] small improvements --- .../CrowdloanContributionStreamableSource.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift index 682c8a94af..f3b4dd1388 100644 --- a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift +++ b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift @@ -29,10 +29,6 @@ final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { self.eventCenter.add(observer: self) } - deinit { - self.eventCenter.remove(observer: self) - } - func fetchHistory( runningIn queue: DispatchQueue?, commitNotificationBlock: CommitNotificationBlock? @@ -62,9 +58,8 @@ final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { let result: Result = Result.success(0) - dispatchInQueueWhenPossible(queue) { [weak delegate] in + dispatchInQueueWhenPossible(queue) { closure(result) - delegate?.didRefresh(result: result) } } } @@ -74,7 +69,9 @@ extension CrowdloanContributionStreamableSource: EventVisitorProtocol { guard event.accountId == accountId, event.chainAssetId.chainId == chainId else { return } - refresh(runningIn: nil, commitNotificationBlock: nil) + refresh(runningIn: nil) { [weak delegate] in + delegate?.didRefresh(result: $0) + } } } From a4dce39b59485c012e8bd267d421c871f4115cd3 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 20 Sep 2022 15:07:41 +0300 Subject: [PATCH 11/52] prevent unknown chain options break sync --- novawallet/Common/Model/ChainRegistry/ChainModel.swift | 2 +- novawallet/Common/Model/ChainRegistry/RemoteChainModel.swift | 2 +- novawalletTests/Helper/ChainModelGenerator.swift | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/novawallet/Common/Model/ChainRegistry/ChainModel.swift b/novawallet/Common/Model/ChainRegistry/ChainModel.swift index 79d3c80d18..4abad4b479 100644 --- a/novawallet/Common/Model/ChainRegistry/ChainModel.swift +++ b/novawallet/Common/Model/ChainRegistry/ChainModel.swift @@ -95,7 +95,7 @@ struct ChainModel: Equatable, Codable, Hashable { addressPrefix = remoteModel.addressPrefix types = remoteModel.types icon = remoteModel.icon - options = remoteModel.options + options = remoteModel.options?.compactMap { ChainOptions(rawValue: $0) } externalApi = remoteModel.externalApi explorers = remoteModel.explorers additional = remoteModel.additional diff --git a/novawallet/Common/Model/ChainRegistry/RemoteChainModel.swift b/novawallet/Common/Model/ChainRegistry/RemoteChainModel.swift index 40767fb482..0fad648b4f 100644 --- a/novawallet/Common/Model/ChainRegistry/RemoteChainModel.swift +++ b/novawallet/Common/Model/ChainRegistry/RemoteChainModel.swift @@ -11,7 +11,7 @@ struct RemoteChainModel: Equatable, Codable, Hashable { let addressPrefix: UInt16 let types: ChainModel.TypesSettings? let icon: URL - let options: [ChainOptions]? + let options: [String]? let externalApi: ChainModel.ExternalApiSet? let explorers: [ChainModel.Explorer]? let additional: JSON? diff --git a/novawalletTests/Helper/ChainModelGenerator.swift b/novawalletTests/Helper/ChainModelGenerator.swift index 5685e8f03c..b90335453a 100644 --- a/novawalletTests/Helper/ChainModelGenerator.swift +++ b/novawalletTests/Helper/ChainModelGenerator.swift @@ -130,6 +130,8 @@ enum ChainModelGenerator { ) ] + let rawOptions = options.compactMap { $0.rawValue } + return RemoteChainModel( chainId: chainId, parentId: nil, @@ -139,7 +141,7 @@ enum ChainModelGenerator { addressPrefix: UInt16(index), types: types, icon: URL(string: "https://github.com")!, - options: options.isEmpty ? nil : options, + options: rawOptions.isEmpty ? nil : rawOptions, externalApi: externalApi, explorers: explorers, additional: nil From 9470a6fa7398087d44ba753c14966947563ee7ae Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 21 Sep 2022 01:02:16 +0300 Subject: [PATCH 12/52] fix transfer all for native asset --- .../Common/Substrate/Types/BalancesTransferEvent.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/novawallet/Common/Substrate/Types/BalancesTransferEvent.swift b/novawallet/Common/Substrate/Types/BalancesTransferEvent.swift index e4b817ca32..f84e6b3ef7 100644 --- a/novawallet/Common/Substrate/Types/BalancesTransferEvent.swift +++ b/novawallet/Common/Substrate/Types/BalancesTransferEvent.swift @@ -3,13 +3,16 @@ import SubstrateSdk import BigInt struct BalancesTransferEvent: Decodable { - let accountId: AccountId + let sender: AccountId + let receiver: AccountId let amount: BigUInt init(from decoder: Decoder) throws { var unkeyedContainer = try decoder.unkeyedContainer() - accountId = try unkeyedContainer.decode(AccountIdCodingWrapper.self).wrappedValue + sender = try unkeyedContainer.decode(AccountIdCodingWrapper.self).wrappedValue + + receiver = try unkeyedContainer.decode(AccountIdCodingWrapper.self).wrappedValue amount = try unkeyedContainer.decode(StringScaleMapper.self).value } From ec066d543a155d7646df2bcea438fc2fbc448ab1 Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 21 Sep 2022 01:43:38 +0300 Subject: [PATCH 13/52] fix transfer all for orml tokens --- .../Common/Model/AssetStorageInfo.swift | 86 +++++++++---- .../WalletRemoteSubscriptionWrapper.swift | 8 +- .../ExtrinsicProcessor+Events.swift | 2 +- .../Substrate/Types/EventCodingPath.swift | 6 +- .../OnChain/OnChainTransferInteractor.swift | 120 ++++++++++++++---- .../AssetStorageInfoOperationFactory.swift | 4 +- 6 files changed, 162 insertions(+), 64 deletions(-) diff --git a/novawallet/Common/Model/AssetStorageInfo.swift b/novawallet/Common/Model/AssetStorageInfo.swift index 24ca5d049b..f93b1d4164 100644 --- a/novawallet/Common/Model/AssetStorageInfo.swift +++ b/novawallet/Common/Model/AssetStorageInfo.swift @@ -6,10 +6,18 @@ enum AssetStorageInfoError: Error { case unexpectedTypeExtras } +struct OrmlTokenStorageInfo { + let currencyId: JSON + let currencyData: Data + let module: String + let existentialDeposit: BigUInt + let canTransferAll: Bool +} + enum AssetStorageInfo { - case native + case native(canTransferAll: Bool) case statemine(extras: StatemineAssetExtras) - case orml(currencyId: JSON, currencyData: Data, module: String, existentialDeposit: BigUInt) + case orml(info: OrmlTokenStorageInfo) } extension AssetStorageInfo { @@ -25,31 +33,9 @@ extension AssetStorageInfo { throw AssetStorageInfoError.unexpectedTypeExtras } - let rawCurrencyId = try Data(hexString: extras.currencyIdScale) - - let decoder = try codingFactory.createDecoder(from: rawCurrencyId) - let currencyId = try decoder.read(type: extras.currencyIdType) + let info = try createOrmlStorageInfo(from: extras, codingFactory: codingFactory) - let moduleName: String - - let tokensTransfer = CallCodingPath.tokensTransfer - if codingFactory.metadata.getCall( - from: tokensTransfer.moduleName, - with: tokensTransfer.callName - ) != nil { - moduleName = tokensTransfer.moduleName - } else { - moduleName = CallCodingPath.currenciesTransfer.moduleName - } - - let existentialDeposit = BigUInt(extras.existentialDeposit) ?? 0 - - return .orml( - currencyId: currencyId, - currencyData: rawCurrencyId, - module: moduleName, - existentialDeposit: existentialDeposit - ) + return .orml(info: info) case .statemine: guard let extras = try asset.typeExtras?.map(to: StatemineAssetExtras.self) else { throw AssetStorageInfoError.unexpectedTypeExtras @@ -57,7 +43,53 @@ extension AssetStorageInfo { return .statemine(extras: extras) case .none: - return .native + let call = CallCodingPath.transferAll + let canTransferAll = codingFactory.metadata.getCall( + from: call.moduleName, + with: call.callName + ) != nil + return .native(canTransferAll: canTransferAll) + } + } + + private static func createOrmlStorageInfo( + from extras: OrmlTokenExtras, + codingFactory: RuntimeCoderFactoryProtocol + ) throws -> OrmlTokenStorageInfo { + let rawCurrencyId = try Data(hexString: extras.currencyIdScale) + + let decoder = try codingFactory.createDecoder(from: rawCurrencyId) + let currencyId = try decoder.read(type: extras.currencyIdType) + + let moduleName: String + + let tokensTransfer = CallCodingPath.tokensTransfer + let transferAllPath: CallCodingPath + + if codingFactory.metadata.getCall( + from: tokensTransfer.moduleName, + with: tokensTransfer.callName + ) != nil { + moduleName = tokensTransfer.moduleName + transferAllPath = CallCodingPath.tokensTransferAll + } else { + moduleName = CallCodingPath.currenciesTransfer.moduleName + transferAllPath = CallCodingPath.currenciesTransferAll } + + let existentialDeposit = BigUInt(extras.existentialDeposit) ?? 0 + + let canTransferAll = codingFactory.metadata.getCall( + from: transferAllPath.moduleName, + with: transferAllPath.callName + ) != nil + + return OrmlTokenStorageInfo( + currencyId: currencyId, + currencyData: rawCurrencyId, + module: moduleName, + existentialDeposit: existentialDeposit, + canTransferAll: canTransferAll + ) } } diff --git a/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionWrapper.swift b/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionWrapper.swift index ce4d0944f2..f52ecb89b1 100644 --- a/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionWrapper.swift +++ b/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionWrapper.swift @@ -150,9 +150,9 @@ extension WalletRemoteSubscriptionWrapper: WalletRemoteSubscriptionWrapperProtoc chainAssetId: chainAsset.chainAssetId, completion: completion ) - case let .orml(_, currencyData, _, _): + case let .orml(info): return subscribeOrml( - using: currencyData, + using: info.currencyData, accountId: accountId, chainAssetId: chainAsset.chainAssetId, completion: completion @@ -185,11 +185,11 @@ extension WalletRemoteSubscriptionWrapper: WalletRemoteSubscriptionWrapperProtoc queue: .main, closure: completion ) - case let .orml(_, currencyData, _, _): + case let .orml(info): remoteSubscriptionService.detachFromOrmlToken( for: subscriptionId, accountId: accountId, - currencyId: currencyData, + currencyId: info.currencyData, chainId: chainAssetId.chainId, queue: .main, closure: completion diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift index 21fd3f0eb6..3ab6b8ce5e 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift @@ -26,7 +26,7 @@ extension ExtrinsicProcessor { metadata: RuntimeMetadataProtocol, context: RuntimeJsonContext ) throws -> BigUInt? { - let eventPaths: [EventCodingPath] = [.tokensTransfer, .currenciesTransfer] + let eventPaths: [EventCodingPath] = [.tokensTransfer, .currenciesTransferred] return try eventRecords.first { record in if let eventPath = metadata.createEventCodingPath(from: record.event), diff --git a/novawallet/Common/Substrate/Types/EventCodingPath.swift b/novawallet/Common/Substrate/Types/EventCodingPath.swift index 344c625b02..2c1b73cbeb 100644 --- a/novawallet/Common/Substrate/Types/EventCodingPath.swift +++ b/novawallet/Common/Substrate/Types/EventCodingPath.swift @@ -37,11 +37,11 @@ extension EventCodingPath { } static var tokensTransfer: EventCodingPath { - EventCodingPath(moduleName: "Tokens", eventName: "Transfered") + EventCodingPath(moduleName: "Tokens", eventName: "Transfer") } - static var currenciesTransfer: EventCodingPath { - EventCodingPath(moduleName: "Currencies", eventName: "Transfered") + static var currenciesTransferred: EventCodingPath { + EventCodingPath(moduleName: "Currencies", eventName: "Transferred") } static var ethereumExecuted: EventCodingPath { diff --git a/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift b/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift index da46b7a0a0..46abc2b770 100644 --- a/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift +++ b/novawallet/Modules/Transfer/BaseTransfer/OnChain/OnChainTransferInteractor.swift @@ -231,44 +231,106 @@ class OnChainTransferInteractor: RuntimeConstantFetching { to builder: ExtrinsicBuilderProtocol, amount: OnChainTransferAmount, recepient: AccountId, - currencyId: JSON, - module: String + tokenStorageInfo: OrmlTokenStorageInfo ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { switch amount { case let .concrete(value): - let call = callFactory.ormlTransfer( - in: module, - currencyId: currencyId, - receiverId: recepient, - amount: value + return try addingOrmlTransferValueCommand( + to: builder, + recepient: recepient, + tokenStorageInfo: tokenStorageInfo, + value: value ) - - let newBuilder = try builder.adding(call: call) - return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) - case .all: - let call = callFactory.ormlTransferAll(in: module, currencyId: currencyId, receiverId: recepient) - let newBuilder = try builder.adding(call: call) - return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) + case let .all(value): + if tokenStorageInfo.canTransferAll { + return try addingOrmlTransferAllCommand( + to: builder, + recepient: recepient, + tokenStorageInfo: tokenStorageInfo + ) + } else { + return try addingOrmlTransferValueCommand( + to: builder, + recepient: recepient, + tokenStorageInfo: tokenStorageInfo, + value: value + ) + } } } + func addingOrmlTransferValueCommand( + to builder: ExtrinsicBuilderProtocol, + recepient: AccountId, + tokenStorageInfo: OrmlTokenStorageInfo, + value: BigUInt + ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { + let call = callFactory.ormlTransfer( + in: tokenStorageInfo.module, + currencyId: tokenStorageInfo.currencyId, + receiverId: recepient, + amount: value + ) + + let newBuilder = try builder.adding(call: call) + return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) + } + + func addingOrmlTransferAllCommand( + to builder: ExtrinsicBuilderProtocol, + recepient: AccountId, + tokenStorageInfo: OrmlTokenStorageInfo + ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { + let call = callFactory.ormlTransferAll( + in: tokenStorageInfo.module, + currencyId: tokenStorageInfo.currencyId, + receiverId: recepient + ) + let newBuilder = try builder.adding(call: call) + return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) + } + func addingNativeTransferCommand( to builder: ExtrinsicBuilderProtocol, amount: OnChainTransferAmount, - recepient: AccountId + recepient: AccountId, + canTransferAll: Bool ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { switch amount { case let .concrete(value): - let call = callFactory.nativeTransfer(to: recepient, amount: value) - let newBuilder = try builder.adding(call: call) - return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) - case .all: - let call = callFactory.nativeTransferAll(to: recepient) - let newBuilder = try builder.adding(call: call) - return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) + return try addingNativeTransferValueCommand( + to: builder, + recepient: recepient, + value: value + ) + case let .all(value): + if canTransferAll { + return try addingNativeTransferAllCommand(to: builder, recepient: recepient) + } else { + return try addingNativeTransferValueCommand(to: builder, recepient: recepient, value: value) + } } } + func addingNativeTransferValueCommand( + to builder: ExtrinsicBuilderProtocol, + recepient: AccountId, + value: BigUInt + ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { + let call = callFactory.nativeTransfer(to: recepient, amount: value) + let newBuilder = try builder.adding(call: call) + return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) + } + + func addingNativeTransferAllCommand( + to builder: ExtrinsicBuilderProtocol, + recepient: AccountId + ) throws -> (ExtrinsicBuilderProtocol, CallCodingPath?) { + let call = callFactory.nativeTransferAll(to: recepient) + let newBuilder = try builder.adding(call: call) + return (newBuilder, CallCodingPath(moduleName: call.moduleName, callName: call.callName)) + } + func addingAssetsTransferCommand( to builder: ExtrinsicBuilderProtocol, amount: OnChainTransferAmount, @@ -295,13 +357,12 @@ class OnChainTransferInteractor: RuntimeConstantFetching { } switch sendingAssetInfo { - case let .orml(currencyId, _, module, _): + case let .orml(info): return try addingOrmlTransferCommand( to: builder, amount: amount, recepient: recepient, - currencyId: currencyId, - module: module + tokenStorageInfo: info ) case let .statemine(extras): return try addingAssetsTransferCommand( @@ -310,8 +371,13 @@ class OnChainTransferInteractor: RuntimeConstantFetching { recepient: recepient, extras: extras ) - case .native: - return try addingNativeTransferCommand(to: builder, amount: amount, recepient: recepient) + case let .native(canTransferAll): + return try addingNativeTransferCommand( + to: builder, + amount: amount, + recepient: recepient, + canTransferAll: canTransferAll + ) } } diff --git a/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift b/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift index 92de27799b..fbf2a3f9db 100644 --- a/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift +++ b/novawallet/Modules/Transfer/Operation/AssetStorageInfoOperationFactory.swift @@ -128,8 +128,8 @@ extension AssetStorageInfoOperationFactory: AssetStorageInfoOperationFactoryProt storage: storage, runtimeService: runtimeService ) - case let .orml(_, _, _, existentialDeposit): - let assetExistence = AssetBalanceExistence(minBalance: existentialDeposit, isSelfSufficient: true) + case let .orml(info): + let assetExistence = AssetBalanceExistence(minBalance: info.existentialDeposit, isSelfSufficient: true) return CompoundOperationWrapper.createWithResult(assetExistence) } } From 132d0eb6400c255ae6b4c6913c822fa5f3ec6d0b Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 21 Sep 2022 11:13:36 +0400 Subject: [PATCH 14/52] remove delegate --- ...rowdloanContributionStreamableSource.swift | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift index f3b4dd1388..aefc2b7296 100644 --- a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift +++ b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift @@ -1,10 +1,6 @@ import Foundation import RobinHood -protocol CrowdloanContributionStreamableSourceDelegate: AnyObject { - func didRefresh(result: Result?) -} - final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { typealias Model = CrowdloanContributionData typealias CommitNotificationBlock = ((Result?) -> Void) @@ -13,7 +9,7 @@ final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { let chainId: ChainModel.Id let accountId: AccountId let eventCenter: EventCenterProtocol - weak var delegate: CrowdloanContributionStreamableSourceDelegate? + var didRefreshClosure: CommitNotificationBlock? init( syncServices: [SyncServiceProtocol], @@ -52,12 +48,13 @@ final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { $0.syncUp() } + let result: Result = Result.success(0) + didRefreshClosure?(result) + guard let closure = commitNotificationBlock else { return } - let result: Result = Result.success(0) - dispatchInQueueWhenPossible(queue) { closure(result) } @@ -69,9 +66,7 @@ extension CrowdloanContributionStreamableSource: EventVisitorProtocol { guard event.accountId == accountId, event.chainAssetId.chainId == chainId else { return } - refresh(runningIn: nil) { [weak delegate] in - delegate?.didRefresh(result: $0) - } + refresh(runningIn: nil, commitNotificationBlock: nil) } } @@ -84,7 +79,9 @@ final class CrowdloanContributionStreamableSourceWrapper: StreamableSourceProtoc init(source: CrowdloanContributionStreamableSource) { self.source = source - self.source.delegate = self + self.source.didRefreshClosure = { [weak self] in + self?.refreshResult = $0 + } } func refresh( @@ -101,9 +98,3 @@ final class CrowdloanContributionStreamableSourceWrapper: StreamableSourceProtoc source.fetchHistory(runningIn: queue, commitNotificationBlock: commitNotificationBlock) } } - -extension CrowdloanContributionStreamableSourceWrapper: CrowdloanContributionStreamableSourceDelegate { - func didRefresh(result: Result?) { - refreshResult = result - } -} From 7d8c6c05da5e30b81080cf98c58fee2f0fad0bef Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 21 Sep 2022 12:32:50 +0400 Subject: [PATCH 15/52] fixes for tests --- .../WalletRemoteSubscriptionService.swift | 32 ++++---- novawalletTests/Mocks/CommonMocks.swift | 73 ++++++++++--------- .../WalletLocalSubscriptionFactoryStub.swift | 4 + 3 files changed, 59 insertions(+), 50 deletions(-) diff --git a/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionService.swift b/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionService.swift index fb18824662..dc9b37391f 100644 --- a/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionService.swift +++ b/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionService.swift @@ -10,7 +10,7 @@ protocol WalletRemoteSubscriptionServiceProtocol { chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, - subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol + subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol? ) -> UUID? func detachFromAccountInfo( @@ -49,7 +49,7 @@ protocol WalletRemoteSubscriptionServiceProtocol { chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, - subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol + subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol? ) -> UUID? // swiftlint:disable:next function_parameter_count @@ -71,7 +71,7 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, - subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol + subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol? ) -> UUID? { do { let storagePath = StorageCodingPath.account @@ -117,11 +117,13 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu ) } - let handlingFactory = AccountInfoSubscriptionHandlingFactory( - accountLocalStorageKey: accountLocalKey, - locksLocalStorageKey: locksLocalKey, - factory: subscriptionHandlingFactory - ) + let handlingFactory = subscriptionHandlingFactory.map { + AccountInfoSubscriptionHandlingFactory( + accountLocalStorageKey: accountLocalKey, + locksLocalStorageKey: locksLocalKey, + factory: $0 + ) + } return attachToSubscription( with: [accountRequest, locksRequest], @@ -264,7 +266,7 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, - subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol + subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol? ) -> UUID? { do { let storageKeyFactory = LocalStorageKeyFactory() @@ -295,11 +297,13 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu param2Encoder: { $0 } ) - let handlingFactory = OrmlTokenSubscriptionHandlingFactory( - accountLocalStorageKey: accountLocalKey, - locksLocalStorageKey: locksLocalKey, - factory: subscriptionHandlingFactory - ) + let handlingFactory = subscriptionHandlingFactory.map { + OrmlTokenSubscriptionHandlingFactory( + accountLocalStorageKey: accountLocalKey, + locksLocalStorageKey: locksLocalKey, + factory: $0 + ) + } return attachToSubscription( with: [accountRequest, locksRequest], diff --git a/novawalletTests/Mocks/CommonMocks.swift b/novawalletTests/Mocks/CommonMocks.swift index f45cdcbf7f..86fc0d81b1 100644 --- a/novawalletTests/Mocks/CommonMocks.swift +++ b/novawalletTests/Mocks/CommonMocks.swift @@ -5732,6 +5732,7 @@ import Cuckoo @testable import SoraKeystore import Foundation +import RobinHood import SubstrateSdk @@ -5760,9 +5761,9 @@ import SubstrateSdk - func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID? { - return cuckoo_manager.call("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", + return cuckoo_manager.call("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", parameters: (accountId, chainId, chainFormat, queue, closure, subscriptionHandlingFactory), escapingParameters: (accountId, chainId, chainFormat, queue, closure, subscriptionHandlingFactory), superclassCall: @@ -5820,9 +5821,9 @@ import SubstrateSdk - func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID? { - return cuckoo_manager.call("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", + return cuckoo_manager.call("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", parameters: (accountId, currencyId, chainId, queue, closure, subscriptionHandlingFactory), escapingParameters: (accountId, currencyId, chainId, queue, closure, subscriptionHandlingFactory), superclassCall: @@ -5857,9 +5858,9 @@ import SubstrateSdk } - func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ProtocolStubFunction<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionServiceProtocol.self, method: "attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) + func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ProtocolStubFunction<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == NativeTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionServiceProtocol.self, method: "attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) } func detachFromAccountInfo(for subscriptionId: M1, accountId: M2, chainId: M3, queue: M4, closure: M5) -> Cuckoo.ProtocolStubNoReturnFunction<(UUID, AccountId, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?)> where M1.MatchedType == UUID, M2.MatchedType == AccountId, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure { @@ -5877,9 +5878,9 @@ import SubstrateSdk return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionServiceProtocol.self, method: "detachFromAsset(for: UUID, accountId: AccountId, extras: StatemineAssetExtras, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?)", parameterMatchers: matchers)) } - func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ProtocolStubFunction<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionServiceProtocol.self, method: "attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) + func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ProtocolStubFunction<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == OrmlTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionServiceProtocol.self, method: "attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) } func detachFromOrmlToken(for subscriptionId: M1, accountId: M2, currencyId: M3, chainId: M4, queue: M5, closure: M6) -> Cuckoo.ProtocolStubNoReturnFunction<(UUID, AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?)> where M1.MatchedType == UUID, M2.MatchedType == AccountId, M3.MatchedType == Data, M4.MatchedType == ChainModel.Id, M5.OptionalMatchedType == DispatchQueue, M6.OptionalMatchedType == RemoteSubscriptionClosure { @@ -5904,9 +5905,9 @@ import SubstrateSdk @discardableResult - func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return cuckoo_manager.verify("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == NativeTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return cuckoo_manager.verify("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -5928,9 +5929,9 @@ import SubstrateSdk } @discardableResult - func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return cuckoo_manager.verify("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == OrmlTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return cuckoo_manager.verify("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -5950,7 +5951,7 @@ import SubstrateSdk - func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID? { return DefaultValueRegistry.defaultValue(for: (UUID?).self) } @@ -5974,7 +5975,7 @@ import SubstrateSdk - func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID? { return DefaultValueRegistry.defaultValue(for: (UUID?).self) } @@ -6013,9 +6014,9 @@ import SubstrateSdk - override func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + override func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID? { - return cuckoo_manager.call("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", + return cuckoo_manager.call("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", parameters: (accountId, chainId, chainFormat, queue, closure, subscriptionHandlingFactory), escapingParameters: (accountId, chainId, chainFormat, queue, closure, subscriptionHandlingFactory), superclassCall: @@ -6073,9 +6074,9 @@ import SubstrateSdk - override func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + override func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID? { - return cuckoo_manager.call("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", + return cuckoo_manager.call("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", parameters: (accountId, currencyId, chainId, queue, closure, subscriptionHandlingFactory), escapingParameters: (accountId, currencyId, chainId, queue, closure, subscriptionHandlingFactory), superclassCall: @@ -6110,9 +6111,9 @@ import SubstrateSdk } - func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ClassStubFunction<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionService.self, method: "attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) + func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ClassStubFunction<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == NativeTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionService.self, method: "attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) } func detachFromAccountInfo(for subscriptionId: M1, accountId: M2, chainId: M3, queue: M4, closure: M5) -> Cuckoo.ClassStubNoReturnFunction<(UUID, AccountId, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?)> where M1.MatchedType == UUID, M2.MatchedType == AccountId, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure { @@ -6130,9 +6131,9 @@ import SubstrateSdk return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionService.self, method: "detachFromAsset(for: UUID, accountId: AccountId, extras: StatemineAssetExtras, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?)", parameterMatchers: matchers)) } - func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ClassStubFunction<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionService.self, method: "attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) + func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ClassStubFunction<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == OrmlTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionService.self, method: "attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) } func detachFromOrmlToken(for subscriptionId: M1, accountId: M2, currencyId: M3, chainId: M4, queue: M5, closure: M6) -> Cuckoo.ClassStubNoReturnFunction<(UUID, AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?)> where M1.MatchedType == UUID, M2.MatchedType == AccountId, M3.MatchedType == Data, M4.MatchedType == ChainModel.Id, M5.OptionalMatchedType == DispatchQueue, M6.OptionalMatchedType == RemoteSubscriptionClosure { @@ -6157,9 +6158,9 @@ import SubstrateSdk @discardableResult - func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return cuckoo_manager.verify("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == NativeTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return cuckoo_manager.verify("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -6181,9 +6182,9 @@ import SubstrateSdk } @discardableResult - func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return cuckoo_manager.verify("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == OrmlTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return cuckoo_manager.verify("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -6203,7 +6204,7 @@ import SubstrateSdk - override func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + override func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID? { return DefaultValueRegistry.defaultValue(for: (UUID?).self) } @@ -6227,7 +6228,7 @@ import SubstrateSdk - override func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + override func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID? { return DefaultValueRegistry.defaultValue(for: (UUID?).self) } diff --git a/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift b/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift index 1a564a7387..a5bb4fe92f 100644 --- a/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift +++ b/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift @@ -60,4 +60,8 @@ final class WalletLocalSubscriptionFactoryStub: WalletLocalSubscriptionFactoryPr func getAllBalancesProvider() throws -> StreamableProvider { throw CommonError.undefined } + + func getLocksProvider(for accountId: AccountId) throws -> StreamableProvider { + throw CommonError.undefined + } } From eae0ce27395fa6c967735ee065819e0ecbd9d2d1 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 21 Sep 2022 12:38:30 +0400 Subject: [PATCH 16/52] bugfix update header view --- novawallet/Modules/AssetList/AssetListPresenter.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/novawallet/Modules/AssetList/AssetListPresenter.swift b/novawallet/Modules/AssetList/AssetListPresenter.swift index 208295cb33..d476fd84df 100644 --- a/novawallet/Modules/AssetList/AssetListPresenter.swift +++ b/novawallet/Modules/AssetList/AssetListPresenter.swift @@ -480,10 +480,14 @@ extension AssetListPresenter: AssetListInteractorOutputProtocol { func didReceiveLocks(result: Result<[AssetLock], Error>) { locksResult = result + + updateHeaderView() } func didReceiveCrowdloans(result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) { crowdloansResult = result + + updateHeaderView() } } From b213bfd78b0153e03aaa2aed8d1f100e34a17779 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 21 Sep 2022 14:51:58 +0400 Subject: [PATCH 17/52] remove wrapper --- ...ContributionLocalSubscriptionFactory.swift | 4 +-- .../Common/Services/BaseSyncService.swift | 4 +-- ...rowdloanContributionStreamableSource.swift | 35 +++---------------- 3 files changed, 8 insertions(+), 35 deletions(-) diff --git a/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift index fee63a31d4..3ed66b12fc 100644 --- a/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift +++ b/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift @@ -69,8 +69,6 @@ final class CrowdloanContributionLocalSubscriptionFactory: SubstrateLocalSubscri eventCenter: eventCenter ) - let selfUpdatingSource = CrowdloanContributionStreamableSourceWrapper(source: source) - let crowdloansFilter = NSPredicate.crowdloanContribution( for: chain.chainId, accountId: accountId @@ -99,7 +97,7 @@ final class CrowdloanContributionLocalSubscriptionFactory: SubstrateLocalSubscri } let provider = StreamableProvider( - source: AnyStreamableSource(selfUpdatingSource), + source: AnyStreamableSource(source), repository: AnyDataProviderRepository(repository), observable: AnyDataProviderRepositoryObservable(observable), operationManager: operationManager diff --git a/novawallet/Common/Services/BaseSyncService.swift b/novawallet/Common/Services/BaseSyncService.swift index 3fd5f78d10..48a9b020f0 100644 --- a/novawallet/Common/Services/BaseSyncService.swift +++ b/novawallet/Common/Services/BaseSyncService.swift @@ -5,6 +5,7 @@ import SubstrateSdk protocol SyncServiceProtocol { func syncUp() func stopSyncUp() + func setup() } class BaseSyncService { @@ -138,11 +139,10 @@ extension BaseSyncService: SyncServiceProtocol { mutex.unlock() } - guard !isSyncing else { + guard isActive, !isSyncing else { return } - isActive = true isSyncing = true performSyncUp() diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift index aefc2b7296..8b6619f3e4 100644 --- a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift +++ b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift @@ -9,7 +9,6 @@ final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { let chainId: ChainModel.Id let accountId: AccountId let eventCenter: EventCenterProtocol - var didRefreshClosure: CommitNotificationBlock? init( syncServices: [SyncServiceProtocol], @@ -23,6 +22,10 @@ final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { self.accountId = accountId self.eventCenter.add(observer: self) + + syncServices.forEach { + $0.setup() + } } func fetchHistory( @@ -55,6 +58,7 @@ final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { return } + let result: Result = Result.success(0) dispatchInQueueWhenPossible(queue) { closure(result) } @@ -69,32 +73,3 @@ extension CrowdloanContributionStreamableSource: EventVisitorProtocol { refresh(runningIn: nil, commitNotificationBlock: nil) } } - -final class CrowdloanContributionStreamableSourceWrapper: StreamableSourceProtocol { - typealias Model = CrowdloanContributionData - typealias CommitNotificationBlock = CrowdloanContributionStreamableSource.CommitNotificationBlock - - private let source: CrowdloanContributionStreamableSource - private var refreshResult: Result? - - init(source: CrowdloanContributionStreamableSource) { - self.source = source - self.source.didRefreshClosure = { [weak self] in - self?.refreshResult = $0 - } - } - - func refresh( - runningIn _: DispatchQueue?, - commitNotificationBlock: CommitNotificationBlock? - ) { - commitNotificationBlock?(refreshResult) - } - - func fetchHistory( - runningIn queue: DispatchQueue?, - commitNotificationBlock: CommitNotificationBlock? - ) { - source.fetchHistory(runningIn: queue, commitNotificationBlock: commitNotificationBlock) - } -} From eb2ec7dc673de35cb69af7f1deedf8a748ae4060 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Wed, 21 Sep 2022 14:52:37 +0400 Subject: [PATCH 18/52] remove closure --- .../CrowdloanContributionStreamableSource.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift index 8b6619f3e4..91d6c51289 100644 --- a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift +++ b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift @@ -51,9 +51,6 @@ final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { $0.syncUp() } - let result: Result = Result.success(0) - didRefreshClosure?(result) - guard let closure = commitNotificationBlock else { return } From 7eaf685550ab181d946de88ba0415006fd0f0f5f Mon Sep 17 00:00:00 2001 From: Gulnaz <666lynx666@mail.ru> Date: Wed, 21 Sep 2022 15:30:35 +0400 Subject: [PATCH 19/52] Total balance breakdown (#410) * update ui * move sync to service * remove intrinsicContentSize for cell * small improvements * remove delegate * fixes for tests * bugfix update header view * remove wrapper * remove closure --- ...ContributionLocalSubscriptionFactory.swift | 11 ++- .../NSCollectionLayoutSection+create.swift | 6 +- .../Common/Services/BaseSyncService.swift | 24 +++++- ...rowdloanContributionStreamableSource.swift | 36 +++++++-- .../WalletRemoteSubscriptionService.swift | 32 ++++---- .../AssetList/AssetListInteractor.swift | 2 - .../AssetList/AssetListPresenter.swift | 4 + .../Currency/View/CurrencyViewLayout.swift | 1 + .../Locks/LocksBalanceViewModelFactory.swift | 40 ++++++++-- novawallet/Modules/Locks/LocksPresenter.swift | 7 +- novawallet/Modules/Locks/LocksProtocols.swift | 5 +- .../Modules/Locks/LocksViewController.swift | 7 +- .../Modules/Locks/LocksViewLayout.swift | 4 +- .../Locks/View/LockCollectionViewCell.swift | 28 ++++--- .../GenericCollectionViewLayout.swift | 2 + novawalletTests/Mocks/CommonMocks.swift | 73 ++++++++++--------- .../WalletLocalSubscriptionFactoryStub.swift | 4 + 17 files changed, 201 insertions(+), 85 deletions(-) diff --git a/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift index 2afbb55b37..3ed66b12fc 100644 --- a/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift +++ b/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift @@ -12,6 +12,7 @@ final class CrowdloanContributionLocalSubscriptionFactory: SubstrateLocalSubscri CrowdloanContributionLocalSubscriptionFactoryProtocol { let operationFactory: CrowdloanOperationFactoryProtocol let paraIdOperationFactory: ParaIdOperationFactoryProtocol + let eventCenter: EventCenterProtocol init( operationFactory: CrowdloanOperationFactoryProtocol, @@ -19,10 +20,12 @@ final class CrowdloanContributionLocalSubscriptionFactory: SubstrateLocalSubscri chainRegistry: ChainRegistryProtocol, storageFacade: StorageFacadeProtocol, paraIdOperationFactory: ParaIdOperationFactoryProtocol, + eventCenter: EventCenterProtocol, logger: LoggerProtocol ) { self.operationFactory = operationFactory self.paraIdOperationFactory = paraIdOperationFactory + self.eventCenter = eventCenter super.init( chainRegistry: chainRegistry, @@ -59,7 +62,12 @@ final class CrowdloanContributionLocalSubscriptionFactory: SubstrateLocalSubscri let syncServices = [onChainSyncService] + offChainSyncServices - let source = CrowdloanContributionStreamableSource(syncServices: syncServices) + let source = CrowdloanContributionStreamableSource( + syncServices: syncServices, + chainId: chain.chainId, + accountId: accountId, + eventCenter: eventCenter + ) let crowdloansFilter = NSPredicate.crowdloanContribution( for: chain.chainId, @@ -168,6 +176,7 @@ extension CrowdloanContributionLocalSubscriptionFactory { chainRegistry: ChainRegistryFacade.sharedRegistry, storageFacade: SubstrateDataStorageFacade.shared, paraIdOperationFactory: ParaIdOperationFactory.shared, + eventCenter: EventCenter.shared, logger: Logger.shared ) } diff --git a/novawallet/Common/Extension/UIKit/NSCollectionLayoutSection+create.swift b/novawallet/Common/Extension/UIKit/NSCollectionLayoutSection+create.swift index ccf4840a38..e55674ee7e 100644 --- a/novawallet/Common/Extension/UIKit/NSCollectionLayoutSection+create.swift +++ b/novawallet/Common/Extension/UIKit/NSCollectionLayoutSection+create.swift @@ -3,6 +3,7 @@ import UIKit extension NSCollectionLayoutSection { struct Settings { let estimatedRowHeight: CGFloat + let absoluteHeaderHeight: CGFloat? let estimatedHeaderHeight: CGFloat let sectionContentInsets: NSDirectionalEdgeInsets let sectionInterGroupSpacing: CGFloat @@ -28,7 +29,10 @@ extension NSCollectionLayoutSection { let headerSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), - heightDimension: .estimated(settings.estimatedHeaderHeight) + heightDimension: + settings.absoluteHeaderHeight.map { + .absolute($0) + } ?? .estimated(settings.estimatedHeaderHeight) ) let section = NSCollectionLayoutSection(group: group) section.contentInsets = settings.sectionContentInsets diff --git a/novawallet/Common/Services/BaseSyncService.swift b/novawallet/Common/Services/BaseSyncService.swift index 97560a8398..48a9b020f0 100644 --- a/novawallet/Common/Services/BaseSyncService.swift +++ b/novawallet/Common/Services/BaseSyncService.swift @@ -3,14 +3,12 @@ import RobinHood import SubstrateSdk protocol SyncServiceProtocol { - func performSyncUp() + func syncUp() func stopSyncUp() func setup() - - var isActive: Bool { get } } -class BaseSyncService: SyncServiceProtocol { +class BaseSyncService { let retryStrategy: ReconnectionStrategyProtocol let logger: LoggerProtocol? @@ -132,3 +130,21 @@ extension BaseSyncService: SchedulerDelegate { performSyncUp() } } + +extension BaseSyncService: SyncServiceProtocol { + func syncUp() { + mutex.lock() + + defer { + mutex.unlock() + } + + guard isActive, !isSyncing else { + return + } + + isSyncing = true + + performSyncUp() + } +} diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift index 9df80c5184..91d6c51289 100644 --- a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift +++ b/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift @@ -3,16 +3,34 @@ import RobinHood final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { typealias Model = CrowdloanContributionData + typealias CommitNotificationBlock = ((Result?) -> Void) let syncServices: [SyncServiceProtocol] + let chainId: ChainModel.Id + let accountId: AccountId + let eventCenter: EventCenterProtocol - init(syncServices: [SyncServiceProtocol]) { + init( + syncServices: [SyncServiceProtocol], + chainId: ChainModel.Id, + accountId: AccountId, + eventCenter: EventCenterProtocol + ) { self.syncServices = syncServices + self.eventCenter = eventCenter + self.chainId = chainId + self.accountId = accountId + + self.eventCenter.add(observer: self) + + syncServices.forEach { + $0.setup() + } } func fetchHistory( runningIn queue: DispatchQueue?, - commitNotificationBlock: ((Result?) -> Void)? + commitNotificationBlock: CommitNotificationBlock? ) { guard let closure = commitNotificationBlock else { return @@ -27,10 +45,10 @@ final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { func refresh( runningIn queue: DispatchQueue?, - commitNotificationBlock: ((Result?) -> Void)? + commitNotificationBlock: CommitNotificationBlock? ) { syncServices.forEach { - $0.isActive ? $0.performSyncUp() : $0.setup() + $0.syncUp() } guard let closure = commitNotificationBlock else { @@ -38,9 +56,17 @@ final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { } let result: Result = Result.success(0) - dispatchInQueueWhenPossible(queue) { closure(result) } } } + +extension CrowdloanContributionStreamableSource: EventVisitorProtocol { + func processAssetBalanceChanged(event: AssetBalanceChanged) { + guard event.accountId == accountId, event.chainAssetId.chainId == chainId else { + return + } + refresh(runningIn: nil, commitNotificationBlock: nil) + } +} diff --git a/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionService.swift b/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionService.swift index fb18824662..dc9b37391f 100644 --- a/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionService.swift +++ b/novawallet/Common/Services/RemoteSubscription/WalletRemoteSubscriptionService.swift @@ -10,7 +10,7 @@ protocol WalletRemoteSubscriptionServiceProtocol { chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, - subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol + subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol? ) -> UUID? func detachFromAccountInfo( @@ -49,7 +49,7 @@ protocol WalletRemoteSubscriptionServiceProtocol { chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, - subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol + subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol? ) -> UUID? // swiftlint:disable:next function_parameter_count @@ -71,7 +71,7 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, - subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol + subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol? ) -> UUID? { do { let storagePath = StorageCodingPath.account @@ -117,11 +117,13 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu ) } - let handlingFactory = AccountInfoSubscriptionHandlingFactory( - accountLocalStorageKey: accountLocalKey, - locksLocalStorageKey: locksLocalKey, - factory: subscriptionHandlingFactory - ) + let handlingFactory = subscriptionHandlingFactory.map { + AccountInfoSubscriptionHandlingFactory( + accountLocalStorageKey: accountLocalKey, + locksLocalStorageKey: locksLocalKey, + factory: $0 + ) + } return attachToSubscription( with: [accountRequest, locksRequest], @@ -264,7 +266,7 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, - subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol + subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol? ) -> UUID? { do { let storageKeyFactory = LocalStorageKeyFactory() @@ -295,11 +297,13 @@ class WalletRemoteSubscriptionService: RemoteSubscriptionService, WalletRemoteSu param2Encoder: { $0 } ) - let handlingFactory = OrmlTokenSubscriptionHandlingFactory( - accountLocalStorageKey: accountLocalKey, - locksLocalStorageKey: locksLocalKey, - factory: subscriptionHandlingFactory - ) + let handlingFactory = subscriptionHandlingFactory.map { + OrmlTokenSubscriptionHandlingFactory( + accountLocalStorageKey: accountLocalKey, + locksLocalStorageKey: locksLocalKey, + factory: $0 + ) + } return attachToSubscription( with: [accountRequest, locksRequest], diff --git a/novawallet/Modules/AssetList/AssetListInteractor.swift b/novawallet/Modules/AssetList/AssetListInteractor.swift index 7661f84182..88c0af5ed3 100644 --- a/novawallet/Modules/AssetList/AssetListInteractor.swift +++ b/novawallet/Modules/AssetList/AssetListInteractor.swift @@ -180,8 +180,6 @@ final class AssetListInteractor: AssetListBaseInteractor { return } crowdloansSubscriptions[chain.identifier] = subscribeToCrowdloansProvider(for: accountId, chain: chain) - crowdloansSubscriptions[chain.identifier]?.refresh() - logger?.debug("Crowdloans for chain: \(chain.name) will refresh") } } diff --git a/novawallet/Modules/AssetList/AssetListPresenter.swift b/novawallet/Modules/AssetList/AssetListPresenter.swift index 208295cb33..d476fd84df 100644 --- a/novawallet/Modules/AssetList/AssetListPresenter.swift +++ b/novawallet/Modules/AssetList/AssetListPresenter.swift @@ -480,10 +480,14 @@ extension AssetListPresenter: AssetListInteractorOutputProtocol { func didReceiveLocks(result: Result<[AssetLock], Error>) { locksResult = result + + updateHeaderView() } func didReceiveCrowdloans(result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) { crowdloansResult = result + + updateHeaderView() } } diff --git a/novawallet/Modules/Currency/View/CurrencyViewLayout.swift b/novawallet/Modules/Currency/View/CurrencyViewLayout.swift index a8f0b4dfe5..453a73ac0b 100644 --- a/novawallet/Modules/Currency/View/CurrencyViewLayout.swift +++ b/novawallet/Modules/Currency/View/CurrencyViewLayout.swift @@ -34,6 +34,7 @@ final class CurrencyViewLayout: UIView { private func createCompositionalLayout() -> UICollectionViewCompositionalLayout { let settings = NSCollectionLayoutSection.Settings( estimatedRowHeight: Constants.estimatedRowHeight, + absoluteHeaderHeight: nil, estimatedHeaderHeight: Constants.estimatedHeaderHeight, sectionContentInsets: Constants.sectionContentInsets, sectionInterGroupSpacing: Constants.interGroupSpacing, diff --git a/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift index a884f324b3..0d91feebd8 100644 --- a/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift +++ b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift @@ -30,7 +30,8 @@ struct FormattedBalance { struct FormattedPlank { let amount: String - let price: Decimal + let price: String? + let priceValue: Decimal } final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { @@ -131,9 +132,11 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { prices: [ChainAssetId: PriceData], locale: Locale ) -> FormattedPlank? { - guard let assetPrecision = chains[chainAssetId.chainId]?.asset(for: chainAssetId.assetId)?.precision else { + guard let assetPrecision = chains[chainAssetId.chainId]?.asset(for: chainAssetId.assetId)?.precision, + let utilityAsset = chains[chainAssetId.chainId]?.utilityAsset() else { return nil } + let priceData = prices[chainAssetId] let rate = priceData.map { Decimal(string: $0.price) ?? 0 } ?? 0 @@ -144,16 +147,34 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { rate: rate ) + let amount = calculateAmount( + from: plank, + precision: utilityAsset.precision, + rate: nil + ) + let formattedAmount = formatAmount( + amount, + assetDisplayInfo: utilityAsset.displayInfo, + locale: locale + ) + let formattedPrice = formatPrice(amount: price, priceData: priceData, locale: locale) - return .init(amount: formattedPrice, price: price) + return .init( + amount: formattedAmount, + price: formattedPrice, + priceValue: price + ) } - private func calculateAmount(from plank: BigUInt, precision: UInt16, rate: Decimal) -> Decimal { + private func calculateAmount(from plank: BigUInt, precision: UInt16, rate: Decimal?) -> Decimal { let amount = Decimal.fromSubstrateAmount( plank, precision: Int16(precision) ) ?? 0.0 - return amount * rate + + return rate.map { + amount * $0 + } ?? amount } private func formatPrice(amount: Decimal, priceData: PriceData?, locale: Locale) -> String { @@ -162,4 +183,13 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { let priceFormatter = assetFormatterFactory.createTokenFormatter(for: assetDisplayInfo) return priceFormatter.value(for: locale).stringFromDecimal(amount) ?? "" } + + private func formatAmount( + _ amount: Decimal, + assetDisplayInfo: AssetBalanceDisplayInfo, + locale: Locale + ) -> String { + let priceFormatter = assetFormatterFactory.createTokenFormatter(for: assetDisplayInfo) + return priceFormatter.value(for: locale).stringFromDecimal(amount) ?? "" + } } diff --git a/novawallet/Modules/Locks/LocksPresenter.swift b/novawallet/Modules/Locks/LocksPresenter.swift index bc96b0d947..e01553c186 100644 --- a/novawallet/Modules/Locks/LocksPresenter.swift +++ b/novawallet/Modules/Locks/LocksPresenter.swift @@ -67,7 +67,7 @@ final class LocksPresenter { balanceModel.locksPrice / balanceModel.totalPrice : 0 let displayPercent = formatter.stringFromDecimal(percent) ?? "" let locksCells = createLocksCells().sorted { - $0.price > $1.price + $0.priceValue > $1.priceValue } return LocksViewSectionModel( @@ -152,8 +152,9 @@ final class LocksPresenter { return LocksViewSectionModel.CellViewModel( id: identifier, title: title, - value: value.amount, - price: value.price + amount: value.amount, + price: value.price, + priceValue: value.priceValue ) } diff --git a/novawallet/Modules/Locks/LocksProtocols.swift b/novawallet/Modules/Locks/LocksProtocols.swift index eca08b3ecf..0a4cbef99a 100644 --- a/novawallet/Modules/Locks/LocksProtocols.swift +++ b/novawallet/Modules/Locks/LocksProtocols.swift @@ -29,7 +29,8 @@ struct LocksViewSectionModel: SectionProtocol, Hashable { struct CellViewModel: Hashable { let id: String let title: String - let value: String - let price: Decimal + let amount: String + let price: String? + let priceValue: Decimal } } diff --git a/novawallet/Modules/Locks/LocksViewController.swift b/novawallet/Modules/Locks/LocksViewController.swift index 0ca31ac287..5fe10c082c 100644 --- a/novawallet/Modules/Locks/LocksViewController.swift +++ b/novawallet/Modules/Locks/LocksViewController.swift @@ -52,7 +52,12 @@ final class LocksViewController: UIViewController, ViewHolder, ModalSheetCollect collectionView: rootView.collectionView, cellProvider: { collectionView, indexPath, model -> UICollectionViewCell? in let cell: LockCollectionViewCell? = collectionView.dequeueReusableCell(for: indexPath) - cell?.bind(title: model.title, value: model.value) + cell?.bind(viewModel: .init( + title: model.title, + amount: model.amount, + price: model.price + ) + ) return cell } ) diff --git a/novawallet/Modules/Locks/LocksViewLayout.swift b/novawallet/Modules/Locks/LocksViewLayout.swift index 46d92ed741..292a82779b 100644 --- a/novawallet/Modules/Locks/LocksViewLayout.swift +++ b/novawallet/Modules/Locks/LocksViewLayout.swift @@ -14,8 +14,8 @@ final class LocksViewLayout: GenericCollectionViewLayout( + lazy var view = GenericTitleValueView( titleView: titleLabel, valueView: valueLabel ) private let titleLabel: UILabel = .create { - $0.font = .regularSubheadline - $0.textColor = R.color.colorWhite48() + $0.font = .regularFootnote + $0.textColor = R.color.colorWhite64() } - private let valueLabel: UILabel = .create { - $0.font = .regularSubheadline - $0.textColor = R.color.colorWhite48() + private let valueLabel: MultiValueView = .create { + $0.valueTop.font = .regularFootnote + $0.valueBottom.font = .caption1 + $0.valueTop.textColor = R.color.colorWhite64() + $0.valueBottom.textColor = R.color.colorWhite64() } override init(frame: CGRect) { @@ -32,9 +34,17 @@ final class LockCollectionViewCell: UICollectionViewCell { $0.edges.equalToSuperview().inset(UIEdgeInsets(top: 14, left: 24, bottom: 14, right: 0)) } } +} + +extension LockCollectionViewCell { + struct Model { + let title: String + let amount: String + let price: String? + } - func bind(title: String, value: String) { - view.titleView.text = title - view.valueView.text = value + func bind(viewModel: Model) { + view.titleView.text = viewModel.title + view.valueView.bind(topValue: viewModel.amount, bottomValue: viewModel.price) } } diff --git a/novawallet/Modules/YourWallets/GenericCollectionViewLayout.swift b/novawallet/Modules/YourWallets/GenericCollectionViewLayout.swift index c8b4cf5cb8..6e24c6852f 100644 --- a/novawallet/Modules/YourWallets/GenericCollectionViewLayout.swift +++ b/novawallet/Modules/YourWallets/GenericCollectionViewLayout.swift @@ -64,6 +64,7 @@ class GenericCollectionViewLayout: UIView { .createSectionLayoutWithFullWidthRow(settings: .init( estimatedRowHeight: settings.estimatedRowHeight, + absoluteHeaderHeight: settings.absoluteHeaderHeight, estimatedHeaderHeight: settings.estimatedSectionHeaderHeight, sectionContentInsets: settings.sectionContentInsets, sectionInterGroupSpacing: settings.interGroupSpacing, @@ -79,6 +80,7 @@ struct GenericCollectionViewLayoutSettings { var pinToVisibleBounds: Bool = true var estimatedHeaderHeight: CGFloat = 36 var estimatedRowHeight: CGFloat = 56 + var absoluteHeaderHeight: CGFloat? var estimatedSectionHeaderHeight: CGFloat = 46 var sectionContentInsets = NSDirectionalEdgeInsets( top: 0, diff --git a/novawalletTests/Mocks/CommonMocks.swift b/novawalletTests/Mocks/CommonMocks.swift index f45cdcbf7f..86fc0d81b1 100644 --- a/novawalletTests/Mocks/CommonMocks.swift +++ b/novawalletTests/Mocks/CommonMocks.swift @@ -5732,6 +5732,7 @@ import Cuckoo @testable import SoraKeystore import Foundation +import RobinHood import SubstrateSdk @@ -5760,9 +5761,9 @@ import SubstrateSdk - func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID? { - return cuckoo_manager.call("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", + return cuckoo_manager.call("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", parameters: (accountId, chainId, chainFormat, queue, closure, subscriptionHandlingFactory), escapingParameters: (accountId, chainId, chainFormat, queue, closure, subscriptionHandlingFactory), superclassCall: @@ -5820,9 +5821,9 @@ import SubstrateSdk - func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID? { - return cuckoo_manager.call("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", + return cuckoo_manager.call("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", parameters: (accountId, currencyId, chainId, queue, closure, subscriptionHandlingFactory), escapingParameters: (accountId, currencyId, chainId, queue, closure, subscriptionHandlingFactory), superclassCall: @@ -5857,9 +5858,9 @@ import SubstrateSdk } - func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ProtocolStubFunction<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionServiceProtocol.self, method: "attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) + func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ProtocolStubFunction<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == NativeTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionServiceProtocol.self, method: "attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) } func detachFromAccountInfo(for subscriptionId: M1, accountId: M2, chainId: M3, queue: M4, closure: M5) -> Cuckoo.ProtocolStubNoReturnFunction<(UUID, AccountId, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?)> where M1.MatchedType == UUID, M2.MatchedType == AccountId, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure { @@ -5877,9 +5878,9 @@ import SubstrateSdk return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionServiceProtocol.self, method: "detachFromAsset(for: UUID, accountId: AccountId, extras: StatemineAssetExtras, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?)", parameterMatchers: matchers)) } - func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ProtocolStubFunction<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionServiceProtocol.self, method: "attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) + func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ProtocolStubFunction<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == OrmlTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionServiceProtocol.self, method: "attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) } func detachFromOrmlToken(for subscriptionId: M1, accountId: M2, currencyId: M3, chainId: M4, queue: M5, closure: M6) -> Cuckoo.ProtocolStubNoReturnFunction<(UUID, AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?)> where M1.MatchedType == UUID, M2.MatchedType == AccountId, M3.MatchedType == Data, M4.MatchedType == ChainModel.Id, M5.OptionalMatchedType == DispatchQueue, M6.OptionalMatchedType == RemoteSubscriptionClosure { @@ -5904,9 +5905,9 @@ import SubstrateSdk @discardableResult - func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return cuckoo_manager.verify("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == NativeTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return cuckoo_manager.verify("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -5928,9 +5929,9 @@ import SubstrateSdk } @discardableResult - func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return cuckoo_manager.verify("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == OrmlTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return cuckoo_manager.verify("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -5950,7 +5951,7 @@ import SubstrateSdk - func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID? { return DefaultValueRegistry.defaultValue(for: (UUID?).self) } @@ -5974,7 +5975,7 @@ import SubstrateSdk - func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID? { return DefaultValueRegistry.defaultValue(for: (UUID?).self) } @@ -6013,9 +6014,9 @@ import SubstrateSdk - override func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + override func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID? { - return cuckoo_manager.call("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", + return cuckoo_manager.call("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", parameters: (accountId, chainId, chainFormat, queue, closure, subscriptionHandlingFactory), escapingParameters: (accountId, chainId, chainFormat, queue, closure, subscriptionHandlingFactory), superclassCall: @@ -6073,9 +6074,9 @@ import SubstrateSdk - override func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + override func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID? { - return cuckoo_manager.call("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", + return cuckoo_manager.call("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", parameters: (accountId, currencyId, chainId, queue, closure, subscriptionHandlingFactory), escapingParameters: (accountId, currencyId, chainId, queue, closure, subscriptionHandlingFactory), superclassCall: @@ -6110,9 +6111,9 @@ import SubstrateSdk } - func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ClassStubFunction<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionService.self, method: "attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) + func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ClassStubFunction<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == NativeTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionService.self, method: "attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) } func detachFromAccountInfo(for subscriptionId: M1, accountId: M2, chainId: M3, queue: M4, closure: M5) -> Cuckoo.ClassStubNoReturnFunction<(UUID, AccountId, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?)> where M1.MatchedType == UUID, M2.MatchedType == AccountId, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure { @@ -6130,9 +6131,9 @@ import SubstrateSdk return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionService.self, method: "detachFromAsset(for: UUID, accountId: AccountId, extras: StatemineAssetExtras, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?)", parameterMatchers: matchers)) } - func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ClassStubFunction<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionService.self, method: "attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) + func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.ClassStubFunction<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == OrmlTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return .init(stub: cuckoo_manager.createStub(for: MockWalletRemoteSubscriptionService.self, method: "attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", parameterMatchers: matchers)) } func detachFromOrmlToken(for subscriptionId: M1, accountId: M2, currencyId: M3, chainId: M4, queue: M5, closure: M6) -> Cuckoo.ClassStubNoReturnFunction<(UUID, AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?)> where M1.MatchedType == UUID, M2.MatchedType == AccountId, M3.MatchedType == Data, M4.MatchedType == ChainModel.Id, M5.OptionalMatchedType == DispatchQueue, M6.OptionalMatchedType == RemoteSubscriptionClosure { @@ -6157,9 +6158,9 @@ import SubstrateSdk @discardableResult - func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return cuckoo_manager.verify("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func attachToAccountInfo(of accountId: M1, chainId: M2, chainFormat: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == ChainModel.Id, M3.MatchedType == ChainFormat, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == NativeTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, ChainModel.Id, ChainFormat, DispatchQueue?, RemoteSubscriptionClosure?, NativeTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: chainId) { $0.1 }, wrap(matchable: chainFormat) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return cuckoo_manager.verify("attachToAccountInfo(of: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -6181,9 +6182,9 @@ import SubstrateSdk } @discardableResult - func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == RemoteSubscriptionHandlingFactoryProtocol { - let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, RemoteSubscriptionHandlingFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] - return cuckoo_manager.verify("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func attachToOrmlToken(of accountId: M1, currencyId: M2, chainId: M3, queue: M4, closure: M5, subscriptionHandlingFactory: M6) -> Cuckoo.__DoNotUse<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?), UUID?> where M1.MatchedType == AccountId, M2.MatchedType == Data, M3.MatchedType == ChainModel.Id, M4.OptionalMatchedType == DispatchQueue, M5.OptionalMatchedType == RemoteSubscriptionClosure, M6.OptionalMatchedType == OrmlTokenSubscriptionFactoryProtocol { + let matchers: [Cuckoo.ParameterMatcher<(AccountId, Data, ChainModel.Id, DispatchQueue?, RemoteSubscriptionClosure?, OrmlTokenSubscriptionFactoryProtocol?)>] = [wrap(matchable: accountId) { $0.0 }, wrap(matchable: currencyId) { $0.1 }, wrap(matchable: chainId) { $0.2 }, wrap(matchable: queue) { $0.3 }, wrap(matchable: closure) { $0.4 }, wrap(matchable: subscriptionHandlingFactory) { $0.5 }] + return cuckoo_manager.verify("attachToOrmlToken(of: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -6203,7 +6204,7 @@ import SubstrateSdk - override func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + override func attachToAccountInfo(of accountId: AccountId, chainId: ChainModel.Id, chainFormat: ChainFormat, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: NativeTokenSubscriptionFactoryProtocol?) -> UUID? { return DefaultValueRegistry.defaultValue(for: (UUID?).self) } @@ -6227,7 +6228,7 @@ import SubstrateSdk - override func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol?) -> UUID? { + override func attachToOrmlToken(of accountId: AccountId, currencyId: Data, chainId: ChainModel.Id, queue: DispatchQueue?, closure: RemoteSubscriptionClosure?, subscriptionHandlingFactory: OrmlTokenSubscriptionFactoryProtocol?) -> UUID? { return DefaultValueRegistry.defaultValue(for: (UUID?).self) } diff --git a/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift b/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift index 1a564a7387..a5bb4fe92f 100644 --- a/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift +++ b/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift @@ -60,4 +60,8 @@ final class WalletLocalSubscriptionFactoryStub: WalletLocalSubscriptionFactoryPr func getAllBalancesProvider() throws -> StreamableProvider { throw CommonError.undefined } + + func getLocksProvider(for accountId: AccountId) throws -> StreamableProvider { + throw CommonError.undefined + } } From 323a4701e6b28d64808a8eb8a5cd00bc1be9d25b Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 21 Sep 2022 14:31:53 +0300 Subject: [PATCH 20/52] fix review comments --- .../ExtrinsicProcessor+Events.swift | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift index 3ab6b8ce5e..1f1ce045f9 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/ExtrinsicProcessor+Events.swift @@ -9,13 +9,7 @@ extension ExtrinsicProcessor { context: RuntimeJsonContext ) throws -> BigUInt? { try eventRecords.first { record in - if - let eventPath = metadata.createEventCodingPath(from: record.event), - eventPath == EventCodingPath.balancesTransfer { - return true - } else { - return false - } + metadata.createEventCodingPath(from: record.event) == .balancesTransfer }.map { eventRecord in try eventRecord.event.params.map(to: BalancesTransferEvent.self, with: context.toRawContext()) }?.amount @@ -28,13 +22,8 @@ extension ExtrinsicProcessor { ) throws -> BigUInt? { let eventPaths: [EventCodingPath] = [.tokensTransfer, .currenciesTransferred] return try eventRecords.first { record in - if - let eventPath = metadata.createEventCodingPath(from: record.event), - eventPaths.contains(eventPath) { - return true - } else { - return false - } + let isTransferAll = metadata.createEventCodingPath(from: record.event).map { eventPaths.contains($0) } + return isTransferAll == true }.map { eventRecord in try eventRecord.event.params.map(to: TokenTransferedEvent.self, with: context.toRawContext()) }?.amount From ae32aaaccbdefe8aeb6f6d7b6e115f96d28fd5ac Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 21 Sep 2022 17:23:41 +0300 Subject: [PATCH 21/52] fix icon --- .../iconLock.imageset/iconLock.pdf | Bin 1472 -> 1480 bytes novawallet/Modules/Locks/LocksPresenter.swift | 2 +- .../Modules/Locks/View/LocksHeaderView.swift | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/novawallet/Assets.xcassets/iconLock.imageset/iconLock.pdf b/novawallet/Assets.xcassets/iconLock.imageset/iconLock.pdf index af52f11cb8e7f0266c82fcb1f58f22b28a884629..a0a792c2012200647e68fbc0405a811bd4363a55 100644 GIT binary patch literal 1480 zcmZvc-*3|}5XayBSKP~_4QYp7>6lQR5J zIiK2h_q`uJTP<%dkr#|12xz{2X8>1MaDB~uy$PR8&UyC1*W3OP6v1^k)%C3}4=h{y zU%Se$-`>FDZvItu>?cDmh6Q5WT&7+654qIg6eLm|rke?-O;^=Sa;*?;0ge|IrA9bR z3?)ZIDJ0}PN9U{o%*Ox>FLLKpF+&w|Tole)JA){=GuDuc(Hz3WqFVtKo1o=VY3U5? z86qDv37A94jmWie6GSzhE4We=DTSJlIod0NlLX~aNFJOdD7bYxS1QCBUBo;~m_kXY zqFkL8Mv}Q`M2y9v`cJ2#{Y0VxNwFmsillBxtN>PX_)-|#H#8R#I z?2%C&W7kL?vaSrZfDT(BUC2X(IhWKYn!1bn44u)Mx*{UStwdvU5|P-^v2`TEaT$7R ztl=q(cZ1k;ZggQtB<6)HsAWPq0eb+p-mG2(Ci5bC_OFN``v~@g`i*!qn}0dM0fE;{2h^*C39ja2<(!> zN%*4d%YCywz53kx2PU9kN7dmMs11|`!f_#WeOOQ@q$ti)vslYNQ;m# zpYOiA@6PA5_44)-dBzxmfcnQz25@x+$?B!MyYGt54S(2w$^ry8TF?X6*Tc(Pwe@aH zp7?)q)vkL2Mc{@ES=%+PII?8v{_IP)xx0bI{rs9Gt5Oys??jukPF)l9SYPM%p zmi9*eLyPh_c}BCiXv>PWbDJxRka49_s|6JXK*4)v0Vx+o>Qv64 zk{O;0YthWWoJ(m^Ora;!LPA7B$Q()*F~c!aRwEpk<9H|k zYlZ{@&lRmgwgZ>%RTO1X;>tA&H7b`>&#@&#eW*>Oa>Qcm4(y5bees8Y#8Ek@-bbdn zK&rf)FvXD$t?`02>P$;&aX^U87-@VRBj%$TZ44ZkO5Rn=V!58r8O=}Er*{pBE87dsb# z&u+UfZt53!nD;ES0AsVK)yTAQZT;Mot_6QDYy#b)t~ysyL_h2!xGoyPfI8VjH0+}U z4^3Cf$NiR41+QQOW1L0(OuqOg|CW#$lR07X0^1mH622(9;!y8SU0)ja$OL3;P~9Is zlcsdw2o4X@dG9x>2xXM#DP-wPy@vE_D@4p*Ss~JmGdgxnu|K$m`R;vKQaR4s`sgm; ne80UIijr*Vx&tq`FK}|d|K}+C{thjgu9xHImYtogzTEx?wG%un diff --git a/novawallet/Modules/Locks/LocksPresenter.swift b/novawallet/Modules/Locks/LocksPresenter.swift index e01553c186..3851361ee0 100644 --- a/novawallet/Modules/Locks/LocksPresenter.swift +++ b/novawallet/Modules/Locks/LocksPresenter.swift @@ -72,7 +72,7 @@ final class LocksPresenter { return LocksViewSectionModel( header: .init( - icon: R.image.iconBrowserSecurity(), + icon: R.image.iconLock(), title: R.string.localizable.walletBalanceLocked( preferredLanguages: selectedLocale.rLanguages ), diff --git a/novawallet/Modules/Locks/View/LocksHeaderView.swift b/novawallet/Modules/Locks/View/LocksHeaderView.swift index 49549ea5c9..c78da190d6 100644 --- a/novawallet/Modules/Locks/View/LocksHeaderView.swift +++ b/novawallet/Modules/Locks/View/LocksHeaderView.swift @@ -30,7 +30,7 @@ final class LocksHeaderView: UICollectionReusableView { } private func setup() { - view.titleView.imageView.contentMode = .scaleAspectFill + view.titleView.imageView.contentMode = .center view.titleView.imageView.tintColor = .white view.titleView.detailsView.titleView.font = .regularSubheadline view.titleView.detailsView.valueView.titleView.contentInsets = .init(top: 2, left: 8, bottom: 3, right: 8) From f2c8ac58b02074210f2933f852a71ad1580720eb Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 22 Sep 2022 14:40:04 +0400 Subject: [PATCH 22/52] init --- novawallet.xcodeproj/project.pbxproj | 4 +- .../Common/Migration/StorageMigrator.swift | 152 ++++++++++++++++ .../.xccurrentversion | 8 + .../SubstrateDataModel.xcdatamodel/contents | 11 +- .../SubstrateDataModel2.xcdatamodel/contents | 164 ++++++++++++++++++ .../Storage/SubstrateDataStorageFacade.swift | 46 ++++- .../Modules/Root/RootPresenterFactory.swift | 11 +- 7 files changed, 374 insertions(+), 22 deletions(-) create mode 100644 novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion create mode 100644 novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel2.xcdatamodel/contents diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 352f405af5..d714455d19 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -4888,6 +4888,7 @@ 8860F3E1289D4FFD00C0BF86 /* SectionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionProtocol.swift; sourceTree = ""; }; 8860F3E3289D50BA00C0BF86 /* Array+SectionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+SectionProtocol.swift"; sourceTree = ""; }; 8860F3E7289D7CF400C0BF86 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; + 88787F0328DB3A7B00B115AB /* SubstrateDataModel2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel2.xcdatamodel; sourceTree = ""; }; 887AFC8628BC95F0002A0422 /* MetaAccountChainResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaAccountChainResponse.swift; sourceTree = ""; }; 887AFC8928BCB313002A0422 /* PolkadotIconDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PolkadotIconDetailsView.swift; sourceTree = ""; }; 887AFC8A28BCB313002A0422 /* SelectableIconSubtitleCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectableIconSubtitleCollectionViewCell.swift; sourceTree = ""; }; @@ -16778,9 +16779,10 @@ 843910CA253F7E6500E3C217 /* SubstrateDataModel.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 88787F0328DB3A7B00B115AB /* SubstrateDataModel2.xcdatamodel */, 843910CB253F7E6500E3C217 /* SubstrateDataModel.xcdatamodel */, ); - currentVersion = 843910CB253F7E6500E3C217 /* SubstrateDataModel.xcdatamodel */; + currentVersion = 88787F0328DB3A7B00B115AB /* SubstrateDataModel2.xcdatamodel */; path = SubstrateDataModel.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/novawallet/Common/Migration/StorageMigrator.swift b/novawallet/Common/Migration/StorageMigrator.swift index b53addffb8..04cf4f6b5a 100644 --- a/novawallet/Common/Migration/StorageMigrator.swift +++ b/novawallet/Common/Migration/StorageMigrator.swift @@ -251,3 +251,155 @@ extension UserStorageMigrator: StorageMigrating { } } } + +final class SubstrateStorageMigrator { + let modelDirectory: String + let model: SubstrateStorageVersion + let storeURL: URL + let fileManager: FileManager + + init( + storeURL: URL, + modelDirectory: String, + model: SubstrateStorageVersion, + fileManager: FileManager + ) { + self.storeURL = storeURL + self.model = model + self.modelDirectory = modelDirectory + self.fileManager = fileManager + } +} + +extension SubstrateStorageMigrator: Migrating { + func migrate() throws { + guard requiresMigration() else { + return + } + migrate { + Logger.shared.info("Substrate storage migration was completed") + } + } +} + +extension SubstrateStorageMigrator: StorageMigrating { + func requiresMigration() -> Bool { + checkIfMigrationNeeded( + to: SubstrateStorageVersion.current, + storeURL: storeURL, + fileManager: fileManager, + modelDirectory: modelDirectory + ) + } + + private func performMigration() { + let destinationVersion = SubstrateStorageVersion.current + + let mom = createManagedObjectModel( + forResource: destinationVersion.rawValue, + modelDirectory: modelDirectory + ) + + let psc = NSPersistentStoreCoordinator(managedObjectModel: mom) + let options = [ + NSMigratePersistentStoresAutomaticallyOption: true, + NSInferMappingModelAutomaticallyOption: true + ] + do { + try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options) + } catch { + fatalError("Failed to add persistent store: \(error)") + } + } + + func migrate(_ completion: @escaping () -> Void) { + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + self?.performMigration() + + DispatchQueue.main.async { + completion() + } + } + } +} + +enum SubstrateStorageVersion: String, CaseIterable { + case version1 = "SubstrateDataModel" + case version2 = "SubstrateDataModel2" + + static var current: SubstrateStorageVersion { + allCases.last! + } + + var nextVersion: SubstrateStorageVersion? { + switch self { + case .version1: + return .version2 + case .version2: + return nil + } + } +} + +private extension StorageMigrating { + typealias Version = CaseIterable & RawRepresentable & Equatable + + func checkIfMigrationNeeded( + to version: T, + storeURL: URL, + fileManager: FileManager, + modelDirectory: String + ) -> Bool where T: Version, T.RawValue == String { + let storageExists = fileManager.fileExists(atPath: storeURL.path) + + guard storageExists else { + return false + } + + guard let metadata = NSPersistentStoreCoordinator.metadata(at: storeURL) else { + return false + } + + let compatibleVersion = T.allCases.first { + let model = createManagedObjectModel(forResource: $0.rawValue, modelDirectory: modelDirectory) + return model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) + } + + return compatibleVersion != version + } + + func compatibleVersionForStoreMetadata( + _ metadata: [String: Any], + modelDirectory: String + ) -> T? where T: Version, T.RawValue == String { + let compatibleVersion = T.allCases.first { + let model = createManagedObjectModel(forResource: $0.rawValue, modelDirectory: modelDirectory) + return model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) + } + + return compatibleVersion + } + + func createManagedObjectModel(forResource resource: String, modelDirectory: String) -> NSManagedObjectModel { + let bundle = Bundle.main + let omoURL = bundle.url( + forResource: resource, + withExtension: "omo", + subdirectory: modelDirectory + ) + + let momURL = bundle.url( + forResource: resource, + withExtension: "mom", + subdirectory: modelDirectory + ) + + guard + let modelURL = omoURL ?? momURL, + let model = NSManagedObjectModel(contentsOf: modelURL) else { + fatalError("Unable to load model in bundle for resource \(resource)") + } + + return model + } +} diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion new file mode 100644 index 0000000000..16a19db870 --- /dev/null +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + SubstrateDataModel2.xcdatamodel + + diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents index 27afd0afe8..52f09d918f 100644 --- a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -83,14 +83,6 @@ - - - - - - - - @@ -159,6 +151,5 @@ - \ No newline at end of file diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel2.xcdatamodel/contents b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel2.xcdatamodel/contents new file mode 100644 index 0000000000..27afd0afe8 --- /dev/null +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel2.xcdatamodel/contents @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/novawallet/Common/Storage/SubstrateDataStorageFacade.swift b/novawallet/Common/Storage/SubstrateDataStorageFacade.swift index af091c75e4..1b4a65e083 100644 --- a/novawallet/Common/Storage/SubstrateDataStorageFacade.swift +++ b/novawallet/Common/Storage/SubstrateDataStorageFacade.swift @@ -1,25 +1,53 @@ import RobinHood import CoreData +enum SubstrateStorageParams { + static let databaseName = "SubstrateDataModel.sqlite" + static let modelDirectory: String = "SubstrateDataModel.momd" + static let modelVersion: SubstrateStorageVersion = .version2 + + static let storageDirectoryURL: URL = { + let baseURL = FileManager.default.urls( + for: .documentDirectory, + in: .userDomainMask + ).first?.appendingPathComponent("CoreData") + + return baseURL! + }() + + static var storageURL: URL { + storageDirectoryURL + } +} + class SubstrateDataStorageFacade: StorageFacadeProtocol { static let shared = SubstrateDataStorageFacade() let databaseService: CoreDataServiceProtocol private init() { - let modelName = "SubstrateDataModel" - let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd") - let databaseName = "\(modelName).sqlite" + let databaseName = SubstrateStorageParams.databaseName + let modelName = SubstrateStorageParams.modelVersion.rawValue + let bundle = Bundle.main - let baseURL = FileManager.default.urls( - for: .documentDirectory, - in: .userDomainMask - ).first?.appendingPathComponent("CoreData") + let omoURL = bundle.url( + forResource: modelName, + withExtension: "omo", + subdirectory: SubstrateStorageParams.modelDirectory + ) + + let momURL = bundle.url( + forResource: modelName, + withExtension: "mom", + subdirectory: SubstrateStorageParams.modelDirectory + ) + + let modelURL = omoURL ?? momURL let persistentSettings = CoreDataPersistentSettings( - databaseDirectory: baseURL!, + databaseDirectory: SubstrateStorageParams.storageDirectoryURL, databaseName: databaseName, - incompatibleModelStrategy: .removeStore + incompatibleModelStrategy: .ignore ) let configuration = CoreDataServiceConfiguration( diff --git a/novawallet/Modules/Root/RootPresenterFactory.swift b/novawallet/Modules/Root/RootPresenterFactory.swift index 035375b6e7..b0b47a57b8 100644 --- a/novawallet/Modules/Root/RootPresenterFactory.swift +++ b/novawallet/Modules/Root/RootPresenterFactory.swift @@ -9,7 +9,7 @@ final class RootPresenterFactory: RootPresenterFactoryProtocol { let keychain = Keychain() let settings = SettingsManager.shared - let dbMigrator = UserStorageMigrator( + let userStorageMigrator = UserStorageMigrator( targetVersion: UserStorageParams.modelVersion, storeURL: UserStorageParams.storageURL, modelDirectory: UserStorageParams.modelDirectory, @@ -18,13 +18,20 @@ final class RootPresenterFactory: RootPresenterFactoryProtocol { fileManager: FileManager.default ) + let substrateSorageMigrator = SubstrateStorageMigrator( + storeURL: SubstrateStorageParams.storageURL, + modelDirectory: SubstrateStorageParams.modelDirectory, + model: SubstrateStorageParams.modelVersion, + fileManager: FileManager.default + ) + let interactor = RootInteractor( settings: SelectedWalletSettings.shared, keystore: keychain, applicationConfig: ApplicationConfig.shared, chainRegistry: ChainRegistryFacade.sharedRegistry, eventCenter: EventCenter.shared, - migrators: [dbMigrator], + migrators: [userStorageMigrator, substrateSorageMigrator], logger: Logger.shared ) From f5bdf8aa07334e1c748c6f27c4d1130053cec748 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 22 Sep 2022 15:27:07 +0400 Subject: [PATCH 23/52] small improvements --- novawallet.xcodeproj/project.pbxproj | 12 ++ .../StorageMigrating+CheckVersion.swift | 64 ++++++ .../Common/Migration/StorageMigrator.swift | 197 +----------------- .../Migration/SubstrateStorageMigrator.swift | 77 +++++++ .../Migration/SubstrateStorageVersion.swift | 17 ++ .../Storage/SubstrateDataStorageFacade.swift | 2 +- .../Modules/Root/RootPresenterFactory.swift | 4 +- 7 files changed, 179 insertions(+), 194 deletions(-) create mode 100644 novawallet/Common/Migration/StorageMigrating+CheckVersion.swift create mode 100644 novawallet/Common/Migration/SubstrateStorageMigrator.swift create mode 100644 novawallet/Common/Migration/SubstrateStorageVersion.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index d714455d19..9f6330992f 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -2107,6 +2107,9 @@ 882A5CED28AFCE3600D0D798 /* ReturnInIntervalsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882A5CEC28AFCE3600D0D798 /* ReturnInIntervalsViewModel.swift */; }; 882A5CEF28AFCE6000D0D798 /* FormattedReturnInIntervalsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882A5CEE28AFCE6000D0D798 /* FormattedReturnInIntervalsViewModel.swift */; }; 882AA13028AE64DC0093BC63 /* CrowdloanYourContributionsTotalCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882AA12F28AE64DC0093BC63 /* CrowdloanYourContributionsTotalCell.swift */; }; + 882C29AA28DC7B3D009CA4B6 /* SubstrateStorageMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882C29A928DC7B3D009CA4B6 /* SubstrateStorageMigrator.swift */; }; + 882C29AC28DC7B7F009CA4B6 /* SubstrateStorageVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882C29AB28DC7B7F009CA4B6 /* SubstrateStorageVersion.swift */; }; + 882C29AE28DC7CB4009CA4B6 /* StorageMigrating+CheckVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882C29AD28DC7CB4009CA4B6 /* StorageMigrating+CheckVersion.swift */; }; 8831F10028C65B95009F7682 /* AssetLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8831F0FF28C65B95009F7682 /* AssetLock.swift */; }; 8836AF4428AA293500A94EDD /* CurrencyManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8836AF4328AA293500A94EDD /* CurrencyManagerTests.swift */; }; 8836AF4828AA49AB00A94EDD /* Currency+btc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8836AF4728AA49AB00A94EDD /* Currency+btc.swift */; }; @@ -4863,6 +4866,9 @@ 882A5CEC28AFCE3600D0D798 /* ReturnInIntervalsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReturnInIntervalsViewModel.swift; sourceTree = ""; }; 882A5CEE28AFCE6000D0D798 /* FormattedReturnInIntervalsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormattedReturnInIntervalsViewModel.swift; sourceTree = ""; }; 882AA12F28AE64DC0093BC63 /* CrowdloanYourContributionsTotalCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanYourContributionsTotalCell.swift; sourceTree = ""; }; + 882C29A928DC7B3D009CA4B6 /* SubstrateStorageMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateStorageMigrator.swift; sourceTree = ""; }; + 882C29AB28DC7B7F009CA4B6 /* SubstrateStorageVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateStorageVersion.swift; sourceTree = ""; }; + 882C29AD28DC7CB4009CA4B6 /* StorageMigrating+CheckVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageMigrating+CheckVersion.swift"; sourceTree = ""; }; 8831F0FF28C65B95009F7682 /* AssetLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetLock.swift; sourceTree = ""; }; 8836AF4328AA293500A94EDD /* CurrencyManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyManagerTests.swift; sourceTree = ""; }; 8836AF4728AA49AB00A94EDD /* Currency+btc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Currency+btc.swift"; sourceTree = ""; }; @@ -12693,6 +12699,9 @@ 84CEAAF426D7ADF20021B881 /* KeystoreMigrator.swift */, 84CEAAF626D7B8010021B881 /* SettingsMigrator.swift */, 8457F90F26EB8288006803E1 /* StorageMigrator+Sync.swift */, + 882C29A928DC7B3D009CA4B6 /* SubstrateStorageMigrator.swift */, + 882C29AB28DC7B7F009CA4B6 /* SubstrateStorageVersion.swift */, + 882C29AD28DC7CB4009CA4B6 /* StorageMigrating+CheckVersion.swift */, ); path = Migration; sourceTree = ""; @@ -13839,6 +13848,7 @@ 88AC186528CA461F00892A9B /* ModalSheetCollectionViewProtocol.swift in Sources */, 8460E711284AB99E002896E9 /* ParaStkHintsViewModelFactory.swift in Sources */, 2A66CFAF25D10EDF0006E4C1 /* PhishingItem.swift in Sources */, + 882C29AA28DC7B3D009CA4B6 /* SubstrateStorageMigrator.swift in Sources */, 8487583E27F070B300495306 /* ApplicationSettingsPresentable.swift in Sources */, 84FEF3E32807E8000042CBE7 /* DAppBrowserPage.swift in Sources */, AEA2C1C02681E9EC0069492E /* ValidatorSearchViewLayout.swift in Sources */, @@ -14904,6 +14914,7 @@ 84B018AC26E01A4100C75E28 /* StakingStateView.swift in Sources */, 842A737C27DCC489006EE1EA /* OperationDetailsTransferView.swift in Sources */, 8472C5B2265CF9C500E2481B /* StakingRewardDestConfirmInteractor.swift in Sources */, + 882C29AE28DC7CB4009CA4B6 /* StorageMigrating+CheckVersion.swift in Sources */, 846CA77A27099B1E0011124C /* StakingAnalyticsLocalSubscriptionFactory.swift in Sources */, 84CEEDE2284E3DCE0039364A /* AccountDetailsNavigationCell.swift in Sources */, 8466781127EB4078007935D3 /* StackNetworkFeeCell.swift in Sources */, @@ -15635,6 +15646,7 @@ DE03CA5AD7F1D0B80DFF13B6 /* DAppBrowserViewController.swift in Sources */, 70C0E48EE41B4C7229F5946C /* DAppBrowserViewLayout.swift in Sources */, FDE2CA45061C620567AC329C /* DAppBrowserViewFactory.swift in Sources */, + 882C29AC28DC7B7F009CA4B6 /* SubstrateStorageVersion.swift in Sources */, 84BC7045289EFF44008A9758 /* TransactionDisplayCode.swift in Sources */, 1D1DC32EFF13F41677A084B7 /* DAppOperationConfirmProtocols.swift in Sources */, 841E5561282E9CC700C8438F /* ParaStkStateMachineProtocols.swift in Sources */, diff --git a/novawallet/Common/Migration/StorageMigrating+CheckVersion.swift b/novawallet/Common/Migration/StorageMigrating+CheckVersion.swift new file mode 100644 index 0000000000..a368ed647c --- /dev/null +++ b/novawallet/Common/Migration/StorageMigrating+CheckVersion.swift @@ -0,0 +1,64 @@ +import CoreData + +extension StorageMigrating { + typealias Version = CaseIterable & RawRepresentable & Equatable + + func checkIfMigrationNeeded( + to version: T, + storeURL: URL, + fileManager: FileManager, + modelDirectory: String + ) -> Bool where T: Version, T.RawValue == String { + let storageExists = fileManager.fileExists(atPath: storeURL.path) + + guard storageExists else { + return false + } + + guard let metadata = NSPersistentStoreCoordinator.metadata(at: storeURL) else { + return false + } + + let compatibleVersion = T.allCases.first { + let model = createManagedObjectModel(forResource: $0.rawValue, modelDirectory: modelDirectory) + return model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) + } + + return compatibleVersion != version + } + + func compatibleVersionForStoreMetadata( + _ metadata: [String: Any], + modelDirectory: String + ) -> T? where T: Version, T.RawValue == String { + let compatibleVersion = T.allCases.first { + let model = createManagedObjectModel(forResource: $0.rawValue, modelDirectory: modelDirectory) + return model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) + } + + return compatibleVersion + } + + func createManagedObjectModel(forResource resource: String, modelDirectory: String) -> NSManagedObjectModel { + let bundle = Bundle.main + let omoURL = bundle.url( + forResource: resource, + withExtension: "omo", + subdirectory: modelDirectory + ) + + let momURL = bundle.url( + forResource: resource, + withExtension: "mom", + subdirectory: modelDirectory + ) + + guard + let modelURL = omoURL ?? momURL, + let model = NSManagedObjectModel(contentsOf: modelURL) else { + fatalError("Unable to load model in bundle for resource \(resource)") + } + + return model + } +} diff --git a/novawallet/Common/Migration/StorageMigrator.swift b/novawallet/Common/Migration/StorageMigrator.swift index 04cf4f6b5a..52a257a304 100644 --- a/novawallet/Common/Migration/StorageMigrator.swift +++ b/novawallet/Common/Migration/StorageMigrator.swift @@ -141,51 +141,18 @@ final class UserStorageMigrator { } private func checkIfMigrationNeeded(to version: UserStorageVersion) -> Bool { - let storageExists = fileManager.fileExists(atPath: storeURL.path) - - guard storageExists else { - return false - } - - guard let metadata = NSPersistentStoreCoordinator.metadata(at: storeURL) else { - return false - } - - let compatibleVersion = compatibleVersionForStoreMetadata(metadata) - - return compatibleVersion != version + checkIfMigrationNeeded(to: version, + storeURL: storeURL, + fileManager: fileManager, + modelDirectory: modelDirectory) } private func compatibleVersionForStoreMetadata(_ metadata: [String: Any]) -> UserStorageVersion? { - let compatibleVersion = UserStorageVersion.allCases.first { - let model = createManagedObjectModel(forResource: $0.rawValue) - return model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) - } - - return compatibleVersion + compatibleVersionForStoreMetadata(metadata, modelDirectory: modelDirectory) } private func createManagedObjectModel(forResource resource: String) -> NSManagedObjectModel { - let bundle = Bundle.main - let omoURL = bundle.url( - forResource: resource, - withExtension: "omo", - subdirectory: modelDirectory - ) - - let momURL = bundle.url( - forResource: resource, - withExtension: "mom", - subdirectory: modelDirectory - ) - - guard - let modelURL = omoURL ?? momURL, - let model = NSManagedObjectModel(contentsOf: modelURL) else { - fatalError("Unable to load model in bundle for resource \(resource)") - } - - return model + createManagedObjectModel(forResource: resource, modelDirectory: modelDirectory) } private func createMapping( @@ -251,155 +218,3 @@ extension UserStorageMigrator: StorageMigrating { } } } - -final class SubstrateStorageMigrator { - let modelDirectory: String - let model: SubstrateStorageVersion - let storeURL: URL - let fileManager: FileManager - - init( - storeURL: URL, - modelDirectory: String, - model: SubstrateStorageVersion, - fileManager: FileManager - ) { - self.storeURL = storeURL - self.model = model - self.modelDirectory = modelDirectory - self.fileManager = fileManager - } -} - -extension SubstrateStorageMigrator: Migrating { - func migrate() throws { - guard requiresMigration() else { - return - } - migrate { - Logger.shared.info("Substrate storage migration was completed") - } - } -} - -extension SubstrateStorageMigrator: StorageMigrating { - func requiresMigration() -> Bool { - checkIfMigrationNeeded( - to: SubstrateStorageVersion.current, - storeURL: storeURL, - fileManager: fileManager, - modelDirectory: modelDirectory - ) - } - - private func performMigration() { - let destinationVersion = SubstrateStorageVersion.current - - let mom = createManagedObjectModel( - forResource: destinationVersion.rawValue, - modelDirectory: modelDirectory - ) - - let psc = NSPersistentStoreCoordinator(managedObjectModel: mom) - let options = [ - NSMigratePersistentStoresAutomaticallyOption: true, - NSInferMappingModelAutomaticallyOption: true - ] - do { - try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options) - } catch { - fatalError("Failed to add persistent store: \(error)") - } - } - - func migrate(_ completion: @escaping () -> Void) { - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - self?.performMigration() - - DispatchQueue.main.async { - completion() - } - } - } -} - -enum SubstrateStorageVersion: String, CaseIterable { - case version1 = "SubstrateDataModel" - case version2 = "SubstrateDataModel2" - - static var current: SubstrateStorageVersion { - allCases.last! - } - - var nextVersion: SubstrateStorageVersion? { - switch self { - case .version1: - return .version2 - case .version2: - return nil - } - } -} - -private extension StorageMigrating { - typealias Version = CaseIterable & RawRepresentable & Equatable - - func checkIfMigrationNeeded( - to version: T, - storeURL: URL, - fileManager: FileManager, - modelDirectory: String - ) -> Bool where T: Version, T.RawValue == String { - let storageExists = fileManager.fileExists(atPath: storeURL.path) - - guard storageExists else { - return false - } - - guard let metadata = NSPersistentStoreCoordinator.metadata(at: storeURL) else { - return false - } - - let compatibleVersion = T.allCases.first { - let model = createManagedObjectModel(forResource: $0.rawValue, modelDirectory: modelDirectory) - return model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) - } - - return compatibleVersion != version - } - - func compatibleVersionForStoreMetadata( - _ metadata: [String: Any], - modelDirectory: String - ) -> T? where T: Version, T.RawValue == String { - let compatibleVersion = T.allCases.first { - let model = createManagedObjectModel(forResource: $0.rawValue, modelDirectory: modelDirectory) - return model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) - } - - return compatibleVersion - } - - func createManagedObjectModel(forResource resource: String, modelDirectory: String) -> NSManagedObjectModel { - let bundle = Bundle.main - let omoURL = bundle.url( - forResource: resource, - withExtension: "omo", - subdirectory: modelDirectory - ) - - let momURL = bundle.url( - forResource: resource, - withExtension: "mom", - subdirectory: modelDirectory - ) - - guard - let modelURL = omoURL ?? momURL, - let model = NSManagedObjectModel(contentsOf: modelURL) else { - fatalError("Unable to load model in bundle for resource \(resource)") - } - - return model - } -} diff --git a/novawallet/Common/Migration/SubstrateStorageMigrator.swift b/novawallet/Common/Migration/SubstrateStorageMigrator.swift new file mode 100644 index 0000000000..663fa52f2b --- /dev/null +++ b/novawallet/Common/Migration/SubstrateStorageMigrator.swift @@ -0,0 +1,77 @@ +import Foundation +import CoreData + +final class SubstrateStorageMigrator { + let modelDirectory: String + let model: SubstrateStorageVersion + let storeURL: URL + let fileManager: FileManager + + init( + storeURL: URL, + modelDirectory: String, + model: SubstrateStorageVersion, + fileManager: FileManager + ) { + self.storeURL = storeURL + self.model = model + self.modelDirectory = modelDirectory + self.fileManager = fileManager + } +} + +// MARK: - Migrating + +extension SubstrateStorageMigrator: Migrating { + func migrate() throws { + guard requiresMigration() else { + return + } + migrate { + Logger.shared.info("Substrate storage migration was completed") + } + } +} + +// MARK: - StorageMigrating + +extension SubstrateStorageMigrator: StorageMigrating { + func requiresMigration() -> Bool { + checkIfMigrationNeeded( + to: SubstrateStorageVersion.current, + storeURL: storeURL, + fileManager: fileManager, + modelDirectory: modelDirectory + ) + } + + private func performMigration() { + let destinationVersion = SubstrateStorageVersion.current + + let mom = createManagedObjectModel( + forResource: destinationVersion.rawValue, + modelDirectory: modelDirectory + ) + + let psc = NSPersistentStoreCoordinator(managedObjectModel: mom) + let options = [ + NSMigratePersistentStoresAutomaticallyOption: true, + NSInferMappingModelAutomaticallyOption: true + ] + do { + try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options) + } catch { + fatalError("Failed to add persistent store: \(error)") + } + } + + func migrate(_ completion: @escaping () -> Void) { + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + self?.performMigration() + + DispatchQueue.main.async { + completion() + } + } + } +} diff --git a/novawallet/Common/Migration/SubstrateStorageVersion.swift b/novawallet/Common/Migration/SubstrateStorageVersion.swift new file mode 100644 index 0000000000..70e23a87f5 --- /dev/null +++ b/novawallet/Common/Migration/SubstrateStorageVersion.swift @@ -0,0 +1,17 @@ +enum SubstrateStorageVersion: String, CaseIterable { + case version1 = "SubstrateDataModel" + case version2 = "SubstrateDataModel2" + + static var current: SubstrateStorageVersion { + allCases.last! + } + + var nextVersion: SubstrateStorageVersion? { + switch self { + case .version1: + return .version2 + case .version2: + return nil + } + } +} diff --git a/novawallet/Common/Storage/SubstrateDataStorageFacade.swift b/novawallet/Common/Storage/SubstrateDataStorageFacade.swift index 1b4a65e083..2f131715ef 100644 --- a/novawallet/Common/Storage/SubstrateDataStorageFacade.swift +++ b/novawallet/Common/Storage/SubstrateDataStorageFacade.swift @@ -16,7 +16,7 @@ enum SubstrateStorageParams { }() static var storageURL: URL { - storageDirectoryURL + storageDirectoryURL.appendingPathComponent(databaseName) } } diff --git a/novawallet/Modules/Root/RootPresenterFactory.swift b/novawallet/Modules/Root/RootPresenterFactory.swift index b0b47a57b8..96417d8892 100644 --- a/novawallet/Modules/Root/RootPresenterFactory.swift +++ b/novawallet/Modules/Root/RootPresenterFactory.swift @@ -18,7 +18,7 @@ final class RootPresenterFactory: RootPresenterFactoryProtocol { fileManager: FileManager.default ) - let substrateSorageMigrator = SubstrateStorageMigrator( + let substrateStorageMigrator = SubstrateStorageMigrator( storeURL: SubstrateStorageParams.storageURL, modelDirectory: SubstrateStorageParams.modelDirectory, model: SubstrateStorageParams.modelVersion, @@ -31,7 +31,7 @@ final class RootPresenterFactory: RootPresenterFactoryProtocol { applicationConfig: ApplicationConfig.shared, chainRegistry: ChainRegistryFacade.sharedRegistry, eventCenter: EventCenter.shared, - migrators: [userStorageMigrator, substrateSorageMigrator], + migrators: [userStorageMigrator, substrateStorageMigrator], logger: Logger.shared ) From 17f5ecb0f91d1577aecef86029c86b5a2b3c69cf Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 22 Sep 2022 16:43:52 +0400 Subject: [PATCH 24/52] fix space --- novawallet/Common/Migration/StorageMigrator.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/novawallet/Common/Migration/StorageMigrator.swift b/novawallet/Common/Migration/StorageMigrator.swift index 52a257a304..3bbb603c61 100644 --- a/novawallet/Common/Migration/StorageMigrator.swift +++ b/novawallet/Common/Migration/StorageMigrator.swift @@ -141,10 +141,12 @@ final class UserStorageMigrator { } private func checkIfMigrationNeeded(to version: UserStorageVersion) -> Bool { - checkIfMigrationNeeded(to: version, - storeURL: storeURL, - fileManager: fileManager, - modelDirectory: modelDirectory) + checkIfMigrationNeeded( + to: version, + storeURL: storeURL, + fileManager: fileManager, + modelDirectory: modelDirectory + ) } private func compatibleVersionForStoreMetadata(_ metadata: [String: Any]) -> UserStorageVersion? { From fb5dcde857c5bb7b1573f6d684a9eaf6ba755e79 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Thu, 22 Sep 2022 17:59:58 +0400 Subject: [PATCH 25/52] added dataprovider and subscription --- ...ContributionLocalSubscriptionFactory.swift | 43 +++++++++++++++++ .../CrowdloansLocalStorageSubscriber.swift | 47 +++++++++++++++++++ .../Common/WalletsListInteractor.swift | 14 +++++- .../Manage/WalletManageInteractor.swift | 4 +- .../Manage/WalletManageViewFactory.swift | 1 + .../Selection/WalletSelectionInteractor.swift | 3 +- 6 files changed, 109 insertions(+), 3 deletions(-) diff --git a/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift index 3ed66b12fc..ec59dcedf4 100644 --- a/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift +++ b/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift @@ -6,6 +6,8 @@ protocol CrowdloanContributionLocalSubscriptionFactoryProtocol { for accountId: AccountId, chain: ChainModel ) -> StreamableProvider? + + func getAllLocalCrowdloanContributionDataProvider() -> StreamableProvider? } final class CrowdloanContributionLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, @@ -159,6 +161,47 @@ final class CrowdloanContributionLocalSubscriptionFactory: SubstrateLocalSubscri ) } } + + func getAllLocalCrowdloanContributionDataProvider() -> StreamableProvider? { + let cacheKey = "all-crowdloanContributions" + + if let provider = getProvider(for: cacheKey) as? StreamableProvider { + return provider + } + + let source = EmptyStreamableSource() + let mapper = CrowdloanContributionDataMapper() + let repository = storageFacade.createRepository( + filter: nil, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + + let observable = CoreDataContextObservable( + service: storageFacade.databaseService, + mapper: AnyCoreDataMapper(mapper), + predicate: { _ in + true + } + ) + + observable.start { [weak self] error in + if let error = error { + self?.logger.error("Did receive error: \(error)") + } + } + + let provider = StreamableProvider( + source: AnyStreamableSource(source), + repository: AnyDataProviderRepository(repository), + observable: AnyDataProviderRepositoryObservable(observable), + operationManager: operationManager + ) + + saveProvider(provider, for: cacheKey) + + return provider + } } extension CrowdloanContributionLocalSubscriptionFactory { diff --git a/novawallet/Common/DataProvider/Subscription/CrowdloansLocalStorageSubscriber.swift b/novawallet/Common/DataProvider/Subscription/CrowdloansLocalStorageSubscriber.swift index e11be7a2f1..e2fcf6197a 100644 --- a/novawallet/Common/DataProvider/Subscription/CrowdloansLocalStorageSubscriber.swift +++ b/novawallet/Common/DataProvider/Subscription/CrowdloansLocalStorageSubscriber.swift @@ -7,6 +7,8 @@ protocol CrowdloanContributionLocalSubscriptionHandler: AnyObject { accountId: AccountId, chain: ChainModel ) + + func handleAllCrowdloans(result: Result<[DataProviderChange], Error>) } protocol CrowdloansLocalStorageSubscriber: AnyObject { @@ -17,6 +19,8 @@ protocol CrowdloansLocalStorageSubscriber: AnyObject { for account: AccountId, chain: ChainModel ) -> StreamableProvider? + + func subscribeToAllCrowdloansProvider() -> StreamableProvider? } extension CrowdloansLocalStorageSubscriber { @@ -66,8 +70,51 @@ extension CrowdloansLocalStorageSubscriber { return provider } + + func subscribeToAllCrowdloansProvider() -> StreamableProvider? { + guard let provider = crowdloansLocalSubscriptionFactory.getAllLocalCrowdloanContributionDataProvider() else { + return nil + } + + let updateClosure = { [weak self] (changes: [DataProviderChange]) in + self?.crowdloansLocalSubscriptionHandler.handleAllCrowdloans(result: .success(changes)) + return + } + + let failureClosure = { [weak self] (error: Error) in + self?.crowdloansLocalSubscriptionHandler.handleAllCrowdloans(result: .failure(error)) + return + } + + let options = StreamableProviderObserverOptions( + alwaysNotifyOnRefresh: true, + waitsInProgressSyncOnAdd: false, + initialSize: 0, + refreshWhenEmpty: false + ) + + provider.addObserver( + self, + deliverOn: .main, + executing: updateClosure, + failing: failureClosure, + options: options + ) + + return provider + } } extension CrowdloansLocalStorageSubscriber where Self: CrowdloanContributionLocalSubscriptionHandler { var crowdloansLocalSubscriptionHandler: CrowdloanContributionLocalSubscriptionHandler { self } } + +extension CrowdloanContributionLocalSubscriptionHandler { + func handleCrowdloans( + result _: Result<[DataProviderChange], Error>, + accountId _: AccountId, + chain _: ChainModel + ) {} + + func handleAllCrowdloans(result _: Result<[DataProviderChange], Error>) {} +} diff --git a/novawallet/Modules/WalletsList/Common/WalletsListInteractor.swift b/novawallet/Modules/WalletsList/Common/WalletsListInteractor.swift index 2bc0125169..6a96f38f91 100644 --- a/novawallet/Modules/WalletsList/Common/WalletsListInteractor.swift +++ b/novawallet/Modules/WalletsList/Common/WalletsListInteractor.swift @@ -8,10 +8,12 @@ class WalletsListInteractor { let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol let walletListLocalSubscriptionFactory: WalletListLocalSubscriptionFactoryProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol + let crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol private(set) var priceSubscription: AnySingleValueProvider<[PriceData]>? private(set) var assetsSubscription: StreamableProvider? private(set) var walletsSubscription: StreamableProvider? + private(set) var crowdloansSubscription: StreamableProvider? private(set) var availableTokenPrice: [ChainAssetId: AssetModel.PriceId] = [:] init( @@ -19,12 +21,14 @@ class WalletsListInteractor { walletListLocalSubscriptionFactory: WalletListLocalSubscriptionFactoryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, - currencyManager: CurrencyManagerProtocol + currencyManager: CurrencyManagerProtocol, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol ) { self.chainRegistry = chainRegistry self.walletListLocalSubscriptionFactory = walletListLocalSubscriptionFactory self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory + self.crowdloansLocalSubscriptionFactory = crowdloansLocalSubscriptionFactory self.currencyManager = currencyManager } @@ -36,6 +40,10 @@ class WalletsListInteractor { assetsSubscription = subscribeAllBalancesProvider() } + private func subscribeToCrowdloans() { + crowdloansSubscription = subscribeToAllCrowdloansProvider() + } + private func subscribeChains() { chainRegistry.chainsSubscribe(self, runningInQueue: .main) { [weak self] changes in self?.basePresenter?.didReceiveChainChanges(changes) @@ -166,3 +174,7 @@ extension WalletsListInteractor: SelectedCurrencyDepending { updatePriceProvider(for: Set(availableTokenPrice.values), currency: selectedCurrency) } } + +extension WalletsListInteractor: CrowdloanContributionLocalSubscriptionHandler, CrowdloansLocalStorageSubscriber { + func handleAllCrowdloans(result _: Result<[DataProviderChange], Error>) {} +} diff --git a/novawallet/Modules/WalletsList/Manage/WalletManageInteractor.swift b/novawallet/Modules/WalletsList/Manage/WalletManageInteractor.swift index 8620e68255..a61fb57bcc 100644 --- a/novawallet/Modules/WalletsList/Manage/WalletManageInteractor.swift +++ b/novawallet/Modules/WalletsList/Manage/WalletManageInteractor.swift @@ -26,6 +26,7 @@ final class WalletManageInteractor: WalletsListInteractor { selectedWalletSettings: SelectedWalletSettings, eventCenter: EventCenterProtocol, currencyManager: CurrencyManagerProtocol, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol, operationQueue: OperationQueue ) { self.repository = repository @@ -38,7 +39,8 @@ final class WalletManageInteractor: WalletsListInteractor { walletListLocalSubscriptionFactory: walletListLocalSubscriptionFactory, walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, - currencyManager: currencyManager + currencyManager: currencyManager, + crowdloansLocalSubscriptionFactory: crowdloansLocalSubscriptionFactory ) } diff --git a/novawallet/Modules/WalletsList/Manage/WalletManageViewFactory.swift b/novawallet/Modules/WalletsList/Manage/WalletManageViewFactory.swift index 1d64b497f7..951ac05398 100644 --- a/novawallet/Modules/WalletsList/Manage/WalletManageViewFactory.swift +++ b/novawallet/Modules/WalletsList/Manage/WalletManageViewFactory.swift @@ -61,6 +61,7 @@ final class WalletManageViewFactory { selectedWalletSettings: SelectedWalletSettings.shared, eventCenter: EventCenter.shared, currencyManager: currencyManager, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactory.shared, operationQueue: OperationManagerFacade.sharedDefaultQueue ) } diff --git a/novawallet/Modules/WalletsList/Selection/WalletSelectionInteractor.swift b/novawallet/Modules/WalletsList/Selection/WalletSelectionInteractor.swift index 350e84f1da..b3d63f8fe9 100644 --- a/novawallet/Modules/WalletsList/Selection/WalletSelectionInteractor.swift +++ b/novawallet/Modules/WalletsList/Selection/WalletSelectionInteractor.swift @@ -31,7 +31,8 @@ final class WalletSelectionInteractor: WalletsListInteractor { walletListLocalSubscriptionFactory: walletListLocalSubscriptionFactory, walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, - currencyManager: currencyManager + currencyManager: currencyManager, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactory.shared ) } } From 497d7dcc80038eea0a9ebc03fee5193129c6a0c6 Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 23 Sep 2022 10:36:37 +0500 Subject: [PATCH 26/52] base balance context --- novawallet/Modules/Wallet/Model/BalanceContext.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/novawallet/Modules/Wallet/Model/BalanceContext.swift b/novawallet/Modules/Wallet/Model/BalanceContext.swift index 64df7660bb..820e1701d0 100644 --- a/novawallet/Modules/Wallet/Model/BalanceContext.swift +++ b/novawallet/Modules/Wallet/Model/BalanceContext.swift @@ -8,10 +8,12 @@ struct BalanceContext { static let priceChangeKey = "account.balance.price.change.key" static let priceIdKey = "account.balance.price.id.key" static let balanceLocksKey = "account.balance.locks.key" + static let crowdloans = "account.balance.crowdloan.key" let free: Decimal let reserved: Decimal let frozen: Decimal + let crowdloans: Decimal let price: Decimal let priceChange: Decimal let priceId: Int? From 7dd8c6a88063ec6a34b9bdbb6ffac89caa636783 Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 23 Sep 2022 14:50:56 +0500 Subject: [PATCH 27/52] add crowdloans to asset details --- novawallet.xcodeproj/project.pbxproj | 14 +- .../Helpers/SubstrateRepositoryFactory.swift | 24 +++ .../Types => Model}/AssetLock.swift | 9 +- ...Locks+Sort.swift => AssetLocks+Sort.swift} | 12 +- .../WalletNetworkFacade+BalanceLocks.swift | 154 ------------------ .../Wallet/WalletNetworkFacade+Storage.swift | 44 +++-- .../Common/Substrate/Types/BalanceLock.swift | 2 + .../ModalPicker/ModalInfoFactory.swift | 43 ++++- novawallet/Modules/Locks/LocksPresenter.swift | 3 +- .../Modules/Wallet/Model/BalanceContext.swift | 31 +++- novawallet/en.lproj/Localizable.strings | 3 +- novawallet/ru.lproj/Localizable.strings | 3 +- 12 files changed, 142 insertions(+), 200 deletions(-) rename novawallet/Common/{Substrate/Types => Model}/AssetLock.swift (85%) rename novawallet/Common/Model/{BalanceLocks+Sort.swift => AssetLocks+Sort.swift} (54%) delete mode 100644 novawallet/Common/Network/Wallet/WalletNetworkFacade+BalanceLocks.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index fc77f3a9e9..b126a35daf 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -104,7 +104,7 @@ 2AC7BC7E2731604C001D99B0 /* ChainAccountChanged.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC7BC7D2731604B001D99B0 /* ChainAccountChanged.swift */; }; 2AC7BC8027319FC7001D99B0 /* BalanceLockType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC7BC7F27319FC7001D99B0 /* BalanceLockType.swift */; }; 2AC7BC822731A1A1001D99B0 /* BalanceLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC7BC812731A1A1001D99B0 /* BalanceLock.swift */; }; - 2AC7BC842731A214001D99B0 /* BalanceLocks+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC7BC832731A214001D99B0 /* BalanceLocks+Sort.swift */; }; + 2AC7BC842731A214001D99B0 /* AssetLocks+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC7BC832731A214001D99B0 /* AssetLocks+Sort.swift */; }; 2AC7BC8927343484001D99B0 /* BottomSheetInfoBalanceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC7BC8827343483001D99B0 /* BottomSheetInfoBalanceCell.swift */; }; 2AC7BC8B273435CE001D99B0 /* BottomSheetInfoTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AC7BC8A273435CE001D99B0 /* BottomSheetInfoTableCell.swift */; }; 2AD0A16A25D3854700312428 /* TransferConfirmCommandProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AD0A16925D3854700312428 /* TransferConfirmCommandProxy.swift */; }; @@ -1549,7 +1549,6 @@ 84B73AD6279B4E0B0071AE16 /* AssetDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B73AD5279B4E0B0071AE16 /* AssetDetails.swift */; }; 84B73AD8279C2EDA0071AE16 /* AssetAccountSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B73AD7279C2EDA0071AE16 /* AssetAccountSubscription.swift */; }; 84B73ADA279D265A0071AE16 /* AssetsSubscriptionHandlingFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B73AD9279D265A0071AE16 /* AssetsSubscriptionHandlingFactory.swift */; }; - 84B73ADC279D7C100071AE16 /* WalletNetworkFacade+BalanceLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B73ADB279D7C0F0071AE16 /* WalletNetworkFacade+BalanceLocks.swift */; }; 84B73ADE279D90BD0071AE16 /* AssetsTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B73ADD279D90BD0071AE16 /* AssetsTransfer.swift */; }; 84B73AE0279E6A600071AE16 /* FeeMetadataContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B73ADF279E6A600071AE16 /* FeeMetadataContext.swift */; }; 84B73AE2279E95810071AE16 /* TransferInfoContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B73AE1279E95810071AE16 /* TransferInfoContext.swift */; }; @@ -2844,7 +2843,7 @@ 2AC7BC7D2731604B001D99B0 /* ChainAccountChanged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainAccountChanged.swift; sourceTree = ""; }; 2AC7BC7F27319FC7001D99B0 /* BalanceLockType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceLockType.swift; sourceTree = ""; }; 2AC7BC812731A1A1001D99B0 /* BalanceLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceLock.swift; sourceTree = ""; }; - 2AC7BC832731A214001D99B0 /* BalanceLocks+Sort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BalanceLocks+Sort.swift"; sourceTree = ""; }; + 2AC7BC832731A214001D99B0 /* AssetLocks+Sort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetLocks+Sort.swift"; sourceTree = ""; }; 2AC7BC8827343483001D99B0 /* BottomSheetInfoBalanceCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetInfoBalanceCell.swift; sourceTree = ""; }; 2AC7BC8A273435CE001D99B0 /* BottomSheetInfoTableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetInfoTableCell.swift; sourceTree = ""; }; 2ACA4A5B186EE6D40BFE9D66 /* ExportMnemonicWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportMnemonicWireframe.swift; sourceTree = ""; }; @@ -4304,7 +4303,6 @@ 84B73AD5279B4E0B0071AE16 /* AssetDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetDetails.swift; sourceTree = ""; }; 84B73AD7279C2EDA0071AE16 /* AssetAccountSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetAccountSubscription.swift; sourceTree = ""; }; 84B73AD9279D265A0071AE16 /* AssetsSubscriptionHandlingFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsSubscriptionHandlingFactory.swift; sourceTree = ""; }; - 84B73ADB279D7C0F0071AE16 /* WalletNetworkFacade+BalanceLocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletNetworkFacade+BalanceLocks.swift"; sourceTree = ""; }; 84B73ADD279D90BD0071AE16 /* AssetsTransfer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsTransfer.swift; sourceTree = ""; }; 84B73ADF279E6A600071AE16 /* FeeMetadataContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeMetadataContext.swift; sourceTree = ""; }; 84B73AE1279E95810071AE16 /* TransferInfoContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferInfoContext.swift; sourceTree = ""; }; @@ -7104,7 +7102,6 @@ 84F1CB3F27CF6BEF0095D523 /* UniquesClassDetails.swift */, 8430D6C42800040A00FFB6AE /* EthereumExecuted.swift */, 84A3B8A12836DA2600DE2669 /* LastAccountIdKeyWrapper.swift */, - 8831F0FF28C65B95009F7682 /* AssetLock.swift */, 847A25B828D7BB1F006AC9F5 /* BalancesTransferEvent.swift */, 847A25BC28D7C0E7006AC9F5 /* TokenTransferedEvent.swift */, ); @@ -8599,7 +8596,6 @@ 84F13F0926F14122006725FF /* ChainAsset.swift */, 841AAC2826F6A59C00F0A25E /* MultiassetCryptoType.swift */, 2AC7BC7F27319FC7001D99B0 /* BalanceLockType.swift */, - 2AC7BC832731A214001D99B0 /* BalanceLocks+Sort.swift */, 849A4EF3279A7AC600AB6709 /* AssetType.swift */, 849A4EF5279A7AEF00AB6709 /* StateminAssetExtras.swift */, 849A4EF7279ABBDD00AB6709 /* AssetBalance.swift */, @@ -8616,6 +8612,8 @@ 84BC7044289EFF44008A9758 /* TransactionDisplayCode.swift */, 84BC7046289EFFFA008A9758 /* ChainWalletDisplayAddress.swift */, 887AFC8628BC95F0002A0422 /* MetaAccountChainResponse.swift */, + 8831F0FF28C65B95009F7682 /* AssetLock.swift */, + 2AC7BC832731A214001D99B0 /* AssetLocks+Sort.swift */, ); path = Model; sourceTree = ""; @@ -9492,7 +9490,6 @@ isa = PBXGroup; children = ( AEE1207B26565CDB003EE80C /* WalletNetworkOperationFactory+MinimalBalance.swift */, - 84B73ADB279D7C0F0071AE16 /* WalletNetworkFacade+BalanceLocks.swift */, 8490150224AB6C01008F705E /* WalletNetworkOperationFactory.swift */, 84C7435A251DC504009576C6 /* WalletNetworkOperationFactory+Protocol.swift */, 846AF83D2525B85100868F37 /* WalletNetworkFacade.swift */, @@ -13936,7 +13933,6 @@ 84D6D7F827A7DE120094FC33 /* AssetListAccountCell.swift in Sources */, 84F2FEFF25E7ADE7008338D5 /* ValidatorPrefs.swift in Sources */, 84BE209E25E85CCA00B4748C /* ServiceCoordinator.swift in Sources */, - 84B73ADC279D7C100071AE16 /* WalletNetworkFacade+BalanceLocks.swift in Sources */, 84D1F31E260F585E0077DDFE /* AddAccount.swift in Sources */, 846CA77C27099DD90011124C /* WeaklyAnalyticsRewardSource.swift in Sources */, 84452F9D25D6768000F47EC5 /* RuntimeMetadataItem.swift in Sources */, @@ -15560,7 +15556,7 @@ 84216FD42827982800479375 /* SelectedRoundCollators.swift in Sources */, 849E17DC27909179002D1744 /* DAppSearchQueryTableViewCell.swift in Sources */, 76F74188F16A370D79033A12 /* AnalyticsRewardDetailsWireframe.swift in Sources */, - 2AC7BC842731A214001D99B0 /* BalanceLocks+Sort.swift in Sources */, + 2AC7BC842731A214001D99B0 /* AssetLocks+Sort.swift in Sources */, 848CCB4E2833CC4D00A1FD00 /* StakingMainStaticViewModel.swift in Sources */, F17C7FA0DB540A803558D1BB /* AnalyticsRewardDetailsPresenter.swift in Sources */, EB544E8D26ABEE4ADE2F939F /* AnalyticsRewardDetailsInteractor.swift in Sources */, diff --git a/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift b/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift index 07859386da..975752eab1 100644 --- a/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift +++ b/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift @@ -46,6 +46,11 @@ protocol SubstrateRepositoryFactoryProtocol { chainId: ChainModel.Id, source: String? ) -> AnyDataProviderRepository + + func createCrowdloanContributionRepository( + accountId: AccountId, + chainId: ChainModel.Id + ) -> AnyDataProviderRepository } final class SubstrateRepositoryFactory: SubstrateRepositoryFactoryProtocol { @@ -220,6 +225,25 @@ final class SubstrateRepositoryFactory: SubstrateRepositoryFactoryProtocol { accountId: accountId, source: source ) + + return createCrowdloanContributionRepository(for: filter) + } + + func createCrowdloanContributionRepository( + accountId: AccountId, + chainId: ChainModel.Id + ) -> AnyDataProviderRepository { + let filter = NSPredicate.crowdloanContribution( + for: chainId, + accountId: accountId + ) + + return createCrowdloanContributionRepository(for: filter) + } + + private func createCrowdloanContributionRepository( + for filter: NSPredicate + ) -> AnyDataProviderRepository { let mapper = CrowdloanContributionDataMapper() let repository = storageFacade.createRepository( filter: filter, diff --git a/novawallet/Common/Substrate/Types/AssetLock.swift b/novawallet/Common/Model/AssetLock.swift similarity index 85% rename from novawallet/Common/Substrate/Types/AssetLock.swift rename to novawallet/Common/Model/AssetLock.swift index 4b983615f5..b0fa19f029 100644 --- a/novawallet/Common/Substrate/Types/AssetLock.swift +++ b/novawallet/Common/Model/AssetLock.swift @@ -8,12 +8,15 @@ struct AssetLock: Equatable { let amount: BigUInt var lockType: LockType? { - guard let typeString = - String(data: type, encoding: .utf8)?.trimmingCharacters(in: .whitespaces) else { + guard let typeString = displayId else { return nil } return LockType(rawValue: typeString.lowercased()) } + + var displayId: String? { + String(data: type, encoding: .utf8)?.trimmingCharacters(in: .whitespaces) + } } extension AssetLock: Identifiable { @@ -45,3 +48,5 @@ extension AssetLock: CustomDebugStringConvertible { ].joined(separator: "\n") } } + +extension AssetLock: Codable {} diff --git a/novawallet/Common/Model/BalanceLocks+Sort.swift b/novawallet/Common/Model/AssetLocks+Sort.swift similarity index 54% rename from novawallet/Common/Model/BalanceLocks+Sort.swift rename to novawallet/Common/Model/AssetLocks+Sort.swift index b2f562f197..58424c44d4 100644 --- a/novawallet/Common/Model/BalanceLocks+Sort.swift +++ b/novawallet/Common/Model/AssetLocks+Sort.swift @@ -1,19 +1,19 @@ import Foundation -typealias BalanceLocks = [BalanceLock] +typealias AssetLocks = [AssetLock] -extension BalanceLocks { - func mainLocks() -> BalanceLocks { +extension AssetLocks { + func mainLocks() -> AssetLocks { LockType.locksOrder.compactMap { lockType in self.first(where: { lock in - lock.displayId == lockType.rawValue + lock.lockType == lockType }) } } - func auxLocks() -> BalanceLocks { + func auxLocks() -> AssetLocks { compactMap { lock in - guard LockType(rawValue: lock.displayId ?? "") != nil else { + guard lock.lockType != nil else { return lock } diff --git a/novawallet/Common/Network/Wallet/WalletNetworkFacade+BalanceLocks.swift b/novawallet/Common/Network/Wallet/WalletNetworkFacade+BalanceLocks.swift deleted file mode 100644 index 25e56409eb..0000000000 --- a/novawallet/Common/Network/Wallet/WalletNetworkFacade+BalanceLocks.swift +++ /dev/null @@ -1,154 +0,0 @@ -import Foundation -import RobinHood -import SubstrateSdk - -extension WalletNetworkFacade { - func createBalanceLocksFetchOperation( - for accountId: AccountId, - asset: AssetModel, - chainId: ChainModel.Id, - chainFormat: ChainFormat - ) -> CompoundOperationWrapper { - if let rawType = asset.type { - switch AssetType(rawValue: rawType) { - case .none, .statemine: - return CompoundOperationWrapper.createWithResult(nil) - case .orml: - return createOrmlBalanceLocksWrapper( - for: accountId, - asset: asset, - chainId: chainId - ) - } - } else { - return createNativeBalanceLocksWrapper( - for: accountId, - chainId: chainId, - chainFormat: chainFormat - ) - } - } - - private func createNativeBalanceLocksWrapper( - for accountId: AccountId, - chainId: ChainModel.Id, - chainFormat: ChainFormat - ) -> CompoundOperationWrapper { - let operationManager = OperationManagerFacade.sharedManager - - let requestFactory = StorageRequestFactory( - remoteFactory: StorageKeyFactory(), - operationManager: operationManager - ) - - guard let connection = chainRegistry.getConnection(for: chainId) else { - return CompoundOperationWrapper.createWithError(ChainRegistryError.connectionUnavailable) - } - - guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { - return CompoundOperationWrapper.createWithError(ChainRegistryError.runtimeMetadaUnavailable) - } - - let coderFactoryOperation = runtimeService.fetchCoderFactoryOperation() - - let wrapper: CompoundOperationWrapper<[StorageResponse]> - - switch chainFormat { - case .substrate: - wrapper = requestFactory.queryItems( - engine: connection, - keyParams: { [accountId] }, - factory: { try coderFactoryOperation.extractNoCancellableResultData() }, - storagePath: StorageCodingPath.balanceLocks - ) - case .ethereum: - wrapper = requestFactory.queryItems( - engine: connection, - keyParams: { [accountId.map { StringScaleMapper(value: $0) }] }, - factory: { try coderFactoryOperation.extractNoCancellableResultData() }, - storagePath: StorageCodingPath.balanceLocks - ) - } - - let mapOperation = ClosureOperation { - try wrapper.targetOperation.extractNoCancellableResultData().first?.value - } - - wrapper.allOperations.forEach { $0.addDependency(coderFactoryOperation) } - - let dependencies = [coderFactoryOperation] + wrapper.allOperations - - dependencies.forEach { mapOperation.addDependency($0) } - - return CompoundOperationWrapper(targetOperation: mapOperation, dependencies: dependencies) - } - - private func createOrmlBalanceLocksWrapper( - for accountId: AccountId, - asset: AssetModel, - chainId: ChainModel.Id - ) -> CompoundOperationWrapper { - guard - let extras = try? asset.typeExtras?.map(to: OrmlTokenExtras.self), - let currencyId = try? Data(hexString: extras.currencyIdScale) else { - return CompoundOperationWrapper.createWithResult(nil) - } - - let operationManager = OperationManagerFacade.sharedManager - - let storageKeyFactory = StorageKeyFactory() - let requestFactory = StorageRequestFactory( - remoteFactory: StorageKeyFactory(), - operationManager: operationManager - ) - - guard let connection = chainRegistry.getConnection(for: chainId) else { - return CompoundOperationWrapper.createWithError(ChainRegistryError.connectionUnavailable) - } - - guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { - return CompoundOperationWrapper.createWithError(ChainRegistryError.runtimeMetadaUnavailable) - } - - let coderFactoryOperation = runtimeService.fetchCoderFactoryOperation() - - let storagePath = StorageCodingPath.ormlTokenLocks - let keyEncodingOperation = DoubleMapKeyEncodingOperation( - path: storagePath, - storageKeyFactory: storageKeyFactory, - keyParams1: [accountId], - keyParams2: [currencyId], - param1Encoder: nil, - param2Encoder: { $0 } - ) - - keyEncodingOperation.configurationBlock = { - do { - keyEncodingOperation.codingFactory = try coderFactoryOperation - .extractNoCancellableResultData() - } catch { - keyEncodingOperation.result = .failure(error) - } - } - - let wrapper: CompoundOperationWrapper<[StorageResponse]> = requestFactory.queryItems( - engine: connection, - keys: { try keyEncodingOperation.extractNoCancellableResultData() }, - factory: { try coderFactoryOperation.extractNoCancellableResultData() }, - storagePath: storagePath - ) - - let mapOperation = ClosureOperation { - try wrapper.targetOperation.extractNoCancellableResultData().first?.value - } - - keyEncodingOperation.addDependency(coderFactoryOperation) - wrapper.addDependency(operations: [keyEncodingOperation]) - - let dependencies = [coderFactoryOperation, keyEncodingOperation] + wrapper.allOperations - - dependencies.forEach { mapOperation.addDependency($0) } - - return CompoundOperationWrapper(targetOperation: mapOperation, dependencies: dependencies) - } -} diff --git a/novawallet/Common/Network/Wallet/WalletNetworkFacade+Storage.swift b/novawallet/Common/Network/Wallet/WalletNetworkFacade+Storage.swift index d31e2eb2ba..8a7da6ef2f 100644 --- a/novawallet/Common/Network/Wallet/WalletNetworkFacade+Storage.swift +++ b/novawallet/Common/Network/Wallet/WalletNetworkFacade+Storage.swift @@ -2,6 +2,7 @@ import Foundation import CommonWallet import RobinHood import SubstrateSdk +import BigInt extension WalletNetworkFacade { func fetchBalanceInfoForAsset( @@ -36,21 +37,30 @@ extension WalletNetworkFacade { options: RepositoryFetchOptions() ) - let balanceLocksWrapper: CompoundOperationWrapper<[BalanceLock]?> = - createBalanceLocksFetchOperation( - for: selectedAccount.accountId, - asset: remoteAsset, - chainId: chain.chainId, - chainFormat: chain.chainFormat - ) + let locksRepository = repositoryFactory.createAssetLocksRepository( + for: selectedAccount.accountId, + chainAssetId: ChainAssetId(chainId: chain.chainId, assetId: remoteAsset.assetId) + ) + + let contributionsRepository = repositoryFactory.createCrowdloanContributionRepository( + accountId: selectedAccount.accountId, + chainId: chain.chainId + ) + + let balanceLocksOperation = locksRepository.fetchAllOperation(with: RepositoryFetchOptions()) + + let crowdloanContributionsOperation = contributionsRepository.fetchAllOperation( + with: RepositoryFetchOptions() + ) let mappingOperation = createBalanceMappingOperation( asset: asset, dependingOn: balanceOperation, - balanceLocksWrapper: balanceLocksWrapper + balanceLocksOperation: balanceLocksOperation, + crowdloanContributionsOperation: crowdloanContributionsOperation ) - let storageOperations = [balanceOperation] + balanceLocksWrapper.allOperations + let storageOperations = [balanceOperation, balanceLocksOperation, crowdloanContributionsOperation] storageOperations.forEach { storageOperation in storageOperation.addDependency(codingFactoryOperation) @@ -82,7 +92,8 @@ extension WalletNetworkFacade { private func createBalanceMappingOperation( asset: WalletAsset, dependingOn balanceOperation: BaseOperation, - balanceLocksWrapper: CompoundOperationWrapper<[BalanceLock]?> + balanceLocksOperation: BaseOperation<[AssetLock]>, + crowdloanContributionsOperation: BaseOperation<[CrowdloanContributionData]> ) -> BaseOperation { ClosureOperation { let maybeAssetBalance = try balanceOperation.extractNoCancellableResultData() @@ -90,10 +101,17 @@ extension WalletNetworkFacade { if let assetBalance = maybeAssetBalance { context = context.byChangingAssetBalance(assetBalance, precision: asset.precision) + } + + let balanceLocks = try balanceLocksOperation.extractNoCancellableResultData() + context = context.byChangingBalanceLocks(balanceLocks) + + let contributions = try crowdloanContributionsOperation.extractNoCancellableResultData() + + let contributionsInPlank = contributions.reduce(BigUInt(0)) { $0 + $1.amount } - if let balanceLocks = try? balanceLocksWrapper.targetOperation.extractNoCancellableResultData() { - context = context.byChangingBalanceLocks(balanceLocks) - } + if let contributionsDecimal = Decimal.fromSubstrateAmount(contributionsInPlank, precision: asset.precision) { + context = context.byChangingCrowdloans(contributionsDecimal) } let balance = BalanceData( diff --git a/novawallet/Common/Substrate/Types/BalanceLock.swift b/novawallet/Common/Substrate/Types/BalanceLock.swift index dae4e81cdd..7d3cd30b0c 100644 --- a/novawallet/Common/Substrate/Types/BalanceLock.swift +++ b/novawallet/Common/Substrate/Types/BalanceLock.swift @@ -19,3 +19,5 @@ struct BalanceLock: Codable, Equatable { )?.trimmingCharacters(in: .whitespaces) } } + +typealias BalanceLocks = [BalanceLock] diff --git a/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift b/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift index 30c17ffa5f..6a8f9fecbc 100644 --- a/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift +++ b/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift @@ -238,7 +238,6 @@ struct ModalInfoFactory { priceFormatter: LocalizableResource, precision: Int16 ) -> [LocalizableResource] { - print(balanceContext.toContext()) let staticModels: [LocalizableResource] = [ LocalizableResource { locale in let title = R.string.localizable @@ -260,6 +259,12 @@ struct ModalInfoFactory { } ] + let crowdloans = createCrowdloansViewModel( + balanceContext: balanceContext, + amountFormatter: amountFormatter, + priceFormatter: priceFormatter + ) + let balanceLockKnownModels: [LocalizableResource] = createLockViewModel( from: balanceContext.balanceLocks.mainLocks(), @@ -278,11 +283,11 @@ struct ModalInfoFactory { precision: precision ) - return balanceLockKnownModels + balanceLockUnknownModels + staticModels + return crowdloans + balanceLockKnownModels + balanceLockUnknownModels + staticModels } private static func createLockViewModel( - from locks: BalanceLocks, + from locks: AssetLocks, balanceContext: BalanceContext, amountFormatter: LocalizableResource, priceFormatter: LocalizableResource, @@ -294,9 +299,7 @@ struct ModalInfoFactory { let amountFormatter = amountFormatter.value(for: locale) let title: String = { - guard let mainTitle = LockType(rawValue: lock.displayId ?? "")? - .displayType - .value(for: locale) else { + guard let mainTitle = lock.lockType?.displayType.value(for: locale) else { return lock.displayId?.capitalized ?? "" } return mainTitle @@ -322,4 +325,32 @@ struct ModalInfoFactory { } } } + + private static func createCrowdloansViewModel( + balanceContext: BalanceContext, + amountFormatter: LocalizableResource, + priceFormatter: LocalizableResource + ) -> [LocalizableResource] { + guard balanceContext.crowdloans > 0 else { + return [] + } + + let viewModel = LocalizableResource { locale in + let formatter = priceFormatter.value(for: locale) + let amountFormatter = amountFormatter.value(for: locale) + + let title = R.string.localizable.walletAccountLocksCrowdloans(preferredLanguages: locale.rLanguages) + + let price = balanceContext.crowdloans * balanceContext.price + + let priceString = balanceContext.price == 0.0 ? nil : formatter.stringFromDecimal(price) + let amountString = amountFormatter.stringFromDecimal(balanceContext.crowdloans) ?? "" + + let balance = BalanceViewModel(amount: amountString, price: priceString) + + return StakingAmountViewModel(title: title, balance: balance) + } + + return [viewModel] + } } diff --git a/novawallet/Modules/Locks/LocksPresenter.swift b/novawallet/Modules/Locks/LocksPresenter.swift index 3851361ee0..d42cc2be70 100644 --- a/novawallet/Modules/Locks/LocksPresenter.swift +++ b/novawallet/Modules/Locks/LocksPresenter.swift @@ -88,8 +88,7 @@ final class LocksPresenter { createCell( amountInPlank: $0.amount, chainAssetId: $0.chainAssetId, - title: $0.lockType.map { $0.displayType.value(for: selectedLocale) } ?? - String(data: $0.type, encoding: .utf8)?.capitalized ?? "", + title: $0.lockType.map { $0.displayType.value(for: selectedLocale) } ?? $0.displayId?.capitalized ?? "", identifier: $0.identifier ) } diff --git a/novawallet/Modules/Wallet/Model/BalanceContext.swift b/novawallet/Modules/Wallet/Model/BalanceContext.swift index 820e1701d0..a7f4ff6494 100644 --- a/novawallet/Modules/Wallet/Model/BalanceContext.swift +++ b/novawallet/Modules/Wallet/Model/BalanceContext.swift @@ -17,12 +17,12 @@ struct BalanceContext { let price: Decimal let priceChange: Decimal let priceId: Int? - let balanceLocks: BalanceLocks + let balanceLocks: [AssetLock] } extension BalanceContext { - var total: Decimal { free + reserved } - var locked: Decimal { reserved + frozen } + var total: Decimal { free + reserved + crowdloans } + var locked: Decimal { reserved + frozen + crowdloans } var available: Decimal { free >= frozen ? free - frozen : 0.0 } } @@ -36,6 +36,7 @@ extension BalanceContext { priceChange = Self.parseContext(key: BalanceContext.priceChangeKey, context: context) priceId = context[BalanceContext.priceIdKey].flatMap { Int($0) } + crowdloans = Self.parseContext(key: BalanceContext.crowdloans, context: context) balanceLocks = Self.parseJSONContext(key: BalanceContext.balanceLocksKey, context: context) } @@ -52,6 +53,7 @@ extension BalanceContext { BalanceContext.freeKey: free.stringWithPointSeparator, BalanceContext.reservedKey: reserved.stringWithPointSeparator, BalanceContext.frozen: frozen.stringWithPointSeparator, + BalanceContext.crowdloans: crowdloans.stringWithPointSeparator, BalanceContext.priceKey: price.stringWithPointSeparator, BalanceContext.priceChangeKey: priceChange.stringWithPointSeparator, BalanceContext.balanceLocksKey: locksStringRepresentation @@ -72,7 +74,7 @@ extension BalanceContext { } } - private static func parseJSONContext(key: String, context: [String: String]) -> [BalanceLock] { + private static func parseJSONContext(key: String, context: [String: String]) -> [AssetLock] { guard let locksStringRepresentation = context[key] else { return [] } guard let JSONData = locksStringRepresentation.data(using: .utf8) else { @@ -80,7 +82,7 @@ extension BalanceContext { } let balanceLocks = try? JSONDecoder().decode( - BalanceLocks.self, + [AssetLock].self, from: JSONData ) @@ -103,6 +105,7 @@ extension BalanceContext { free: free, reserved: reserved, frozen: max(miscFrozen, feeFrozen), + crowdloans: crowdloans, price: price, priceChange: priceChange, priceId: priceId, @@ -122,6 +125,7 @@ extension BalanceContext { free: free, reserved: reserved, frozen: frozen, + crowdloans: crowdloans, price: price, priceChange: priceChange, priceId: priceId, @@ -130,12 +134,13 @@ extension BalanceContext { } func byChangingBalanceLocks( - _ updatedLocks: BalanceLocks + _ updatedLocks: [AssetLock] ) -> BalanceContext { BalanceContext( free: free, reserved: reserved, frozen: frozen, + crowdloans: crowdloans, price: price, priceChange: priceChange, priceId: priceId, @@ -148,10 +153,24 @@ extension BalanceContext { free: free, reserved: reserved, frozen: frozen, + crowdloans: crowdloans, price: newPrice, priceChange: newPriceChange, priceId: newPriceId, balanceLocks: balanceLocks ) } + + func byChangingCrowdloans(_ newCrowdloans: Decimal) -> BalanceContext { + BalanceContext( + free: free, + reserved: reserved, + frozen: frozen, + crowdloans: newCrowdloans, + price: price, + priceChange: priceChange, + priceId: priceId, + balanceLocks: balanceLocks + ) + } } diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index e9dfea879e..c4a098ffc8 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -973,4 +973,5 @@ "yield.boost.time.not.loaded.message" = "Please, wait until execution time is calculated"; "yield.boost.task.not.found.title" = "Yield boost already disabled"; "yield.boost.task.not.found.message" = "Yield boost already disabled for the selected collator"; -"common.not.available" = "N/A"; \ No newline at end of file +"common.not.available" = "N/A"; +"wallet.account.locks.crowdloans" = "Crowdloans"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 01d1108192..088f73e683 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -973,4 +973,5 @@ "yield.boost.time.not.loaded.message" = "Пожалуйста, подождите пока подсчитается время исполнения операции"; "yield.boost.task.not.found.title" = "Yield boost уже отключен"; "yield.boost.task.not.found.message" = "Yield boost уже отключен для выбранного коллатора"; -"common.not.available" = "N/A"; \ No newline at end of file +"common.not.available" = "N/A"; +"wallet.account.locks.crowdloans" = "Краудлоуны"; From 260c13984ccf59c79100252dd6553bad26b55576 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 23 Sep 2022 14:56:36 +0400 Subject: [PATCH 28/52] remove CDAssetLock from origin scheme --- .../SubstrateDataModel.xcdatamodel/contents | 9 --------- 1 file changed, 9 deletions(-) diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents index 52f09d918f..91c21cef57 100644 --- a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents @@ -22,14 +22,6 @@ - - - - - - - - @@ -139,7 +131,6 @@ - From fa4c117c4e758d563fb00780e5a224f3c347f47e Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 23 Sep 2022 17:27:41 +0400 Subject: [PATCH 29/52] added tests --- novawallet.xcodeproj/project.pbxproj | 4 + .../SubstrateStorageMigrationTests.swift | 199 ++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 novawalletTests/Common/Migration/SubstrateStorageMigrationTests.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index eb0273228b..0e7969624a 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -2149,6 +2149,7 @@ 8887813C28B62B0A00E7290F /* FlexibleSpaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8887813B28B62B0A00E7290F /* FlexibleSpaceView.swift */; }; 8887813E28B7AA3100E7290F /* RoundedIconTitleCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8887813D28B7AA3100E7290F /* RoundedIconTitleCollectionHeaderView.swift */; }; 8887814028B7AAB700E7290F /* RoundedIconTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8887813F28B7AAB700E7290F /* RoundedIconTitleView.swift */; }; + 8890E51628DDC98C001D3994 /* SubstrateStorageMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8890E51528DDC98C001D3994 /* SubstrateStorageMigrationTests.swift */; }; 88A0C52128D49A090083A524 /* CrowdloanOffChainSyncService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A0C52028D49A090083A524 /* CrowdloanOffChainSyncService.swift */; }; 88A0E0FF28A284C700A9C940 /* SelectedCurrencyDepending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A0E0FC28A284C700A9C940 /* SelectedCurrencyDepending.swift */; }; 88A0E10028A284C700A9C940 /* CurrencyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A0E0FD28A284C700A9C940 /* CurrencyManager.swift */; }; @@ -4912,6 +4913,7 @@ 8887813B28B62B0A00E7290F /* FlexibleSpaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleSpaceView.swift; sourceTree = ""; }; 8887813D28B7AA3100E7290F /* RoundedIconTitleCollectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedIconTitleCollectionHeaderView.swift; sourceTree = ""; }; 8887813F28B7AAB700E7290F /* RoundedIconTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedIconTitleView.swift; sourceTree = ""; }; + 8890E51528DDC98C001D3994 /* SubstrateStorageMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateStorageMigrationTests.swift; sourceTree = ""; }; 889A825F58F5CB54118A9D35 /* SelectValidatorsStartWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartWireframe.swift; sourceTree = ""; }; 88A0C52028D49A090083A524 /* CrowdloanOffChainSyncService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanOffChainSyncService.swift; sourceTree = ""; }; 88A0E0FC28A284C700A9C940 /* SelectedCurrencyDepending.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectedCurrencyDepending.swift; sourceTree = ""; }; @@ -8242,6 +8244,7 @@ isa = PBXGroup; children = ( 849ECD3426DE70B900F542A3 /* SingleToMultiassetUserMigrationTests.swift */, + 8890E51528DDC98C001D3994 /* SubstrateStorageMigrationTests.swift */, ); path = Migration; sourceTree = ""; @@ -16054,6 +16057,7 @@ 84B7C72E289BFA79001A3566 /* CustomValidatorListTestDataGenerator.swift in Sources */, 84B7C746289BFA79001A3566 /* WalletHistoryFilterTests.swift in Sources */, 843EC7A82701F63600C7DC7E /* PriceProviderFactoryStub.swift in Sources */, + 8890E51628DDC98C001D3994 /* SubstrateStorageMigrationTests.swift in Sources */, 84B7C70E289BFA79001A3566 /* SettingsTests.swift in Sources */, 84563D0924F46B7F0055591D /* ManagedAccountItemMapperTests.swift in Sources */, 845B822726EFFE0200D25C72 /* MetaAccountMapperTests.swift in Sources */, diff --git a/novawalletTests/Common/Migration/SubstrateStorageMigrationTests.swift b/novawalletTests/Common/Migration/SubstrateStorageMigrationTests.swift new file mode 100644 index 0000000000..088d704a6a --- /dev/null +++ b/novawalletTests/Common/Migration/SubstrateStorageMigrationTests.swift @@ -0,0 +1,199 @@ +import XCTest +import RobinHood +import CoreData + +@testable import novawallet + +final class SubstrateStorageMigrationTests: XCTestCase { + + let databaseDirectoryURL = FileManager + .default + .temporaryDirectory + .appendingPathComponent("CoreData") + .appendingPathComponent("SubstrateStorageMigrationTests") + + let databaseName = SubstrateStorageParams.databaseName + let modelDirectory = SubstrateStorageParams.modelDirectory + var storeURL: URL { databaseDirectoryURL.appendingPathComponent(databaseName) } + let mapper = ChainModelMapper() + + override func setUpWithError() throws { + try super.setUpWithError() + + try removeDirectory(at: databaseDirectoryURL) + try FileManager.default.createDirectory(at: databaseDirectoryURL, withIntermediateDirectories: true) + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + + try removeDirectory(at: databaseDirectoryURL) + } + + func testMigrationVersion1ToVersion2() { + let timeout: TimeInterval = 5 + let generatedChains = generateChainsWithTimeout(timeout) + XCTAssertGreaterThan(generatedChains.count, 0) + + let migrator = SubstrateStorageMigrator(storeURL: storeURL, + modelDirectory: modelDirectory, + model: .version2, + fileManager: FileManager.default) + + XCTAssertTrue(migrator.requiresMigration(), "Migration is not required") + + let migrateExpectation = XCTestExpectation(description: "Migration expectation") + migrator.migrate { + migrateExpectation.fulfill() + } + + wait(for: [migrateExpectation], timeout: timeout) + + let fetchedChains = fetchChainsWithTimeout(timeout) + + let sortedChainsBeforeMigration = generatedChains.sorted { $0.identifier < $1.identifier } + let sortedChainsAfterMigration = fetchedChains.sorted { $0.identifier < $1.identifier } + XCTAssertEqual(sortedChainsBeforeMigration, sortedChainsAfterMigration) + } + + private func generateChainsWithTimeout(_ timeout: TimeInterval) -> [ChainModel] { + var generatedChains: [ChainModel] = [] + let expectation = XCTestExpectation(description: "Generate chains expectation") + + generateChains { result in + switch result { + case .failure(let error): + XCTFail(error.localizedDescription) + case .success(let chains): + generatedChains = chains + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + return generatedChains + } + + private func fetchChainsWithTimeout(_ timeout: TimeInterval) -> [ChainModel] { + var fetchedChains: [ChainModel] = [] + let expectation = XCTestExpectation(description: "Fetch chains expectation") + + fetchChains { result in + switch result { + case .failure(let error): + XCTFail(error.localizedDescription) + case .success(let chains): + fetchedChains = chains + expectation.fulfill() + } + } + + wait(for: [expectation], timeout: timeout) + return fetchedChains + } + + private func generateChains(completion: @escaping (Result<[ChainModel], Error>) -> Void) { + let chains = ChainModelGenerator.generate(count: 5) + let dbService = createCoreDataService(for: .version1) + + dbService.performAsync { [unowned self] (context, error) in + if let error = error { + completion(.failure(error)) + return + } + + guard let context = context else { + completion(.failure(TestError.noCoreDataContext)) + return + } + + do { + try chains.forEach { + let newChain = NSEntityDescription.insertNewObject(forEntityName: "CDChain", + into: context) as! CDChain + try mapper.populate(entity: newChain, + from: $0, + using: context) + + } + + try context.save() + + completion(.success(chains)) + } catch { + completion(.failure(error)) + } + } + } + + private func fetchChains(completion: @escaping (Result<[ChainModel], Error>) -> Void) { + let dbService = createCoreDataService(for: .version2) + + dbService.performAsync { [unowned self] (context, error) in + if let error = error { + completion(.failure(error)) + return + } + + guard let context = context else { + completion(.failure(TestError.noCoreDataContext)) + return + } + do { + let request = NSFetchRequest(entityName: "CDChain") + guard let results = try context.fetch(request) as? [CDChain] else { + throw TestError.unexpectedEntity + } + let chains = try results.map(mapper.transform) + completion(.success(chains)) + } catch { + completion(.failure(error)) + } + } + + } + + private func createCoreDataService(for version: SubstrateStorageVersion) -> CoreDataServiceProtocol { + let modelURL = Bundle.main.url( + forResource: version.rawValue, + withExtension: "mom", + subdirectory: modelDirectory + )! + + let persistentSettings = CoreDataPersistentSettings( + databaseDirectory: databaseDirectoryURL, + databaseName: databaseName, + incompatibleModelStrategy: .ignore + ) + + let configuration = CoreDataServiceConfiguration( + modelURL: modelURL, + storageType: .persistent(settings: persistentSettings) + ) + + return CoreDataService(configuration: configuration) + } + + private func removeDirectory(at directoryURL: URL) throws { + let fileManager = FileManager.default + + guard let tmpFiles = try? fileManager.contentsOfDirectory(at: directoryURL, + includingPropertiesForKeys: nil, + options: .skipsHiddenFiles) else { + return + } + + try tmpFiles.forEach(fileManager.removeItem) + try fileManager.removeItem(at: directoryURL) + } + +} + +// MARK: - Errors + +extension SubstrateStorageMigrationTests { + enum TestError: String, Error { + case noCoreDataContext + case unexpectedEntity + } +} From 13b1c194975571e8bb9692451d1584f113f6ba35 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Fri, 23 Sep 2022 17:35:00 +0400 Subject: [PATCH 30/52] remove force unwrap --- .../Common/Migration/SubstrateStorageMigrationTests.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/novawalletTests/Common/Migration/SubstrateStorageMigrationTests.swift b/novawalletTests/Common/Migration/SubstrateStorageMigrationTests.swift index 088d704a6a..a8edf0bdcb 100644 --- a/novawalletTests/Common/Migration/SubstrateStorageMigrationTests.swift +++ b/novawalletTests/Common/Migration/SubstrateStorageMigrationTests.swift @@ -109,8 +109,12 @@ final class SubstrateStorageMigrationTests: XCTestCase { do { try chains.forEach { - let newChain = NSEntityDescription.insertNewObject(forEntityName: "CDChain", - into: context) as! CDChain + let insertedObject = NSEntityDescription.insertNewObject(forEntityName: "CDChain", + into: context) + guard let newChain = insertedObject as? CDChain else { + throw TestError.unexpectedEntity + } + try mapper.populate(entity: newChain, from: $0, using: context) From 8ced8b737ea3a4dcc9aca0bf674b8a6232d05b5a Mon Sep 17 00:00:00 2001 From: ERussel Date: Fri, 23 Sep 2022 19:40:10 +0500 Subject: [PATCH 31/52] fix wallet details --- .../AssetList/AssetListViewFactory.swift | 9 +- .../AssetList/AssetListWireframe.swift | 3 +- .../Modules/Wallet/WalletDetailsUpdater.swift | 96 +++++++++++++++++-- 3 files changed, 100 insertions(+), 8 deletions(-) diff --git a/novawallet/Modules/AssetList/AssetListViewFactory.swift b/novawallet/Modules/AssetList/AssetListViewFactory.swift index 2dbe4f4e2c..b3a82fac9d 100644 --- a/novawallet/Modules/AssetList/AssetListViewFactory.swift +++ b/novawallet/Modules/AssetList/AssetListViewFactory.swift @@ -21,7 +21,14 @@ struct AssetListViewFactory { logger: Logger.shared ) - let wireframe = AssetListWireframe(walletUpdater: WalletDetailsUpdater.shared) + let walletUpdater = WalletDetailsUpdater( + eventCenter: EventCenter.shared, + crowdloansLocalSubscriptionFactory: interactor.crowdloansLocalSubscriptionFactory, + walletLocalSubscriptionFactory: interactor.walletLocalSubscriptionFactory, + walletSettings: interactor.selectedWalletSettings + ) + + let wireframe = AssetListWireframe(walletUpdater: walletUpdater) let nftDownloadService = NftFileDownloadService( cacheBasePath: ApplicationConfig.shared.fileCachePath, diff --git a/novawallet/Modules/AssetList/AssetListWireframe.swift b/novawallet/Modules/AssetList/AssetListWireframe.swift index ba1899b3d8..d8fb169529 100644 --- a/novawallet/Modules/AssetList/AssetListWireframe.swift +++ b/novawallet/Modules/AssetList/AssetListWireframe.swift @@ -22,7 +22,8 @@ final class AssetListWireframe: AssetListWireframeProtocol { try? context.createAssetDetails(for: assetId, in: navigationController) - walletUpdater.context = context + let chainAsset = ChainAsset(chain: chain, asset: asset) + walletUpdater.setup(context: context, chainAsset: chainAsset) } func showAssetsManage(from view: AssetListViewProtocol?) { diff --git a/novawallet/Modules/Wallet/WalletDetailsUpdater.swift b/novawallet/Modules/Wallet/WalletDetailsUpdater.swift index a44985c7a6..e14a802bc5 100644 --- a/novawallet/Modules/Wallet/WalletDetailsUpdater.swift +++ b/novawallet/Modules/Wallet/WalletDetailsUpdater.swift @@ -1,27 +1,111 @@ import Foundation import CommonWallet +import RobinHood protocol WalletDetailsUpdating: AnyObject { - var context: CommonWalletContextProtocol? { get set } + var context: CommonWalletContextProtocol? { get } + + func setup(context: CommonWalletContextProtocol, chainAsset: ChainAsset) } final class WalletDetailsUpdater: WalletDetailsUpdating, EventVisitorProtocol { - static let shared = WalletDetailsUpdater(eventCenter: EventCenter.shared) + weak var context: CommonWalletContextProtocol? { + didSet { + clearProvidersIfNeeded() + } + } - weak var context: CommonWalletContextProtocol? let eventCenter: EventCenterProtocol + let crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol + let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol + let walletSettings: SelectedWalletSettings - init(eventCenter: EventCenterProtocol) { + private var crowdloanContributionsDataProvider: StreamableProvider? + private var assetsLockDataProvider: StreamableProvider? + private var balanceDataProvider: StreamableProvider? + + init( + eventCenter: EventCenterProtocol, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + walletSettings: SelectedWalletSettings + ) { self.eventCenter = eventCenter + self.crowdloansLocalSubscriptionFactory = crowdloansLocalSubscriptionFactory + self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory + self.walletSettings = walletSettings eventCenter.add(observer: self, dispatchIn: .main) } - func processBalanceChanged(event _: WalletBalanceChanged) { - try? context?.prepareAccountUpdateCommand().execute() + func setup(context: CommonWalletContextProtocol, chainAsset: ChainAsset) { + clearProviders() + + self.context = context + + if let wallet = walletSettings.value { + subscribe(for: wallet, chainAsset: chainAsset) + } } func processNewTransaction(event _: WalletNewTransactionInserted) { try? context?.prepareAccountUpdateCommand().execute() } + + private func subscribe(for wallet: MetaAccountModel, chainAsset: ChainAsset) { + guard let accountId = wallet.fetch(for: chainAsset.chain.accountRequest())?.accountId else { + return + } + + balanceDataProvider = subscribeToAssetBalanceProvider( + for: accountId, + chainId: chainAsset.chain.chainId, + assetId: chainAsset.asset.assetId + ) + + assetsLockDataProvider = subscribeToAllLocksProvider(for: accountId) + + crowdloanContributionsDataProvider = subscribeToCrowdloansProvider(for: accountId, chain: chainAsset.chain) + } + + private func updateAccount() { + try? context?.prepareAccountUpdateCommand().execute() + } + + private func clearProvidersIfNeeded() { + if context == nil { + clearProviders() + } + } + + private func clearProviders() { + balanceDataProvider = nil + crowdloanContributionsDataProvider = nil + assetsLockDataProvider = nil + } +} + +extension WalletDetailsUpdater: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { + func handleAssetBalance( + result _: Result, + accountId _: AccountId, + chainId _: ChainModel.Id, + assetId _: AssetModel.Id + ) { + updateAccount() + } + + func handleAccountLocks(result _: Result<[DataProviderChange], Error>, accountId _: AccountId) { + updateAccount() + } +} + +extension WalletDetailsUpdater: CrowdloanContributionLocalSubscriptionHandler, CrowdloansLocalStorageSubscriber { + func handleCrowdloans( + result _: Result<[DataProviderChange], Error>, + accountId _: AccountId, + chain _: ChainModel + ) { + updateAccount() + } } From f5cad5705c1c7642c660045e35a3e196a1d02ccf Mon Sep 17 00:00:00 2001 From: ERussel Date: Sat, 24 Sep 2022 17:03:22 +0500 Subject: [PATCH 32/52] fix chain registry init --- .../Services/ChainRegistry/ChainRegistryFacade.swift | 6 +++++- novawallet/Modules/Root/RootInteractor.swift | 8 ++++---- novawallet/Modules/Root/RootPresenterFactory.swift | 2 +- novawalletTests/Mocks/ChainRegistryStub.swift | 4 ++++ novawalletTests/Modules/Root/RootTests.swift | 3 +-- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/novawallet/Common/Services/ChainRegistry/ChainRegistryFacade.swift b/novawallet/Common/Services/ChainRegistry/ChainRegistryFacade.swift index 29e9ab4181..fe1ebf7f88 100644 --- a/novawallet/Common/Services/ChainRegistry/ChainRegistryFacade.swift +++ b/novawallet/Common/Services/ChainRegistry/ChainRegistryFacade.swift @@ -1,5 +1,9 @@ import Foundation -final class ChainRegistryFacade { +protocol ChainRegistryFacadeProtocol { + static var sharedRegistry: ChainRegistryProtocol { get } +} + +final class ChainRegistryFacade: ChainRegistryFacadeProtocol { static let sharedRegistry: ChainRegistryProtocol = ChainRegistryFactory.createDefaultRegistry() } diff --git a/novawallet/Modules/Root/RootInteractor.swift b/novawallet/Modules/Root/RootInteractor.swift index 6dbde34ec8..e801ddd50b 100644 --- a/novawallet/Modules/Root/RootInteractor.swift +++ b/novawallet/Modules/Root/RootInteractor.swift @@ -9,7 +9,7 @@ final class RootInteractor { let settings: SelectedWalletSettings let keystore: KeystoreProtocol let applicationConfig: ApplicationConfigProtocol - let chainRegistry: ChainRegistryProtocol + let chainRegistryFacade: ChainRegistryFacadeProtocol.Type let eventCenter: EventCenterProtocol let migrators: [Migrating] let logger: LoggerProtocol? @@ -18,7 +18,7 @@ final class RootInteractor { settings: SelectedWalletSettings, keystore: KeystoreProtocol, applicationConfig: ApplicationConfigProtocol, - chainRegistry: ChainRegistryProtocol, + chainRegistryFacade: ChainRegistryFacadeProtocol.Type, eventCenter: EventCenterProtocol, migrators: [Migrating], logger: LoggerProtocol? = nil @@ -26,7 +26,7 @@ final class RootInteractor { self.settings = settings self.keystore = keystore self.applicationConfig = applicationConfig - self.chainRegistry = chainRegistry + self.chainRegistryFacade = chainRegistryFacade self.eventCenter = eventCenter self.migrators = migrators self.logger = logger @@ -103,6 +103,6 @@ extension RootInteractor: RootInteractorInputProtocol { } } - chainRegistry.syncUp() + chainRegistryFacade.sharedRegistry.syncUp() } } diff --git a/novawallet/Modules/Root/RootPresenterFactory.swift b/novawallet/Modules/Root/RootPresenterFactory.swift index 96417d8892..c35821fb95 100644 --- a/novawallet/Modules/Root/RootPresenterFactory.swift +++ b/novawallet/Modules/Root/RootPresenterFactory.swift @@ -29,7 +29,7 @@ final class RootPresenterFactory: RootPresenterFactoryProtocol { settings: SelectedWalletSettings.shared, keystore: keychain, applicationConfig: ApplicationConfig.shared, - chainRegistry: ChainRegistryFacade.sharedRegistry, + chainRegistryFacade: ChainRegistryFacade.self, eventCenter: EventCenter.shared, migrators: [userStorageMigrator, substrateStorageMigrator], logger: Logger.shared diff --git a/novawalletTests/Mocks/ChainRegistryStub.swift b/novawalletTests/Mocks/ChainRegistryStub.swift index 21ebe645d5..8652087435 100644 --- a/novawalletTests/Mocks/ChainRegistryStub.swift +++ b/novawalletTests/Mocks/ChainRegistryStub.swift @@ -3,6 +3,10 @@ import Foundation import RobinHood import Cuckoo +enum ChainRegistryFacadeStub: ChainRegistryFacadeProtocol { + static var sharedRegistry: ChainRegistryProtocol = MockChainRegistryProtocol().applyDefault(for: Set()) +} + extension MockChainRegistryProtocol { func applyDefault(for chains: Set) -> MockChainRegistryProtocol { stub(self) { stub in diff --git a/novawalletTests/Modules/Root/RootTests.swift b/novawalletTests/Modules/Root/RootTests.swift index 71b9ffc548..99970624c8 100644 --- a/novawalletTests/Modules/Root/RootTests.swift +++ b/novawalletTests/Modules/Root/RootTests.swift @@ -124,11 +124,10 @@ class RootTests: XCTestCase { keystore: KeystoreProtocol, migrators: [Migrating] = [] ) -> RootPresenter { - let chainRegistry = MockChainRegistryProtocol().applyDefault(for: Set()) let interactor = RootInteractor(settings: settings, keystore: keystore, applicationConfig: ApplicationConfig.shared, - chainRegistry: chainRegistry, + chainRegistryFacade: ChainRegistryFacadeStub.self, eventCenter: MockEventCenterProtocol(), migrators: migrators) let presenter = RootPresenter() From cb2f4c3c8d6c26f3f12981fb131af0b1c411d755 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sat, 24 Sep 2022 17:44:05 +0500 Subject: [PATCH 33/52] make chain registry init lazy --- .../Services/ChainRegistry/ChainRegistryFacade.swift | 6 ++---- novawallet/Modules/Root/RootInteractor.swift | 8 ++++---- novawallet/Modules/Root/RootPresenterFactory.swift | 2 +- novawalletIntegrationTests/ChainRegistry+Setup.swift | 6 +++++- novawalletTests/Mocks/ChainRegistryStub.swift | 4 ---- novawalletTests/Modules/Root/RootTests.swift | 3 ++- 6 files changed, 14 insertions(+), 15 deletions(-) diff --git a/novawallet/Common/Services/ChainRegistry/ChainRegistryFacade.swift b/novawallet/Common/Services/ChainRegistry/ChainRegistryFacade.swift index fe1ebf7f88..d633ee479c 100644 --- a/novawallet/Common/Services/ChainRegistry/ChainRegistryFacade.swift +++ b/novawallet/Common/Services/ChainRegistry/ChainRegistryFacade.swift @@ -1,9 +1,7 @@ import Foundation -protocol ChainRegistryFacadeProtocol { - static var sharedRegistry: ChainRegistryProtocol { get } -} +typealias ChainRegistryLazyClosure = () -> ChainRegistryProtocol -final class ChainRegistryFacade: ChainRegistryFacadeProtocol { +enum ChainRegistryFacade { static let sharedRegistry: ChainRegistryProtocol = ChainRegistryFactory.createDefaultRegistry() } diff --git a/novawallet/Modules/Root/RootInteractor.swift b/novawallet/Modules/Root/RootInteractor.swift index e801ddd50b..46b9cfe093 100644 --- a/novawallet/Modules/Root/RootInteractor.swift +++ b/novawallet/Modules/Root/RootInteractor.swift @@ -9,7 +9,7 @@ final class RootInteractor { let settings: SelectedWalletSettings let keystore: KeystoreProtocol let applicationConfig: ApplicationConfigProtocol - let chainRegistryFacade: ChainRegistryFacadeProtocol.Type + let chainRegistryClosure: ChainRegistryLazyClosure let eventCenter: EventCenterProtocol let migrators: [Migrating] let logger: LoggerProtocol? @@ -18,7 +18,7 @@ final class RootInteractor { settings: SelectedWalletSettings, keystore: KeystoreProtocol, applicationConfig: ApplicationConfigProtocol, - chainRegistryFacade: ChainRegistryFacadeProtocol.Type, + chainRegistryClosure: @escaping ChainRegistryLazyClosure, eventCenter: EventCenterProtocol, migrators: [Migrating], logger: LoggerProtocol? = nil @@ -26,7 +26,7 @@ final class RootInteractor { self.settings = settings self.keystore = keystore self.applicationConfig = applicationConfig - self.chainRegistryFacade = chainRegistryFacade + self.chainRegistryClosure = chainRegistryClosure self.eventCenter = eventCenter self.migrators = migrators self.logger = logger @@ -103,6 +103,6 @@ extension RootInteractor: RootInteractorInputProtocol { } } - chainRegistryFacade.sharedRegistry.syncUp() + chainRegistryClosure().syncUp() } } diff --git a/novawallet/Modules/Root/RootPresenterFactory.swift b/novawallet/Modules/Root/RootPresenterFactory.swift index c35821fb95..429e075fe4 100644 --- a/novawallet/Modules/Root/RootPresenterFactory.swift +++ b/novawallet/Modules/Root/RootPresenterFactory.swift @@ -29,7 +29,7 @@ final class RootPresenterFactory: RootPresenterFactoryProtocol { settings: SelectedWalletSettings.shared, keystore: keychain, applicationConfig: ApplicationConfig.shared, - chainRegistryFacade: ChainRegistryFacade.self, + chainRegistryClosure: { ChainRegistryFacade.sharedRegistry }, eventCenter: EventCenter.shared, migrators: [userStorageMigrator, substrateStorageMigrator], logger: Logger.shared diff --git a/novawalletIntegrationTests/ChainRegistry+Setup.swift b/novawalletIntegrationTests/ChainRegistry+Setup.swift index 12e56e6782..ee77dc655a 100644 --- a/novawalletIntegrationTests/ChainRegistry+Setup.swift +++ b/novawalletIntegrationTests/ChainRegistry+Setup.swift @@ -8,9 +8,11 @@ extension ChainRegistryFacade { let chainRegistry = ChainRegistryFactory.createDefaultRegistry(from: storageFacade) chainRegistry.syncUp() + let target = NSObject() + let semaphore = DispatchSemaphore(value: 0) chainRegistry.chainsSubscribe( - self, runningInQueue: .global() + target, runningInQueue: .global() ) { changes in if !changes.isEmpty { semaphore.signal() @@ -19,6 +21,8 @@ extension ChainRegistryFacade { semaphore.wait() + chainRegistry.chainsUnsubscribe(target) + return chainRegistry } } diff --git a/novawalletTests/Mocks/ChainRegistryStub.swift b/novawalletTests/Mocks/ChainRegistryStub.swift index 8652087435..21ebe645d5 100644 --- a/novawalletTests/Mocks/ChainRegistryStub.swift +++ b/novawalletTests/Mocks/ChainRegistryStub.swift @@ -3,10 +3,6 @@ import Foundation import RobinHood import Cuckoo -enum ChainRegistryFacadeStub: ChainRegistryFacadeProtocol { - static var sharedRegistry: ChainRegistryProtocol = MockChainRegistryProtocol().applyDefault(for: Set()) -} - extension MockChainRegistryProtocol { func applyDefault(for chains: Set) -> MockChainRegistryProtocol { stub(self) { stub in diff --git a/novawalletTests/Modules/Root/RootTests.swift b/novawalletTests/Modules/Root/RootTests.swift index 99970624c8..282849c1fd 100644 --- a/novawalletTests/Modules/Root/RootTests.swift +++ b/novawalletTests/Modules/Root/RootTests.swift @@ -124,10 +124,11 @@ class RootTests: XCTestCase { keystore: KeystoreProtocol, migrators: [Migrating] = [] ) -> RootPresenter { + let chainRegistry = MockChainRegistryProtocol().applyDefault(for: Set()) let interactor = RootInteractor(settings: settings, keystore: keystore, applicationConfig: ApplicationConfig.shared, - chainRegistryFacade: ChainRegistryFacadeStub.self, + chainRegistryClosure: { chainRegistry }, eventCenter: MockEventCenterProtocol(), migrators: migrators) let presenter = RootPresenter() From 217c27ca6ff4d6e894e15bbb7cc4be7b629252c2 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sat, 24 Sep 2022 18:12:00 +0500 Subject: [PATCH 34/52] make substrate migration sync --- .../Migration/StorageMigrator+Sync.swift | 14 +++++++++++- .../Migration/SubstrateStorageMigrator.swift | 22 ++++++------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/novawallet/Common/Migration/StorageMigrator+Sync.swift b/novawallet/Common/Migration/StorageMigrator+Sync.swift index 42a0218772..bb63b7a46c 100644 --- a/novawallet/Common/Migration/StorageMigrator+Sync.swift +++ b/novawallet/Common/Migration/StorageMigrator+Sync.swift @@ -8,6 +8,18 @@ extension UserStorageMigrator: Migrating { performMigration() - Logger.shared.info("Db migration completed") + Logger.shared.info("User storage migration was completed") + } +} + +extension SubstrateStorageMigrator: Migrating { + func migrate() throws { + guard requiresMigration() else { + return + } + + performMigration() + + Logger.shared.info("Substrate storage migration was completed") } } diff --git a/novawallet/Common/Migration/SubstrateStorageMigrator.swift b/novawallet/Common/Migration/SubstrateStorageMigrator.swift index 663fa52f2b..c8414a12fc 100644 --- a/novawallet/Common/Migration/SubstrateStorageMigrator.swift +++ b/novawallet/Common/Migration/SubstrateStorageMigrator.swift @@ -20,19 +20,6 @@ final class SubstrateStorageMigrator { } } -// MARK: - Migrating - -extension SubstrateStorageMigrator: Migrating { - func migrate() throws { - guard requiresMigration() else { - return - } - migrate { - Logger.shared.info("Substrate storage migration was completed") - } - } -} - // MARK: - StorageMigrating extension SubstrateStorageMigrator: StorageMigrating { @@ -45,7 +32,7 @@ extension SubstrateStorageMigrator: StorageMigrating { ) } - private func performMigration() { + func performMigration() { let destinationVersion = SubstrateStorageVersion.current let mom = createManagedObjectModel( @@ -59,7 +46,12 @@ extension SubstrateStorageMigrator: StorageMigrating { NSInferMappingModelAutomaticallyOption: true ] do { - try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options) + try psc.addPersistentStore( + ofType: NSSQLiteStoreType, + configurationName: nil, + at: storeURL, + options: options + ) } catch { fatalError("Failed to add persistent store: \(error)") } From c9ea4b610b8f27bbcaff9cca9f6c24b34240d61e Mon Sep 17 00:00:00 2001 From: ERussel Date: Sun, 25 Sep 2022 03:12:06 +0500 Subject: [PATCH 35/52] fix memory leak --- .../AccountDetails/View/AssetDetailsContainingViewFactory.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/novawallet/Modules/Wallet/AccountDetails/View/AssetDetailsContainingViewFactory.swift b/novawallet/Modules/Wallet/AccountDetails/View/AssetDetailsContainingViewFactory.swift index cb7182811a..5517393da3 100644 --- a/novawallet/Modules/Wallet/AccountDetails/View/AssetDetailsContainingViewFactory.swift +++ b/novawallet/Modules/Wallet/AccountDetails/View/AssetDetailsContainingViewFactory.swift @@ -9,7 +9,7 @@ class AssetDetailsContainingViewFactory: AccountDetailsContainingViewFactoryProt let selectedAccountId: AccountId let selectedAccountType: MetaAccountModelType - var commandFactory: WalletCommandFactoryProtocol? + weak var commandFactory: WalletCommandFactoryProtocol? init( chainAsset: ChainAsset, From a630fe2b2a0b4d6bdaebd436b3631a65acbd09b9 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sun, 25 Sep 2022 13:50:17 +0500 Subject: [PATCH 36/52] fix subscriptions --- .../WalletLocalStorageSubscriber.swift | 58 ++++++++++++++++++ .../WalletLocalSubscriptionHandler.swift | 14 +++++ .../WalletLocalSubscriptionFactory.swift | 59 ++++++++++++++++--- .../Modules/Wallet/WalletDetailsUpdater.swift | 31 +++++++--- 4 files changed, 147 insertions(+), 15 deletions(-) diff --git a/novawallet/Common/DataProvider/Subscription/WalletLocalStorageSubscriber.swift b/novawallet/Common/DataProvider/Subscription/WalletLocalStorageSubscriber.swift index 3fda9d5721..abc1ac66d5 100644 --- a/novawallet/Common/DataProvider/Subscription/WalletLocalStorageSubscriber.swift +++ b/novawallet/Common/DataProvider/Subscription/WalletLocalStorageSubscriber.swift @@ -26,6 +26,12 @@ protocol WalletLocalStorageSubscriber where Self: AnyObject { func subscribeToAllLocksProvider( for accountId: AccountId ) -> StreamableProvider? + + func subscribeToLocksProvider( + for accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) -> StreamableProvider? } extension WalletLocalStorageSubscriber { @@ -243,6 +249,58 @@ extension WalletLocalStorageSubscriber { return locksProvider } + + func subscribeToLocksProvider( + for accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) -> StreamableProvider? { + guard + let locksProvider = try? walletLocalSubscriptionFactory.getLocksProvider( + for: accountId, + chainId: chainId, + assetId: assetId + ) else { + return nil + } + + let updateClosure = { [weak self] (changes: [DataProviderChange]) in + self?.walletLocalSubscriptionHandler.handleAccountLocks( + result: .success(changes), + accountId: accountId, + chainId: chainId, + assetId: assetId + ) + return + } + + let failureClosure = { [weak self] (error: Error) in + self?.walletLocalSubscriptionHandler.handleAccountLocks( + result: .failure(error), + accountId: accountId, + chainId: chainId, + assetId: assetId + ) + return + } + + let options = StreamableProviderObserverOptions( + alwaysNotifyOnRefresh: false, + waitsInProgressSyncOnAdd: false, + initialSize: 0, + refreshWhenEmpty: false + ) + + locksProvider.addObserver( + self, + deliverOn: .main, + executing: updateClosure, + failing: failureClosure, + options: options + ) + + return locksProvider + } } extension WalletLocalStorageSubscriber where Self: WalletLocalSubscriptionHandler { diff --git a/novawallet/Common/DataProvider/Subscription/WalletLocalSubscriptionHandler.swift b/novawallet/Common/DataProvider/Subscription/WalletLocalSubscriptionHandler.swift index 64de1966bf..2bff23fe88 100644 --- a/novawallet/Common/DataProvider/Subscription/WalletLocalSubscriptionHandler.swift +++ b/novawallet/Common/DataProvider/Subscription/WalletLocalSubscriptionHandler.swift @@ -26,6 +26,13 @@ protocol WalletLocalSubscriptionHandler { result: Result<[DataProviderChange], Error>, accountId: AccountId ) + + func handleAccountLocks( + result: Result<[DataProviderChange], Error>, + accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) } extension WalletLocalSubscriptionHandler { @@ -53,4 +60,11 @@ extension WalletLocalSubscriptionHandler { result _: Result<[DataProviderChange], Error>, accountId _: AccountId ) {} + + func handleAccountLocks( + result _: Result<[DataProviderChange], Error>, + accountId _: AccountId, + chainId _: ChainModel.Id, + assetId _: AssetModel.Id + ) {} } diff --git a/novawallet/Common/DataProvider/WalletLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/WalletLocalSubscriptionFactory.swift index 4b1c5f8294..a5283834a9 100644 --- a/novawallet/Common/DataProvider/WalletLocalSubscriptionFactory.swift +++ b/novawallet/Common/DataProvider/WalletLocalSubscriptionFactory.swift @@ -18,6 +18,12 @@ protocol WalletLocalSubscriptionFactoryProtocol { func getAllBalancesProvider() throws -> StreamableProvider func getLocksProvider(for accountId: AccountId) throws -> StreamableProvider + + func getLocksProvider( + for accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) throws -> StreamableProvider } final class WalletLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, @@ -182,13 +188,54 @@ final class WalletLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, return provider } + let filter = NSPredicate.assetLock(for: accountId) + + let provider = createAssetLocksProvider(for: filter) { entity in + accountId.toHex() == entity.chainAccountId + } + + saveProvider(provider, for: cacheKey) + + return provider + } + + func getLocksProvider( + for accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) throws -> StreamableProvider { + let cacheKey = "locks-\(accountId.toHex())-\(chainId)-\(assetId)" + + if let provider = getProvider(for: cacheKey) as? StreamableProvider { + return provider + } + + let filter = NSPredicate.assetLock( + for: accountId, + chainAssetId: ChainAssetId(chainId: chainId, assetId: assetId) + ) + + let provider = createAssetLocksProvider(for: filter) { entity in + accountId.toHex() == entity.chainAccountId && + chainId == entity.chainId && + assetId == entity.assetId + } + + saveProvider(provider, for: cacheKey) + + return provider + } + + private func createAssetLocksProvider( + for repositoryFilter: NSPredicate, + observingFilter: @escaping (CDAssetLock) -> Bool + ) -> StreamableProvider { let source = EmptyStreamableSource() let mapper = AssetLockMapper() - let filter = NSPredicate.assetLock(for: accountId) let repository = storageFacade.createRepository( - filter: filter, + filter: repositoryFilter, sortDescriptors: [], mapper: AnyCoreDataMapper(mapper) ) @@ -197,7 +244,7 @@ final class WalletLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, service: storageFacade.databaseService, mapper: AnyCoreDataMapper(mapper), predicate: { entity in - accountId.toHex() == entity.chainAccountId + observingFilter(entity) } ) @@ -207,15 +254,11 @@ final class WalletLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, } } - let provider = StreamableProvider( + return StreamableProvider( source: AnyStreamableSource(source), repository: AnyDataProviderRepository(repository), observable: AnyDataProviderRepositoryObservable(observable), operationManager: operationManager ) - - saveProvider(provider, for: cacheKey) - - return provider } } diff --git a/novawallet/Modules/Wallet/WalletDetailsUpdater.swift b/novawallet/Modules/Wallet/WalletDetailsUpdater.swift index e14a802bc5..bc58197d14 100644 --- a/novawallet/Modules/Wallet/WalletDetailsUpdater.swift +++ b/novawallet/Modules/Wallet/WalletDetailsUpdater.swift @@ -8,12 +8,17 @@ protocol WalletDetailsUpdating: AnyObject { func setup(context: CommonWalletContextProtocol, chainAsset: ChainAsset) } +/** + * Class is responsible for monitoring balance or transaction changes + * and ask CommonWallet to update itself. + * + * Note: Currently there is no way to know whether CommonWallet was closed. + * So, before processing the event we should manually check whether context + * exists and clear observers otherwise. + */ + final class WalletDetailsUpdater: WalletDetailsUpdating, EventVisitorProtocol { - weak var context: CommonWalletContextProtocol? { - didSet { - clearProvidersIfNeeded() - } - } + weak var context: CommonWalletContextProtocol? let eventCenter: EventCenterProtocol let crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol @@ -63,7 +68,11 @@ final class WalletDetailsUpdater: WalletDetailsUpdating, EventVisitorProtocol { assetId: chainAsset.asset.assetId ) - assetsLockDataProvider = subscribeToAllLocksProvider(for: accountId) + assetsLockDataProvider = subscribeToLocksProvider( + for: accountId, + chainId: chainAsset.chain.chainId, + assetId: chainAsset.asset.assetId + ) crowdloanContributionsDataProvider = subscribeToCrowdloansProvider(for: accountId, chain: chainAsset.chain) } @@ -92,10 +101,17 @@ extension WalletDetailsUpdater: WalletLocalStorageSubscriber, WalletLocalSubscri chainId _: ChainModel.Id, assetId _: AssetModel.Id ) { + clearProvidersIfNeeded() updateAccount() } - func handleAccountLocks(result _: Result<[DataProviderChange], Error>, accountId _: AccountId) { + func handleAccountLocks( + result _: Result<[DataProviderChange], Error>, + accountId _: AccountId, + chainId _: ChainModel.Id, + assetId _: AssetModel.Id + ) { + clearProvidersIfNeeded() updateAccount() } } @@ -106,6 +122,7 @@ extension WalletDetailsUpdater: CrowdloanContributionLocalSubscriptionHandler, C accountId _: AccountId, chain _: ChainModel ) { + clearProvidersIfNeeded() updateAccount() } } From 18df0c1ead606adeeab52e99c30d48fed1e0108c Mon Sep 17 00:00:00 2001 From: ERussel Date: Sun, 25 Sep 2022 13:57:29 +0500 Subject: [PATCH 37/52] remove not used event center events --- novawallet.xcodeproj/project.pbxproj | 8 -------- novawallet/Common/EventCenter/EventVisitor.swift | 4 ---- .../Common/EventCenter/Events/WalletBalanceChanged.swift | 7 ------- .../Common/EventCenter/Events/WalletStakingChanged.swift | 7 ------- .../StorageSubscription/AccountInfoSubscription.swift | 2 -- .../StorageSubscription/AssetsBalanceUpdater.swift | 2 -- .../StorageSubscription/OrmlAccountSubscription.swift | 2 -- 7 files changed, 32 deletions(-) delete mode 100644 novawallet/Common/EventCenter/Events/WalletBalanceChanged.swift delete mode 100644 novawallet/Common/EventCenter/Events/WalletStakingChanged.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 825e9e2672..a5b4b75a27 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -692,10 +692,8 @@ 8438E1D224BFAAD2001BDB13 /* JSONRPCTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8438E1D124BFAAD2001BDB13 /* JSONRPCTests.swift */; }; 843910B0253ED36C00E3C217 /* ChainStorageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910AF253ED36C00E3C217 /* ChainStorageItem.swift */; }; 843910B2253ED4D100E3C217 /* CDChainStorageItem+CoreDataDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910B1253ED4D100E3C217 /* CDChainStorageItem+CoreDataDecodable.swift */; }; - 843910B4253EE52100E3C217 /* WalletBalanceChanged.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910B3253EE52100E3C217 /* WalletBalanceChanged.swift */; }; 843910B6253EE62B00E3C217 /* DataProviderChange+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910B5253EE62B00E3C217 /* DataProviderChange+Result.swift */; }; 843910B9253EFB8100E3C217 /* StorageKeyFactory+Implicit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910B8253EFB8100E3C217 /* StorageKeyFactory+Implicit.swift */; }; - 843910BB253F021E00E3C217 /* WalletStakingChanged.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910BA253F021E00E3C217 /* WalletStakingChanged.swift */; }; 843910C1253F36F300E3C217 /* BaseStorageChildSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910C0253F36F300E3C217 /* BaseStorageChildSubscription.swift */; }; 843910C3253F39B100E3C217 /* WalletNetworkFacade+Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910C2253F39B100E3C217 /* WalletNetworkFacade+Storage.swift */; }; 843910C5253F561500E3C217 /* CompoundOperationWrapper+Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843910C4253F561500E3C217 /* CompoundOperationWrapper+Result.swift */; }; @@ -3438,10 +3436,8 @@ 8438E1D324BFAAD2001BDB13 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 843910AF253ED36C00E3C217 /* ChainStorageItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainStorageItem.swift; sourceTree = ""; }; 843910B1253ED4D100E3C217 /* CDChainStorageItem+CoreDataDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CDChainStorageItem+CoreDataDecodable.swift"; sourceTree = ""; }; - 843910B3253EE52100E3C217 /* WalletBalanceChanged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletBalanceChanged.swift; sourceTree = ""; }; 843910B5253EE62B00E3C217 /* DataProviderChange+Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataProviderChange+Result.swift"; sourceTree = ""; }; 843910B8253EFB8100E3C217 /* StorageKeyFactory+Implicit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageKeyFactory+Implicit.swift"; sourceTree = ""; }; - 843910BA253F021E00E3C217 /* WalletStakingChanged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletStakingChanged.swift; sourceTree = ""; }; 843910C0253F36F300E3C217 /* BaseStorageChildSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStorageChildSubscription.swift; sourceTree = ""; }; 843910C2253F39B100E3C217 /* WalletNetworkFacade+Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletNetworkFacade+Storage.swift"; sourceTree = ""; }; 843910C4253F561500E3C217 /* CompoundOperationWrapper+Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CompoundOperationWrapper+Result.swift"; sourceTree = ""; }; @@ -11133,8 +11129,6 @@ children = ( 84EBC55A24F660F500459D15 /* SelectedAccountChanged.swift */, 840882AF2514024800177E20 /* SelectedConnectionChanged.swift */, - 843910B3253EE52100E3C217 /* WalletBalanceChanged.swift */, - 843910BA253F021E00E3C217 /* WalletStakingChanged.swift */, 84FD3DBA254104B600A234E3 /* WalletNewTransactionInserted.swift */, 84BEE22225646ABF00D05EB3 /* SelectedUsernameChanged.swift */, 8488ECDE258CE118004591CC /* PurchaseCompleted.swift */, @@ -13773,7 +13767,6 @@ 842898D1265A955A002D5D65 /* ImageViewModel.swift in Sources */, 8490145324A93FD1008F705E /* FearlessLoadingViewFactory.swift in Sources */, 8472976C260B1CAD009B86D0 /* InitiatedBondingConfirmInteractor.swift in Sources */, - 843910BB253F021E00E3C217 /* WalletStakingChanged.swift in Sources */, 84EBC55F24F71D6A00459D15 /* UITableViewCell+ReorderColor.swift in Sources */, F43F934B26D76E8E00A6B529 /* AnalyticsRewardsBaseView.swift in Sources */, 84D8F17124D856D300AF43E9 /* SNAddressType+ViewModel.swift in Sources */, @@ -14559,7 +14552,6 @@ 84CB2250270360AC0041C8C1 /* StakingLocalSubscriptionFactory.swift in Sources */, 8448221826B1624E007F4492 /* SelectValidatorsConfirmViewLayout.swift in Sources */, 8490145624A9404E008F705E /* AttributedStringDecorator.swift in Sources */, - 843910B4253EE52100E3C217 /* WalletBalanceChanged.swift in Sources */, D9046DBC27453D5C00C29F2E /* ParallelContributionResponse.swift in Sources */, 8499FED227BFA39300712589 /* DataChangesDiffCalculator.swift in Sources */, 887AFC8C28BCB314002A0422 /* PolkadotIconDetailsView.swift in Sources */, diff --git a/novawallet/Common/EventCenter/EventVisitor.swift b/novawallet/Common/EventCenter/EventVisitor.swift index eb7895a57e..d14af67485 100644 --- a/novawallet/Common/EventCenter/EventVisitor.swift +++ b/novawallet/Common/EventCenter/EventVisitor.swift @@ -5,8 +5,6 @@ protocol EventVisitorProtocol: AnyObject { func processSelectedAccountChanged(event: SelectedAccountChanged) func processSelectedUsernameChanged(event: SelectedUsernameChanged) func processSelectedConnectionChanged(event: SelectedConnectionChanged) - func processBalanceChanged(event: WalletBalanceChanged) - func processStakingChanged(event: WalletStakingInfoChanged) func processNewTransaction(event: WalletNewTransactionInserted) func processPurchaseCompletion(event: PurchaseCompleted) func processTypeRegistryPrepared(event: TypeRegistryPrepared) @@ -34,8 +32,6 @@ extension EventVisitorProtocol { func processChainAccountChanged(event _: ChainAccountChanged) {} func processSelectedAccountChanged(event _: SelectedAccountChanged) {} func processSelectedConnectionChanged(event _: SelectedConnectionChanged) {} - func processBalanceChanged(event _: WalletBalanceChanged) {} - func processStakingChanged(event _: WalletStakingInfoChanged) {} func processNewTransaction(event _: WalletNewTransactionInserted) {} func processSelectedUsernameChanged(event _: SelectedUsernameChanged) {} func processPurchaseCompletion(event _: PurchaseCompleted) {} diff --git a/novawallet/Common/EventCenter/Events/WalletBalanceChanged.swift b/novawallet/Common/EventCenter/Events/WalletBalanceChanged.swift deleted file mode 100644 index e4b2e5babc..0000000000 --- a/novawallet/Common/EventCenter/Events/WalletBalanceChanged.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -struct WalletBalanceChanged: EventProtocol { - func accept(visitor: EventVisitorProtocol) { - visitor.processBalanceChanged(event: self) - } -} diff --git a/novawallet/Common/EventCenter/Events/WalletStakingChanged.swift b/novawallet/Common/EventCenter/Events/WalletStakingChanged.swift deleted file mode 100644 index ea20543b3f..0000000000 --- a/novawallet/Common/EventCenter/Events/WalletStakingChanged.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -struct WalletStakingInfoChanged: EventProtocol { - func accept(visitor: EventVisitorProtocol) { - visitor.processStakingChanged(event: self) - } -} diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/AccountInfoSubscription.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/AccountInfoSubscription.swift index 3d86384b6c..6d7cfe0b54 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/AccountInfoSubscription.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/AccountInfoSubscription.swift @@ -175,8 +175,6 @@ final class AccountInfoSubscription: BaseStorageChildSubscription { let maybeItem = try? changesWrapper.targetOperation.extractNoCancellableResultData() if maybeItem != nil { - self?.eventCenter.notify(with: WalletBalanceChanged()) - let assetBalanceChangeEvent = AssetBalanceChanged( chainAssetId: chainAssetId, accountId: accountId, diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/AssetsBalanceUpdater.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/AssetsBalanceUpdater.swift index 1fa5ed58a1..d8079299f1 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/AssetsBalanceUpdater.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/AssetsBalanceUpdater.swift @@ -128,8 +128,6 @@ final class AssetsBalanceUpdater { let maybeItem = try? changesWrapper.targetOperation.extractNoCancellableResultData() if maybeItem != nil { - self?.eventCenter.notify(with: WalletBalanceChanged()) - let assetBalanceChangeEvent = AssetBalanceChanged( chainAssetId: chainAssetId, accountId: accountId, diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/OrmlAccountSubscription.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/OrmlAccountSubscription.swift index fffb7ea471..1e22d1d5fa 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/OrmlAccountSubscription.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/OrmlAccountSubscription.swift @@ -172,8 +172,6 @@ final class OrmlAccountSubscription: BaseStorageChildSubscription { let maybeItem = try? changesWrapper.targetOperation.extractNoCancellableResultData() if maybeItem != nil { - self?.eventCenter.notify(with: WalletBalanceChanged()) - let assetBalanceChangeEvent = AssetBalanceChanged( chainAssetId: chainAssetId, accountId: accountId, From 5314cd3b53589425d84c3eaa879de9827a090b25 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sun, 25 Sep 2022 15:25:00 +0500 Subject: [PATCH 38/52] fix tests --- novawallet.xcodeproj/project.pbxproj | 12 -- .../WalletLocalSubscriptionFactoryStub.swift | 8 ++ .../BalanceLocks/BalanceLocksTests.swift | 117 ------------------ 3 files changed, 8 insertions(+), 129 deletions(-) delete mode 100644 novawalletTests/Modules/BalanceLocks/BalanceLocksTests.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index a5b4b75a27..bfca5455cf 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -1604,7 +1604,6 @@ 84B7C741289BFA79001A3566 /* AccountConfirmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C6F8289BFA79001A3566 /* AccountConfirmTests.swift */; }; 84B7C742289BFA79001A3566 /* AssetSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C6FA289BFA79001A3566 /* AssetSelectionTests.swift */; }; 84B7C743289BFA79001A3566 /* AnalyticsRewardDetailsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C6FC289BFA79001A3566 /* AnalyticsRewardDetailsTests.swift */; }; - 84B7C744289BFA79001A3566 /* BalanceLocksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C700289BFA79001A3566 /* BalanceLocksTests.swift */; }; 84B7C745289BFA79001A3566 /* AssetsManageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C702289BFA79001A3566 /* AssetsManageTests.swift */; }; 84B7C746289BFA79001A3566 /* WalletHistoryFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C704289BFA79001A3566 /* WalletHistoryFilterTests.swift */; }; 84B7C747289BFA79001A3566 /* AccountManagementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7C706289BFA79001A3566 /* AccountManagementTests.swift */; }; @@ -4360,7 +4359,6 @@ 84B7C6F8289BFA79001A3566 /* AccountConfirmTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountConfirmTests.swift; sourceTree = ""; }; 84B7C6FA289BFA79001A3566 /* AssetSelectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetSelectionTests.swift; sourceTree = ""; }; 84B7C6FC289BFA79001A3566 /* AnalyticsRewardDetailsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsRewardDetailsTests.swift; sourceTree = ""; }; - 84B7C700289BFA79001A3566 /* BalanceLocksTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceLocksTests.swift; sourceTree = ""; }; 84B7C702289BFA79001A3566 /* AssetsManageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetsManageTests.swift; sourceTree = ""; }; 84B7C704289BFA79001A3566 /* WalletHistoryFilterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilterTests.swift; sourceTree = ""; }; 84B7C706289BFA79001A3566 /* AccountManagementTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManagementTests.swift; sourceTree = ""; }; @@ -9738,7 +9736,6 @@ 84B7C6FB289BFA79001A3566 /* AnalyticsRewardDetails */, 84B7C6FD289BFA79001A3566 /* ParaStkStakeConfirm */, 84B7C6FE289BFA79001A3566 /* ChainAddressDetails */, - 84B7C6FF289BFA79001A3566 /* BalanceLocks */, 84B7C701289BFA79001A3566 /* AssetsManage */, 84B7C703289BFA79001A3566 /* WalletHistoryFilter */, 84B7C705289BFA79001A3566 /* AccountManagement */, @@ -10330,14 +10327,6 @@ path = ChainAddressDetails; sourceTree = ""; }; - 84B7C6FF289BFA79001A3566 /* BalanceLocks */ = { - isa = PBXGroup; - children = ( - 84B7C700289BFA79001A3566 /* BalanceLocksTests.swift */, - ); - path = BalanceLocks; - sourceTree = ""; - }; 84B7C701289BFA79001A3566 /* AssetsManage */ = { isa = PBXGroup; children = ( @@ -16085,7 +16074,6 @@ 84B7C745289BFA79001A3566 /* AssetsManageTests.swift in Sources */, 84B7C728289BFA79001A3566 /* YourValidatorListTests.swift in Sources */, 84B7C743289BFA79001A3566 /* AnalyticsRewardDetailsTests.swift in Sources */, - 84B7C744289BFA79001A3566 /* BalanceLocksTests.swift in Sources */, 84B7C72F289BFA79001A3566 /* CustomValidatorListComposerTests.swift in Sources */, 84B7C717289BFA79001A3566 /* DAppAuthConfirmTests.swift in Sources */, 84F4387F25D9D61300AEDA56 /* SubstrateStorageTestFacade.swift in Sources */, diff --git a/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift b/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift index a5bb4fe92f..0812f98505 100644 --- a/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift +++ b/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift @@ -64,4 +64,12 @@ final class WalletLocalSubscriptionFactoryStub: WalletLocalSubscriptionFactoryPr func getLocksProvider(for accountId: AccountId) throws -> StreamableProvider { throw CommonError.undefined } + + func getLocksProvider( + for accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) throws -> StreamableProvider { + throw CommonError.undefined + } } diff --git a/novawalletTests/Modules/BalanceLocks/BalanceLocksTests.swift b/novawalletTests/Modules/BalanceLocks/BalanceLocksTests.swift deleted file mode 100644 index c743429db0..0000000000 --- a/novawalletTests/Modules/BalanceLocks/BalanceLocksTests.swift +++ /dev/null @@ -1,117 +0,0 @@ -import XCTest -@testable import novawallet - -class BalanceLocksTest: XCTestCase { - func testLocksRestoration() throws { - - // given - let context = ["account.balance.price.change.key": "0", - "account.balance.fee.frozen.key": "2.3657237", - "account.balance.locks.key":"[{\"id\":[\"115\",\"116\",\"97\",\"107\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2365723700000\",\"reasons\":2}]", - "account.balance.reserved.key": "1.00205", - "account.balance.misc.frozen.key": "2.3657237", - "account.balance.price.key": "0", - "account.balance.minimal.key": "0.01", - "account.balance.free.key": "5.06791402788"] - - // when - - let balanceContext = BalanceContext.init(context: context) - - // then - - XCTAssertEqual(balanceContext.balanceLocks.count, 1) - } - - func testMainLocksSeparation() throws { - // given - let context = ["account.balance.price.change.key": "0", - "account.balance.fee.frozen.key": "2.3657237", - "account.balance.locks.key":"[{\"id\":[\"115\",\"116\",\"97\",\"107\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2365723700000\",\"reasons\":2},{\"id\":[\"115\",\"116\",\"97\",\"115\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2365723700000\",\"reasons\":2}]", - "account.balance.reserved.key": "1.00205", - "account.balance.misc.frozen.key": "2.3657237", - "account.balance.price.key": "0", - "account.balance.minimal.key": "0.01", - "account.balance.free.key": "5.06791402788"] - - // when - - let balanceContext = BalanceContext.init(context: context) - let mainLocks = balanceContext.balanceLocks.mainLocks() - - // then - - XCTAssertEqual(mainLocks.count, 1) - - XCTAssertEqual(LockType(rawValue: mainLocks.first?.displayId ?? ""), .staking) - } - - func testAuxLocksSeparation() throws { - // given - let context = ["account.balance.price.change.key": "0", - "account.balance.fee.frozen.key": "2.3657237", - "account.balance.locks.key":"[{\"id\":[\"115\",\"116\",\"97\",\"107\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2365723700000\",\"reasons\":2},{\"id\":[\"115\",\"116\",\"97\",\"115\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2365723700000\",\"reasons\":2}]", - "account.balance.reserved.key": "1.00205", - "account.balance.misc.frozen.key": "2.3657237", - "account.balance.price.key": "0", - "account.balance.minimal.key": "0.01", - "account.balance.free.key": "5.06791402788"] - - // when - - let balanceContext = BalanceContext.init(context: context) - let auxLocks = balanceContext.balanceLocks.auxLocks() - - // then - - XCTAssertEqual(auxLocks.count, 1) - - XCTAssertEqual(auxLocks.first?.displayId, "stasing") - } - - func testMainLocksOrder() throws { - // given - let context = ["account.balance.price.change.key": "0", - "account.balance.fee.frozen.key": "2.3657237", - "account.balance.locks.key":"[{\"id\":[\"115\",\"116\",\"97\",\"107\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2365723700000\",\"reasons\":2},{\"id\":[\"118\",\"101\",\"115\",\"116\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2365723700000\",\"reasons\":2}]", - "account.balance.reserved.key": "1.00205", - "account.balance.misc.frozen.key": "2.3657237", - "account.balance.price.key": "0", - "account.balance.minimal.key": "0.01", - "account.balance.free.key": "5.06791402788"] - - // when - - let balanceContext = BalanceContext.init(context: context) - let mainLocks = balanceContext.balanceLocks.mainLocks() - - // then - - XCTAssertEqual(mainLocks.count, 2) - - XCTAssertEqual(LockType(rawValue: mainLocks.first?.displayId ?? ""), .vesting) - } - - func testAuxLocksOrder() throws { - // given - let context = ["account.balance.price.change.key": "0", - "account.balance.fee.frozen.key": "2.3657237", - "account.balance.locks.key":"[{\"id\":[\"107\",\"105\",\"115\",\"115\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2365723700000\",\"reasons\":2},{\"id\":[\"115\",\"116\",\"97\",\"115\",\"105\",\"110\",\"103\",\"32\"],\"amount\":\"2375723700000\",\"reasons\":2}]", - "account.balance.reserved.key": "1.00205", - "account.balance.misc.frozen.key": "2.3657237", - "account.balance.price.key": "0", - "account.balance.minimal.key": "0.01", - "account.balance.free.key": "5.06791402788"] - - // when - - let balanceContext = BalanceContext.init(context: context) - let auxLocks = balanceContext.balanceLocks.auxLocks() - - // then - - XCTAssertEqual(auxLocks.count, 2) - - XCTAssertEqual(auxLocks.first?.displayId, "stasing") - } -} From a22bcbe3e1e0174e6f0aa3635b4e4d0f62b47143 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sun, 25 Sep 2022 17:44:18 +0500 Subject: [PATCH 39/52] display total balance including crowdloans --- .../AssetList/AssetListInteractor.swift | 106 +----------------- .../AssetList/AssetListPresenter.swift | 23 ++-- .../AssetList/AssetListProtocols.swift | 1 - .../Base/AssetListBaseInteractor.swift | 80 +++++++++++++ .../AssetListBaseInteractorProtocol.swift | 1 + .../Base/AssetListBasePresenter.swift | 24 +++- .../AssetsSearch/AssetsSearchPresenter.swift | 20 +++- .../AssetsSearchViewFactory.swift | 1 + 8 files changed, 139 insertions(+), 117 deletions(-) diff --git a/novawallet/Modules/AssetList/AssetListInteractor.swift b/novawallet/Modules/AssetList/AssetListInteractor.swift index 88c0af5ed3..ca267d827c 100644 --- a/novawallet/Modules/AssetList/AssetListInteractor.swift +++ b/novawallet/Modules/AssetList/AssetListInteractor.swift @@ -16,17 +16,14 @@ final class AssetListInteractor: AssetListBaseInteractor { } let nftLocalSubscriptionFactory: NftLocalSubscriptionFactoryProtocol - let crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol let eventCenter: EventCenterProtocol let settingsManager: SettingsManagerProtocol private var nftSubscription: StreamableProvider? private var nftChainIds: Set? - private var crowdloanChainIds = Set() + private var assetLocksSubscriptions: [AccountId: StreamableProvider] = [:] private var locks: [ChainAssetId: [AssetLock]] = [:] - private var crowdloansSubscriptions: [ChainModel.Id: StreamableProvider] = [:] - private var crowdloans: [ChainModel.Id: [CrowdloanContributionData]] = [:] init( selectedWalletSettings: SelectedWalletSettings, @@ -43,12 +40,12 @@ final class AssetListInteractor: AssetListBaseInteractor { self.nftLocalSubscriptionFactory = nftLocalSubscriptionFactory self.eventCenter = eventCenter self.settingsManager = settingsManager - self.crowdloansLocalSubscriptionFactory = crowdloansLocalSubscriptionFactory super.init( selectedWalletSettings: selectedWalletSettings, chainRegistry: chainRegistry, walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, + crowdloansLocalSubscriptionFactory: crowdloansLocalSubscriptionFactory, priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, currencyManager: currencyManager, logger: logger @@ -60,6 +57,7 @@ final class AssetListInteractor: AssetListBaseInteractor { clearNftSubscription() clearLocksSubscription() clearCrowdloansSubscription() + guard let selectedMetaAccount = selectedWalletSettings.value else { return } @@ -77,7 +75,7 @@ final class AssetListInteractor: AssetListBaseInteractor { updateAccountInfoSubscription(from: changes) setupNftSubscription(from: Array(availableChains.values)) updateLocksSubscription(from: changes) - setupCrowdloansSubscription(from: Array(availableChains.values)) + updateCrowdloansSubscription(from: Array(availableChains.values)) } private func clearLocksSubscription() { @@ -110,13 +108,6 @@ final class AssetListInteractor: AssetListBaseInteractor { nftChainIds = nil } - private func clearCrowdloansSubscription() { - crowdloansSubscriptions.values.forEach { $0.removeObserver(self) } - crowdloansSubscriptions = [:] - crowdloans = [:] - crowdloanChainIds = .init() - } - override func applyChanges( allChanges: [DataProviderChange], accountDependentChanges: [DataProviderChange] @@ -126,7 +117,6 @@ final class AssetListInteractor: AssetListBaseInteractor { updateConnectionStatus(from: allChanges) setupNftSubscription(from: Array(availableChains.values)) updateLocksSubscription(from: allChanges) - setupCrowdloansSubscription(from: Array(availableChains.values)) } private func updateConnectionStatus(from changes: [DataProviderChange]) { @@ -159,30 +149,6 @@ final class AssetListInteractor: AssetListBaseInteractor { nftSubscription?.refresh() } - private func setupCrowdloansSubscription(from allChains: [ChainModel]) { - guard let selectedMetaAccount = selectedWalletSettings.value else { - return - } - let crowdloanChains = allChains.filter { $0.hasCrowdloans } - let newCrowdloanChainIds = Set(crowdloanChains.map(\.chainId)) - - guard !crowdloanChains.isEmpty, crowdloanChainIds != newCrowdloanChainIds else { - return - } - - clearCrowdloansSubscription() - crowdloanChainIds = newCrowdloanChainIds - - for chain in crowdloanChains { - guard let accountId = selectedMetaAccount.fetch( - for: chain.accountRequest() - )?.accountId else { - return - } - crowdloansSubscriptions[chain.identifier] = subscribeToCrowdloansProvider(for: accountId, chain: chain) - } - } - override func setup() { provideHidesZeroBalances() providerWalletInfo() @@ -205,32 +171,6 @@ final class AssetListInteractor: AssetListBaseInteractor { } } - override func handleAccountBalance( - result: Result<[DataProviderChange], Error>, - accountId: AccountId - ) { - super.handleAccountBalance(result: result, accountId: accountId) - - switch result { - case let .failure(error): - logger?.error(error.localizedDescription) - case let .success(changes): - var updatingChains = Set() - updatingChains = changes.reduce(into: updatingChains) { accum, change in - if let assetBalanceId = assetBalanceIdMapping[change.identifier], - crowdloanChainIds.contains(assetBalanceId.chainId), - assetBalanceId.accountId == accountId { - accum.insert(assetBalanceId.chainId) - } - } - - updatingChains.forEach { chainId in - crowdloansSubscriptions[chainId]?.refresh() - logger?.debug("Crowdloans for chain: \(chainId) will refresh") - } - } - } - override func handleAccountLocks(result: Result<[DataProviderChange], Error>, accountId: AccountId) { switch result { case let .success(changes): @@ -351,41 +291,3 @@ extension AssetListInteractor: EventVisitorProtocol { provideHidesZeroBalances() } } - -extension AssetListInteractor: CrowdloanContributionLocalSubscriptionHandler, CrowdloansLocalStorageSubscriber { - func handleCrowdloans( - result: Result<[DataProviderChange], Error>, - accountId: AccountId, - chain: ChainModel - ) { - guard let selectedMetaAccount = selectedWalletSettings.value else { - return - } - guard let chainAccountId = selectedMetaAccount.fetch( - for: chain.accountRequest() - )?.accountId, chainAccountId == accountId else { - logger?.warning("Crowdloans updates can't be handled because account for selected wallet for chain: \(chain.name) is different") - return - } - - switch result { - case let .failure(error): - presenter?.didReceiveCrowdloans(result: .failure(error)) - case let .success(changes): - crowdloans = changes.reduce( - into: crowdloans - ) { result, change in - switch change { - case let .insert(crowdloan), let .update(crowdloan): - var items = result[chain.chainId] ?? [] - items.addOrReplaceSingle(crowdloan) - result[chain.chainId] = items - case let .delete(deletedIdentifier): - result[chain.chainId]?.removeAll(where: { $0.identifier == deletedIdentifier }) - } - } - - presenter?.didReceiveCrowdloans(result: .success(crowdloans)) - } - } -} diff --git a/novawallet/Modules/AssetList/AssetListPresenter.swift b/novawallet/Modules/AssetList/AssetListPresenter.swift index d476fd84df..0206e19274 100644 --- a/novawallet/Modules/AssetList/AssetListPresenter.swift +++ b/novawallet/Modules/AssetList/AssetListPresenter.swift @@ -21,7 +21,6 @@ final class AssetListPresenter: AssetListBasePresenter { private var hidesZeroBalances: Bool? private(set) var connectionStates: [ChainModel.Id: WebSocketEngine.State] = [:] private(set) var locksResult: Result<[AssetLock], Error>? - private(set) var crowdloansResult: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>? private var scheduler: SchedulerProtocol? @@ -215,10 +214,12 @@ final class AssetListPresenter: AssetListBasePresenter { } let maybePrices = try? priceResult?.get() + let maybeCrowdloans = try? crowdloansResult?.get() let viewModels: [AssetListGroupViewModel] = groups.allItems.compactMap { groupModel in createGroupViewModel( from: groupModel, maybePrices: maybePrices, + maybeCrowdloans: maybeCrowdloans, hidesZeroBalances: hidesZeroBalances ) } @@ -257,6 +258,7 @@ final class AssetListPresenter: AssetListBasePresenter { private func createGroupViewModel( from groupModel: AssetListGroupModel, maybePrices: [ChainAssetId: PriceData]?, + maybeCrowdloans: [ChainModel.Id: [CrowdloanContributionData]]?, hidesZeroBalances: Bool ) -> AssetListGroupViewModel? { let chain = groupModel.chain @@ -290,7 +292,12 @@ final class AssetListPresenter: AssetListBasePresenter { } let assetInfoList: [AssetListAssetAccountInfo] = filteredAssets.map { asset in - createAssetAccountInfo(from: asset, chain: chain, maybePrices: maybePrices) + createAssetAccountInfo( + from: asset, + chain: chain, + maybePrices: maybePrices, + maybeCrowdloans: maybeCrowdloans + ) } return viewModelFactory.createGroupViewModel( @@ -379,6 +386,12 @@ final class AssetListPresenter: AssetListBasePresenter { updateAssetsView() } + + override func didReceiveCrowdloans(result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) { + super.didReceiveCrowdloans(result: result) + + updateAssetsView() + } } extension AssetListPresenter: AssetListPresenterProtocol { @@ -483,12 +496,6 @@ extension AssetListPresenter: AssetListInteractorOutputProtocol { updateHeaderView() } - - func didReceiveCrowdloans(result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) { - crowdloansResult = result - - updateHeaderView() - } } extension AssetListPresenter: Localizable { diff --git a/novawallet/Modules/AssetList/AssetListProtocols.swift b/novawallet/Modules/AssetList/AssetListProtocols.swift index e8e22d41b7..4aee0b3c7c 100644 --- a/novawallet/Modules/AssetList/AssetListProtocols.swift +++ b/novawallet/Modules/AssetList/AssetListProtocols.swift @@ -34,7 +34,6 @@ protocol AssetListInteractorOutputProtocol: AssetListBaseInteractorOutputProtoco func didChange(name: String) func didReceive(hidesZeroBalances: Bool) func didReceiveLocks(result: Result<[AssetLock], Error>) - func didReceiveCrowdloans(result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) } protocol AssetListWireframeProtocol: AnyObject, WalletSwitchPresentable { diff --git a/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift b/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift index e6d7101c39..4f27e749c2 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift @@ -10,11 +10,17 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip let selectedWalletSettings: SelectedWalletSettings let chainRegistry: ChainRegistryProtocol let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol + let crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol let logger: LoggerProtocol? private(set) var assetBalanceSubscriptions: [AccountId: StreamableProvider] = [:] private(set) var assetBalanceIdMapping: [String: AssetBalanceId] = [:] + + private var crowdloansSubscriptions: [ChainModel.Id: StreamableProvider] = [:] + private var crowdloans: [ChainModel.Id: [CrowdloanContributionData]] = [:] + private var crowdloanChainIds = Set() + private(set) var priceSubscription: AnySingleValueProvider<[PriceData]>? private(set) var availableTokenPrice: [ChainAssetId: AssetModel.PriceId] = [:] private(set) var availableChains: [ChainModel.Id: ChainModel] = [:] @@ -23,6 +29,7 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip selectedWalletSettings: SelectedWalletSettings, chainRegistry: ChainRegistryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, currencyManager: CurrencyManagerProtocol, logger: LoggerProtocol? = nil @@ -30,6 +37,7 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip self.selectedWalletSettings = selectedWalletSettings self.chainRegistry = chainRegistry self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory + self.crowdloansLocalSubscriptionFactory = crowdloansLocalSubscriptionFactory self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory self.logger = logger self.currencyManager = currencyManager @@ -42,6 +50,13 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip assetBalanceIdMapping = [:] } + func clearCrowdloansSubscription() { + crowdloansSubscriptions.values.forEach { $0.removeObserver(self) } + crowdloansSubscriptions = [:] + crowdloans = [:] + crowdloanChainIds = .init() + } + private func handle(changes: [DataProviderChange]) { guard let selectedMetaAccount = selectedWalletSettings.value else { return @@ -67,6 +82,7 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip updateAvailableChains(from: allChanges) updateAccountInfoSubscription(from: accountDependentChanges) updatePriceSubscription(from: allChanges) + updateCrowdloansSubscription(from: Array(availableChains.values)) } func updateAvailableChains(from changes: [DataProviderChange]) { @@ -206,6 +222,32 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip ) } + func updateCrowdloansSubscription(from allChains: [ChainModel]) { + guard let selectedMetaAccount = selectedWalletSettings.value else { + return + } + + let crowdloanChains = allChains.filter { $0.hasCrowdloans } + let newCrowdloanChainIds = Set(crowdloanChains.map(\.chainId)) + + guard !crowdloanChains.isEmpty, crowdloanChainIds != newCrowdloanChainIds else { + return + } + + clearCrowdloansSubscription() + crowdloanChainIds = newCrowdloanChainIds + + for chain in crowdloanChains { + let request = chain.accountRequest() + + guard let accountId = selectedMetaAccount.fetch(for: request)?.accountId else { + return + } + + crowdloansSubscriptions[chain.identifier] = subscribeToCrowdloansProvider(for: accountId, chain: chain) + } + } + func subscribeChains() { chainRegistry.chainsSubscribe(self, runningInQueue: .main) { [weak self] changes in self?.handle(changes: changes) @@ -306,6 +348,44 @@ extension AssetListBaseInteractor { } } +extension AssetListBaseInteractor: CrowdloanContributionLocalSubscriptionHandler, CrowdloansLocalStorageSubscriber { + func handleCrowdloans( + result: Result<[DataProviderChange], Error>, + accountId: AccountId, + chain: ChainModel + ) { + guard let selectedMetaAccount = selectedWalletSettings.value else { + return + } + guard let chainAccountId = selectedMetaAccount.fetch( + for: chain.accountRequest() + )?.accountId, chainAccountId == accountId else { + logger?.warning("Crowdloans updates can't be handled because account for selected wallet for chain: \(chain.name) is different") + return + } + + switch result { + case let .failure(error): + basePresenter?.didReceiveCrowdloans(result: .failure(error)) + case let .success(changes): + crowdloans = changes.reduce( + into: crowdloans + ) { result, change in + switch change { + case let .insert(crowdloan), let .update(crowdloan): + var items = result[chain.chainId] ?? [] + items.addOrReplaceSingle(crowdloan) + result[chain.chainId] = items + case let .delete(deletedIdentifier): + result[chain.chainId]?.removeAll(where: { $0.identifier == deletedIdentifier }) + } + } + + basePresenter?.didReceiveCrowdloans(result: .success(crowdloans)) + } + } +} + extension AssetListBaseInteractor: SelectedCurrencyDepending { func applyCurrency() { guard basePresenter != nil else { diff --git a/novawallet/Modules/AssetList/Base/AssetListBaseInteractorProtocol.swift b/novawallet/Modules/AssetList/Base/AssetListBaseInteractorProtocol.swift index 3414620c27..9c2c778fdd 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBaseInteractorProtocol.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBaseInteractorProtocol.swift @@ -9,5 +9,6 @@ protocol AssetListBaseInteractorInputProtocol: AnyObject { protocol AssetListBaseInteractorOutputProtocol: AnyObject { func didReceiveChainModelChanges(_ changes: [DataProviderChange]) func didReceiveBalance(results: [ChainAssetId: Result]) + func didReceiveCrowdloans(result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) func didReceivePrices(result: Result<[ChainAssetId: PriceData], Error>?) } diff --git a/novawallet/Modules/AssetList/Base/AssetListBasePresenter.swift b/novawallet/Modules/AssetList/Base/AssetListBasePresenter.swift index 9e49e8f329..8adc8f3e76 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBasePresenter.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBasePresenter.swift @@ -10,6 +10,7 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { private(set) var balanceResults: [ChainAssetId: Result] = [:] private(set) var balances: [ChainAssetId: Result] = [:] private(set) var allChains: [ChainModel.Id: ChainModel] = [:] + private(set) var crowdloansResult: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>? init() { groups = Self.createGroupsDiffCalculator(from: []) @@ -53,7 +54,8 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { func createAssetAccountInfo( from asset: AssetListAssetModel, chain: ChainModel, - maybePrices: [ChainAssetId: PriceData]? + maybePrices: [ChainAssetId: PriceData]?, + maybeCrowdloans: [ChainModel.Id: [CrowdloanContributionData]]? ) -> AssetListAssetAccountInfo { let assetModel = asset.assetModel let chainAssetId = ChainAssetId(chainId: chain.chainId, assetId: assetModel.assetId) @@ -68,12 +70,24 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { priceData = nil } - let balance = try? asset.balanceResult?.get() + let maybeBalance = try? asset.balanceResult?.get() + + let maybeContributions = maybeCrowdloans?[chain.chainId]?.reduce(BigUInt(0)) { result, contribution in + result + contribution.amount + } + + let totalBalance: BigUInt? + + if let balance = maybeBalance, let contribution = maybeContributions { + totalBalance = balance + contribution + } else { + totalBalance = maybeBalance ?? maybeContributions + } return AssetListAssetAccountInfo( assetId: asset.assetModel.assetId, assetInfo: assetInfo, - balance: balance, + balance: totalBalance, priceData: priceData ) } @@ -185,4 +199,8 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { groups.apply(changes: groupChanges) } + + func didReceiveCrowdloans(result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) { + crowdloansResult = result + } } diff --git a/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift b/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift index 2c0cf6d19e..6f972136cc 100644 --- a/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift +++ b/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift @@ -114,8 +114,10 @@ final class AssetsSearchPresenter: AssetListBasePresenter { private func provideAssetsViewModel() { let maybePrices = try? priceResult?.get() + let maybeCrowdloans = try? crowdloansResult?.get() + let viewModels: [AssetListGroupViewModel] = groups.allItems.compactMap { groupModel in - createGroupViewModel(from: groupModel, maybePrices: maybePrices) + createGroupViewModel(from: groupModel, maybePrices: maybePrices, maybeCrowdloans: maybeCrowdloans) } if viewModels.isEmpty, !balanceResults.isEmpty, balanceResults.count >= allChains.count { @@ -127,14 +129,20 @@ final class AssetsSearchPresenter: AssetListBasePresenter { private func createGroupViewModel( from groupModel: AssetListGroupModel, - maybePrices: [ChainAssetId: PriceData]? + maybePrices: [ChainAssetId: PriceData]?, + maybeCrowdloans: [ChainModel.Id: [CrowdloanContributionData]]? ) -> AssetListGroupViewModel? { let chain = groupModel.chain let assets = groupLists[chain.chainId]?.allItems ?? [] let assetInfoList: [AssetListAssetAccountInfo] = assets.map { asset in - createAssetAccountInfo(from: asset, chain: chain, maybePrices: maybePrices) + createAssetAccountInfo( + from: asset, + chain: chain, + maybePrices: maybePrices, + maybeCrowdloans: maybeCrowdloans + ) } return viewModelFactory.createGroupViewModel( @@ -163,6 +171,12 @@ final class AssetsSearchPresenter: AssetListBasePresenter { filterAndUpdateView() } + + override func didReceiveCrowdloans(result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) { + super.didReceiveCrowdloans(result: result) + + filterAndUpdateView() + } } extension AssetsSearchPresenter: AssetsSearchPresenterProtocol { diff --git a/novawallet/Modules/AssetsSearch/AssetsSearchViewFactory.swift b/novawallet/Modules/AssetsSearch/AssetsSearchViewFactory.swift index 888366af9e..c2f86a41a0 100644 --- a/novawallet/Modules/AssetsSearch/AssetsSearchViewFactory.swift +++ b/novawallet/Modules/AssetsSearch/AssetsSearchViewFactory.swift @@ -13,6 +13,7 @@ struct AssetsSearchViewFactory { selectedWalletSettings: SelectedWalletSettings.shared, chainRegistry: ChainRegistryFacade.sharedRegistry, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, currencyManager: currencyManager, logger: Logger.shared From bbc4f23b56cbb4f8c02b7dff062eaf929687cdb0 Mon Sep 17 00:00:00 2001 From: ERussel Date: Sun, 25 Sep 2022 18:13:01 +0500 Subject: [PATCH 40/52] fix sorting of locks on Asset Details --- .../ModalPicker/ModalInfoFactory.swift | 140 +++++++++++------- 1 file changed, 84 insertions(+), 56 deletions(-) diff --git a/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift b/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift index 6a8f9fecbc..fe84754aef 100644 --- a/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift +++ b/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift @@ -8,6 +8,9 @@ struct ModalInfoFactory { static let headerHeight: CGFloat = 40.0 static let footerHeight: CGFloat = 0.0 + typealias LockSortingViewModel = (value: Decimal, viewModel: LocalizableResource) + typealias LocksSortingViewModel = [LockSortingViewModel] + static func createParaStkRewardDetails( for maxReward: Decimal, avgReward: Decimal, @@ -238,26 +241,11 @@ struct ModalInfoFactory { priceFormatter: LocalizableResource, precision: Int16 ) -> [LocalizableResource] { - let staticModels: [LocalizableResource] = [ - LocalizableResource { locale in - let title = R.string.localizable - .walletBalanceReserved(preferredLanguages: locale.rLanguages) - - let amountString = amountFormatter.value(for: locale).stringFromDecimal(balanceContext.reserved) ?? "" - - let formatter = priceFormatter.value(for: locale) - - let price = balanceContext.reserved * balanceContext.price - let priceString = balanceContext.price == 0.0 ? nil : formatter.stringFromDecimal(price) - - let balance = BalanceViewModel( - amount: amountString, - price: priceString - ) - - return StakingAmountViewModel(title: title, balance: balance) - } - ] + let reserved: LocksSortingViewModel = createReservedViewModel( + balanceContext: balanceContext, + amountFormatter: amountFormatter, + priceFormatter: priceFormatter + ) let crowdloans = createCrowdloansViewModel( balanceContext: balanceContext, @@ -265,25 +253,26 @@ struct ModalInfoFactory { priceFormatter: priceFormatter ) - let balanceLockKnownModels: [LocalizableResource] = - createLockViewModel( - from: balanceContext.balanceLocks.mainLocks(), - balanceContext: balanceContext, - amountFormatter: amountFormatter, - priceFormatter: priceFormatter, - precision: precision - ) - - let balanceLockUnknownModels: [LocalizableResource] = - createLockViewModel( - from: balanceContext.balanceLocks.auxLocks(), - balanceContext: balanceContext, - amountFormatter: amountFormatter, - priceFormatter: priceFormatter, - precision: precision - ) + let balanceLockKnownModels: LocksSortingViewModel = createLockViewModel( + from: balanceContext.balanceLocks.mainLocks(), + balanceContext: balanceContext, + amountFormatter: amountFormatter, + priceFormatter: priceFormatter, + precision: precision + ) + + let balanceLockUnknownModels: LocksSortingViewModel = createLockViewModel( + from: balanceContext.balanceLocks.auxLocks(), + balanceContext: balanceContext, + amountFormatter: amountFormatter, + priceFormatter: priceFormatter, + precision: precision + ) - return crowdloans + balanceLockKnownModels + balanceLockUnknownModels + staticModels + return (balanceLockKnownModels + balanceLockUnknownModels + crowdloans + reserved) + .sorted { viewModel1, viewModel2 in + viewModel1.value >= viewModel2.value + }.map(\.viewModel) } private static func createLockViewModel( @@ -292,9 +281,16 @@ struct ModalInfoFactory { amountFormatter: LocalizableResource, priceFormatter: LocalizableResource, precision: Int16 - ) -> [LocalizableResource] { + ) -> LocksSortingViewModel { locks.map { lock in - LocalizableResource { locale in + let lockAmount = Decimal.fromSubstrateAmount( + lock.amount, + precision: precision + ) ?? 0.0 + + let price = lockAmount * balanceContext.price + + let viewModel = LocalizableResource { locale in let formatter = priceFormatter.value(for: locale) let amountFormatter = amountFormatter.value(for: locale) @@ -305,12 +301,6 @@ struct ModalInfoFactory { return mainTitle }() - let lockAmount = Decimal.fromSubstrateAmount( - lock.amount, - precision: precision - ) ?? 0.0 - let price = lockAmount * balanceContext.price - let priceString = balanceContext.price == 0.0 ? nil : formatter.stringFromDecimal(price) let amountString = amountFormatter.stringFromDecimal(lockAmount) ?? "" @@ -319,10 +309,10 @@ struct ModalInfoFactory { price: priceString ) - return StakingAmountViewModel( - title: title, balance: balance - ) + return StakingAmountViewModel(title: title, balance: balance) } + + return (price, viewModel) } } @@ -330,27 +320,65 @@ struct ModalInfoFactory { balanceContext: BalanceContext, amountFormatter: LocalizableResource, priceFormatter: LocalizableResource - ) -> [LocalizableResource] { + ) -> LocksSortingViewModel { guard balanceContext.crowdloans > 0 else { return [] } + let title = LocalizableResource { locale in + R.string.localizable.walletAccountLocksCrowdloans(preferredLanguages: locale.rLanguages) + } + + return createLockFieldViewModel( + amount: balanceContext.crowdloans, + price: balanceContext.price, + localizedTitle: title, + amountFormatter: amountFormatter, + priceFormatter: priceFormatter + ) + } + + private static func createReservedViewModel( + balanceContext: BalanceContext, + amountFormatter: LocalizableResource, + priceFormatter: LocalizableResource + ) -> LocksSortingViewModel { + let title = LocalizableResource { locale in + R.string.localizable.walletBalanceReserved(preferredLanguages: locale.rLanguages) + } + + return createLockFieldViewModel( + amount: balanceContext.reserved, + price: balanceContext.price, + localizedTitle: title, + amountFormatter: amountFormatter, + priceFormatter: priceFormatter + ) + } + + private static func createLockFieldViewModel( + amount: Decimal, + price: Decimal, + localizedTitle: LocalizableResource, + amountFormatter: LocalizableResource, + priceFormatter: LocalizableResource + ) -> LocksSortingViewModel { + let totalPrice = amount * price + let viewModel = LocalizableResource { locale in let formatter = priceFormatter.value(for: locale) let amountFormatter = amountFormatter.value(for: locale) - let title = R.string.localizable.walletAccountLocksCrowdloans(preferredLanguages: locale.rLanguages) - - let price = balanceContext.crowdloans * balanceContext.price + let title = localizedTitle.value(for: locale) - let priceString = balanceContext.price == 0.0 ? nil : formatter.stringFromDecimal(price) - let amountString = amountFormatter.stringFromDecimal(balanceContext.crowdloans) ?? "" + let priceString = totalPrice == 0.0 ? nil : formatter.stringFromDecimal(totalPrice) + let amountString = amountFormatter.stringFromDecimal(amount) ?? "" let balance = BalanceViewModel(amount: amountString, price: priceString) return StakingAmountViewModel(title: title, balance: balance) } - return [viewModel] + return [LockSortingViewModel(value: totalPrice, viewModel: viewModel)] } } From bc5bcd93677bba898aa19e8123ff88895c66b20f Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Mon, 26 Sep 2022 18:43:24 +0300 Subject: [PATCH 41/52] added crowdloans to wallets list --- .../Common/WalletsListInteractor.swift | 10 ++- .../Common/WalletsListPresenter.swift | 30 +++++++++ .../Common/WalletsListProtocols.swift | 1 + .../Common/WalletsListViewModelFactory.swift | 64 +++++++++++++++++++ 4 files changed, 104 insertions(+), 1 deletion(-) diff --git a/novawallet/Modules/WalletsList/Common/WalletsListInteractor.swift b/novawallet/Modules/WalletsList/Common/WalletsListInteractor.swift index 6a96f38f91..04ce057717 100644 --- a/novawallet/Modules/WalletsList/Common/WalletsListInteractor.swift +++ b/novawallet/Modules/WalletsList/Common/WalletsListInteractor.swift @@ -140,6 +140,7 @@ extension WalletsListInteractor: WalletsListInteractorInputProtocol { subscribeChains() subscribeAssets() subscribeWallets() + subscribeToCrowdloans() } } @@ -176,5 +177,12 @@ extension WalletsListInteractor: SelectedCurrencyDepending { } extension WalletsListInteractor: CrowdloanContributionLocalSubscriptionHandler, CrowdloansLocalStorageSubscriber { - func handleAllCrowdloans(result _: Result<[DataProviderChange], Error>) {} + func handleAllCrowdloans(result: Result<[DataProviderChange], Error>) { + switch result { + case let .success(changes): + basePresenter?.didReceiveCrowdloanContributionChanges(changes) + case let .failure(error): + basePresenter?.didReceiveError(error) + } + } } diff --git a/novawallet/Modules/WalletsList/Common/WalletsListPresenter.swift b/novawallet/Modules/WalletsList/Common/WalletsListPresenter.swift index bd02eeb879..afa5bf4105 100644 --- a/novawallet/Modules/WalletsList/Common/WalletsListPresenter.swift +++ b/novawallet/Modules/WalletsList/Common/WalletsListPresenter.swift @@ -24,6 +24,8 @@ class WalletsListPresenter { private var identifierMapping: [String: AssetBalanceId] = [:] private var balances: [AccountId: [ChainAssetId: BigUInt]] = [:] + private var crowdloanContributions: [AccountId: [ChainModel.Id: BigUInt]] = [:] + private var crowdloanContributionsMapping: [String: CrowdloanContributionData] = [:] private var prices: [ChainAssetId: PriceData] = [:] private var chains: [ChainModel.Id: ChainModel] = [:] @@ -51,6 +53,7 @@ class WalletsListPresenter { for: walletsList.allItems, chains: chains, balances: balances, + crowdloanContributions: crowdloanContributions, prices: prices, locale: selectedLocale ) @@ -127,6 +130,33 @@ extension WalletsListPresenter: WalletsListInteractorOutputProtocol { updateViewModels() } + func didReceiveCrowdloanContributionChanges(_ changes: [DataProviderChange]) { + for change in changes { + switch change { + case let .insert(item), let .update(item): + var accountCrowdloan = crowdloanContributions[item.accountId] ?? [:] + let value: BigUInt = accountCrowdloan[item.chainId] ?? 0 + accountCrowdloan[item.chainId] = value + item.amount + crowdloanContributions[item.accountId] = accountCrowdloan + crowdloanContributionsMapping[item.identifier] = item + case let .delete(deletedIdentifier): + if let accountCrowdloanId = crowdloanContributionsMapping[deletedIdentifier] { + var accountCrowdloan = crowdloanContributions[accountCrowdloanId.accountId] + if let contribution = accountCrowdloan?[accountCrowdloanId.chainId], contribution > accountCrowdloanId.amount { + accountCrowdloan?[accountCrowdloanId.chainId] = min(0, contribution - accountCrowdloanId.amount) + } else { + accountCrowdloan?[accountCrowdloanId.chainId] = nil + } + crowdloanContributions[accountCrowdloanId.accountId] = accountCrowdloan + } + + crowdloanContributionsMapping[deletedIdentifier] = nil + } + } + + updateViewModels() + } + func didReceiveError(_ error: Error) { logger.error("Did receive error: \(error)") diff --git a/novawallet/Modules/WalletsList/Common/WalletsListProtocols.swift b/novawallet/Modules/WalletsList/Common/WalletsListProtocols.swift index 5cc28a4a8a..d53d5b1d15 100644 --- a/novawallet/Modules/WalletsList/Common/WalletsListProtocols.swift +++ b/novawallet/Modules/WalletsList/Common/WalletsListProtocols.swift @@ -23,6 +23,7 @@ protocol WalletsListInteractorOutputProtocol: AnyObject { func didReceiveBalancesChanges(_ changes: [DataProviderChange]) func didReceiveChainChanges(_ changes: [DataProviderChange]) func didReceivePrices(_ prices: [ChainAssetId: PriceData]) + func didReceiveCrowdloanContributionChanges(_ changes: [DataProviderChange]) func didReceiveError(_ error: Error) } diff --git a/novawallet/Modules/WalletsList/Common/WalletsListViewModelFactory.swift b/novawallet/Modules/WalletsList/Common/WalletsListViewModelFactory.swift index 182664fed5..bf2cd51e60 100644 --- a/novawallet/Modules/WalletsList/Common/WalletsListViewModelFactory.swift +++ b/novawallet/Modules/WalletsList/Common/WalletsListViewModelFactory.swift @@ -8,6 +8,7 @@ protocol WalletsListViewModelFactoryProtocol { for wallets: [ManagedMetaAccountModel], chains: [ChainModel.Id: ChainModel], balances: [AccountId: [ChainAssetId: BigUInt]], + crowdloanContributions: [AccountId: [ChainModel.Id: BigUInt]], prices: [ChainAssetId: PriceData], locale: Locale ) -> [WalletsListSectionViewModel] @@ -16,6 +17,7 @@ protocol WalletsListViewModelFactoryProtocol { for wallet: ManagedMetaAccountModel, chains: [ChainModel.Id: ChainModel], balances: [AccountId: [ChainAssetId: BigUInt]], + crowdloanContributions: [AccountId: [ChainModel.Id: BigUInt]], prices: [ChainAssetId: PriceData], locale: Locale ) -> WalletsListViewModel @@ -69,6 +71,7 @@ final class WalletsListViewModelFactory { for wallet: ManagedMetaAccountModel, chains: [ChainModel.Id: ChainModel], balances: [AccountId: [ChainAssetId: BigUInt]], + crowdloanContributions: [AccountId: [ChainModel.Id: BigUInt]], prices: [ChainAssetId: PriceData] ) -> Decimal { let chainAccountIds = wallet.info.chainAccounts.map(\.chainId) @@ -83,6 +86,24 @@ final class WalletsListViewModelFactory { includingChainIds: Set(), excludingChainIds: Set(chainAccountIds) ) + + totalValue += crowdloanContributions[substrateAccountId]? + .filter { !chainAccountIds.contains($0.key) } + .reduce(Decimal(0)) { result, contribution in + guard let asset = chains[contribution.key]?.utilityAsset(), + let priceData = prices[ChainAssetId(chainId: contribution.key, assetId: asset.assetId)], + let price = Decimal(string: priceData.price) else { + return result + } + guard let decimalAmount = Decimal.fromSubstrateAmount( + contribution.value, + precision: Int16(bitPattern: asset.precision) + ) else { + return result + } + + return result + decimalAmount * price + } ?? 0 } if let ethereumAddress = wallet.info.ethereumAddress { @@ -93,6 +114,24 @@ final class WalletsListViewModelFactory { includingChainIds: Set(), excludingChainIds: Set(chainAccountIds) ) + + totalValue += crowdloanContributions[ethereumAddress]? + .filter { !chainAccountIds.contains($0.key) } + .reduce(Decimal(0)) { result, contribution in + guard let asset = chains[contribution.key]?.utilityAsset(), + let priceData = prices[ChainAssetId(chainId: contribution.key, assetId: asset.assetId)], + let price = Decimal(string: priceData.price) else { + return result + } + guard let decimalAmount = Decimal.fromSubstrateAmount( + contribution.value, + precision: Int16(bitPattern: asset.precision) + ) else { + return result + } + + return result + decimalAmount * price + } ?? 0 } wallet.info.chainAccounts.forEach { chainAccount in @@ -103,6 +142,22 @@ final class WalletsListViewModelFactory { includingChainIds: [chainAccount.chainId], excludingChainIds: Set() ) + totalValue += crowdloanContributions[chainAccount.accountId]? + .reduce(Decimal(0)) { result, contribution in + guard let asset = chains[contribution.key]?.utilityAsset(), + let priceData = prices[ChainAssetId(chainId: contribution.key, assetId: asset.assetId)], + let price = Decimal(string: priceData.price) else { + return result + } + guard let decimalAmount = Decimal.fromSubstrateAmount( + contribution.value, + precision: Int16(bitPattern: asset.precision) + ) else { + return result + } + + return result + decimalAmount * price + } ?? 0 } return totalValue @@ -113,6 +168,7 @@ final class WalletsListViewModelFactory { wallets: [ManagedMetaAccountModel], chains: [ChainModel.Id: ChainModel], balances: [AccountId: [ChainAssetId: BigUInt]], + crowdloanContributions: [AccountId: [ChainModel.Id: BigUInt]], prices: [ChainAssetId: PriceData], locale: Locale ) -> WalletsListSectionViewModel? { @@ -125,6 +181,7 @@ final class WalletsListViewModelFactory { for: $0, chains: chains, balances: balances, + crowdloanContributions: crowdloanContributions, prices: prices, locale: locale ) @@ -143,6 +200,7 @@ extension WalletsListViewModelFactory: WalletsListViewModelFactoryProtocol { for wallet: ManagedMetaAccountModel, chains: [ChainModel.Id: ChainModel], balances: [AccountId: [ChainAssetId: BigUInt]], + crowdloanContributions: [AccountId: [ChainModel.Id: BigUInt]], prices: [ChainAssetId: PriceData], locale: Locale ) -> WalletsListViewModel { @@ -150,6 +208,7 @@ extension WalletsListViewModelFactory: WalletsListViewModelFactoryProtocol { for: wallet, chains: chains, balances: balances, + crowdloanContributions: crowdloanContributions, prices: prices ) @@ -176,6 +235,7 @@ extension WalletsListViewModelFactory: WalletsListViewModelFactoryProtocol { for wallets: [ManagedMetaAccountModel], chains: [ChainModel.Id: ChainModel], balances: [AccountId: [ChainAssetId: BigUInt]], + crowdloanContributions: [AccountId: [ChainModel.Id: BigUInt]], prices: [ChainAssetId: PriceData], locale: Locale ) -> [WalletsListSectionViewModel] { @@ -187,6 +247,7 @@ extension WalletsListViewModelFactory: WalletsListViewModelFactoryProtocol { wallets: wallets, chains: chains, balances: balances, + crowdloanContributions: crowdloanContributions, prices: prices, locale: locale ) { @@ -199,6 +260,7 @@ extension WalletsListViewModelFactory: WalletsListViewModelFactoryProtocol { wallets: wallets, chains: chains, balances: balances, + crowdloanContributions: crowdloanContributions, prices: prices, locale: locale ) { @@ -211,6 +273,7 @@ extension WalletsListViewModelFactory: WalletsListViewModelFactoryProtocol { wallets: wallets, chains: chains, balances: balances, + crowdloanContributions: crowdloanContributions, prices: prices, locale: locale ) { @@ -223,6 +286,7 @@ extension WalletsListViewModelFactory: WalletsListViewModelFactoryProtocol { wallets: wallets, chains: chains, balances: balances, + crowdloanContributions: crowdloanContributions, prices: prices, locale: locale ) { From 360164df1d8e4018c835ffa76a707c4816a05257 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Mon, 26 Sep 2022 20:03:24 +0300 Subject: [PATCH 42/52] cleanup --- .../Common/CrowdloanContributionId.swift | 7 ++ .../Common/WalletsListPresenter.swift | 22 +++-- .../Common/WalletsListViewModelFactory.swift | 90 +++++++++---------- 3 files changed, 61 insertions(+), 58 deletions(-) create mode 100644 novawallet/Modules/WalletsList/Common/CrowdloanContributionId.swift diff --git a/novawallet/Modules/WalletsList/Common/CrowdloanContributionId.swift b/novawallet/Modules/WalletsList/Common/CrowdloanContributionId.swift new file mode 100644 index 0000000000..c3003357ad --- /dev/null +++ b/novawallet/Modules/WalletsList/Common/CrowdloanContributionId.swift @@ -0,0 +1,7 @@ +import BigInt + +struct CrowdloanContributionId { + let chainId: ChainModel.Id + let accountId: AccountId + let amount: BigUInt +} diff --git a/novawallet/Modules/WalletsList/Common/WalletsListPresenter.swift b/novawallet/Modules/WalletsList/Common/WalletsListPresenter.swift index afa5bf4105..75c7226b82 100644 --- a/novawallet/Modules/WalletsList/Common/WalletsListPresenter.swift +++ b/novawallet/Modules/WalletsList/Common/WalletsListPresenter.swift @@ -25,7 +25,7 @@ class WalletsListPresenter { private var identifierMapping: [String: AssetBalanceId] = [:] private var balances: [AccountId: [ChainAssetId: BigUInt]] = [:] private var crowdloanContributions: [AccountId: [ChainModel.Id: BigUInt]] = [:] - private var crowdloanContributionsMapping: [String: CrowdloanContributionData] = [:] + private var crowdloanContributionsMapping: [String: CrowdloanContributionId] = [:] private var prices: [ChainAssetId: PriceData] = [:] private var chains: [ChainModel.Id: ChainModel] = [:] @@ -138,16 +138,22 @@ extension WalletsListPresenter: WalletsListInteractorOutputProtocol { let value: BigUInt = accountCrowdloan[item.chainId] ?? 0 accountCrowdloan[item.chainId] = value + item.amount crowdloanContributions[item.accountId] = accountCrowdloan - crowdloanContributionsMapping[item.identifier] = item + crowdloanContributionsMapping[item.identifier] = CrowdloanContributionId( + chainId: item.chainId, + accountId: item.accountId, + amount: item.amount + ) case let .delete(deletedIdentifier): - if let accountCrowdloanId = crowdloanContributionsMapping[deletedIdentifier] { - var accountCrowdloan = crowdloanContributions[accountCrowdloanId.accountId] - if let contribution = accountCrowdloan?[accountCrowdloanId.chainId], contribution > accountCrowdloanId.amount { - accountCrowdloan?[accountCrowdloanId.chainId] = min(0, contribution - accountCrowdloanId.amount) + if let accountContributionId = crowdloanContributionsMapping[deletedIdentifier] { + var accountContributions = crowdloanContributions[accountContributionId.accountId] + if let contribution = accountContributions?[accountContributionId.chainId], + contribution > accountContributionId.amount { + let newAmount = contribution - accountContributionId.amount + accountContributions?[accountContributionId.chainId] = newAmount } else { - accountCrowdloan?[accountCrowdloanId.chainId] = nil + accountContributions?[accountContributionId.chainId] = nil } - crowdloanContributions[accountCrowdloanId.accountId] = accountCrowdloan + crowdloanContributions[accountContributionId.accountId] = accountContributions } crowdloanContributionsMapping[deletedIdentifier] = nil diff --git a/novawallet/Modules/WalletsList/Common/WalletsListViewModelFactory.swift b/novawallet/Modules/WalletsList/Common/WalletsListViewModelFactory.swift index bf2cd51e60..ca7aac71f6 100644 --- a/novawallet/Modules/WalletsList/Common/WalletsListViewModelFactory.swift +++ b/novawallet/Modules/WalletsList/Common/WalletsListViewModelFactory.swift @@ -87,23 +87,12 @@ final class WalletsListViewModelFactory { excludingChainIds: Set(chainAccountIds) ) - totalValue += crowdloanContributions[substrateAccountId]? - .filter { !chainAccountIds.contains($0.key) } - .reduce(Decimal(0)) { result, contribution in - guard let asset = chains[contribution.key]?.utilityAsset(), - let priceData = prices[ChainAssetId(chainId: contribution.key, assetId: asset.assetId)], - let price = Decimal(string: priceData.price) else { - return result - } - guard let decimalAmount = Decimal.fromSubstrateAmount( - contribution.value, - precision: Int16(bitPattern: asset.precision) - ) else { - return result - } - - return result + decimalAmount * price - } ?? 0 + let contributions = crowdloanContributions[substrateAccountId]?.filter { !chainAccountIds.contains($0.key) } ?? [:] + totalValue += calculateCrowdloanContribution( + contributions, + chains: chains, + prices: prices + ) } if let ethereumAddress = wallet.info.ethereumAddress { @@ -115,23 +104,12 @@ final class WalletsListViewModelFactory { excludingChainIds: Set(chainAccountIds) ) - totalValue += crowdloanContributions[ethereumAddress]? - .filter { !chainAccountIds.contains($0.key) } - .reduce(Decimal(0)) { result, contribution in - guard let asset = chains[contribution.key]?.utilityAsset(), - let priceData = prices[ChainAssetId(chainId: contribution.key, assetId: asset.assetId)], - let price = Decimal(string: priceData.price) else { - return result - } - guard let decimalAmount = Decimal.fromSubstrateAmount( - contribution.value, - precision: Int16(bitPattern: asset.precision) - ) else { - return result - } - - return result + decimalAmount * price - } ?? 0 + let contributions = crowdloanContributions[ethereumAddress]?.filter { !chainAccountIds.contains($0.key) } ?? [:] + totalValue += calculateCrowdloanContribution( + contributions, + chains: chains, + prices: prices + ) } wallet.info.chainAccounts.forEach { chainAccount in @@ -142,22 +120,12 @@ final class WalletsListViewModelFactory { includingChainIds: [chainAccount.chainId], excludingChainIds: Set() ) - totalValue += crowdloanContributions[chainAccount.accountId]? - .reduce(Decimal(0)) { result, contribution in - guard let asset = chains[contribution.key]?.utilityAsset(), - let priceData = prices[ChainAssetId(chainId: contribution.key, assetId: asset.assetId)], - let price = Decimal(string: priceData.price) else { - return result - } - guard let decimalAmount = Decimal.fromSubstrateAmount( - contribution.value, - precision: Int16(bitPattern: asset.precision) - ) else { - return result - } - - return result + decimalAmount * price - } ?? 0 + let contributions = crowdloanContributions[chainAccount.accountId] ?? [:] + totalValue += calculateCrowdloanContribution( + contributions, + chains: chains, + prices: prices + ) } return totalValue @@ -193,6 +161,28 @@ final class WalletsListViewModelFactory { return nil } } + + private func calculateCrowdloanContribution( + _ contributions: [ChainModel.Id: BigUInt], + chains: [ChainModel.Id: ChainModel], + prices: [ChainAssetId: PriceData] + ) -> Decimal { + contributions.reduce(0) { result, contribution in + guard let asset = chains[contribution.key]?.utilityAsset(), + let priceData = prices[ChainAssetId(chainId: contribution.key, assetId: asset.assetId)], + let price = Decimal(string: priceData.price) else { + return result + } + guard let decimalAmount = Decimal.fromSubstrateAmount( + contribution.value, + precision: Int16(bitPattern: asset.precision) + ) else { + return result + } + + return result + decimalAmount * price + } + } } extension WalletsListViewModelFactory: WalletsListViewModelFactoryProtocol { From d7e542148f78788fed7407c89a612c599b9207a6 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Mon, 26 Sep 2022 20:08:19 +0300 Subject: [PATCH 43/52] buildfix --- novawallet.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index bfca5455cf..5bf7e67198 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -2166,6 +2166,7 @@ 88C7165628C8CD050015D1E9 /* UICollectionViewDiffableDataSource+apply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C7165528C8CD050015D1E9 /* UICollectionViewDiffableDataSource+apply.swift */; }; 88C7165828C8D3280015D1E9 /* LockCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C7165728C8D3270015D1E9 /* LockCollectionViewCell.swift */; }; 88C7165A28C8D3450015D1E9 /* LocksHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C7165928C8D3450015D1E9 /* LocksHeaderView.swift */; }; + 88CD321028E2137300542F0D /* CrowdloanContributionId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CD320F28E2137200542F0D /* CrowdloanContributionId.swift */; }; 88D997AE28AB86FE006135A5 /* YourContributionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D997AD28AB86FD006135A5 /* YourContributionsView.swift */; }; 88D997B028ABC8C0006135A5 /* BlurredTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D997AF28ABC8C0006135A5 /* BlurredTableViewCell.swift */; }; 88D997B228ABC90E006135A5 /* AboutCrowdloansView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D997B128ABC90E006135A5 /* AboutCrowdloansView.swift */; }; @@ -4927,6 +4928,7 @@ 88C7165528C8CD050015D1E9 /* UICollectionViewDiffableDataSource+apply.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionViewDiffableDataSource+apply.swift"; sourceTree = ""; }; 88C7165728C8D3270015D1E9 /* LockCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockCollectionViewCell.swift; sourceTree = ""; }; 88C7165928C8D3450015D1E9 /* LocksHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocksHeaderView.swift; sourceTree = ""; }; + 88CD320F28E2137200542F0D /* CrowdloanContributionId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionId.swift; sourceTree = ""; }; 88D997AD28AB86FD006135A5 /* YourContributionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YourContributionsView.swift; sourceTree = ""; }; 88D997AF28ABC8C0006135A5 /* BlurredTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurredTableViewCell.swift; sourceTree = ""; }; 88D997B128ABC90E006135A5 /* AboutCrowdloansView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutCrowdloansView.swift; sourceTree = ""; }; @@ -6836,6 +6838,7 @@ 842EBB2C289096D300B952D8 /* Common */ = { isa = PBXGroup; children = ( + 88CD320F28E2137200542F0D /* CrowdloanContributionId.swift */, A8FC21B4670E7B22B787357D /* WalletsListProtocols.swift */, 6F404EE82BC45BFE0F42E0A4 /* WalletsListWireframe.swift */, 0D6E67AD564867E121601F18 /* WalletsListPresenter.swift */, @@ -14683,6 +14686,7 @@ 84720730277C335000F593DD /* DAppListFlowLayout.swift in Sources */, AE2C84DF25EF98BA00986716 /* AnyValidatorInfoInteractor.swift in Sources */, 8490142E24A935FE008F705E /* LoadableViewProtocol.swift in Sources */, + 88CD321028E2137300542F0D /* CrowdloanContributionId.swift in Sources */, 84F2FF0725E7AF8F008338D5 /* EraValidatorInfo.swift in Sources */, 84E25BF627E9A51D00290BF1 /* LeaseParam.swift in Sources */, 843939902636F88F0087658D /* YourValidatorsModel.swift in Sources */, From d59d974018505bb6fa5c2c65d2873a5b2d610b31 Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Mon, 26 Sep 2022 20:19:29 +0300 Subject: [PATCH 44/52] fix for crowdloan update --- .../Modules/WalletsList/Common/WalletsListPresenter.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novawallet/Modules/WalletsList/Common/WalletsListPresenter.swift b/novawallet/Modules/WalletsList/Common/WalletsListPresenter.swift index 75c7226b82..f7719b8282 100644 --- a/novawallet/Modules/WalletsList/Common/WalletsListPresenter.swift +++ b/novawallet/Modules/WalletsList/Common/WalletsListPresenter.swift @@ -134,9 +134,10 @@ extension WalletsListPresenter: WalletsListInteractorOutputProtocol { for change in changes { switch change { case let .insert(item), let .update(item): + let previousAmount = crowdloanContributionsMapping[item.identifier]?.amount ?? 0 var accountCrowdloan = crowdloanContributions[item.accountId] ?? [:] let value: BigUInt = accountCrowdloan[item.chainId] ?? 0 - accountCrowdloan[item.chainId] = value + item.amount + accountCrowdloan[item.chainId] = value - previousAmount + item.amount crowdloanContributions[item.accountId] = accountCrowdloan crowdloanContributionsMapping[item.identifier] = CrowdloanContributionId( chainId: item.chainId, From 877bf1dd6d834eb0362b08db6f9ce3e95a31d667 Mon Sep 17 00:00:00 2001 From: ERussel Date: Mon, 26 Sep 2022 23:06:14 +0500 Subject: [PATCH 45/52] fix locks loading --- .../Modules/AssetList/AssetListInteractor.swift | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/novawallet/Modules/AssetList/AssetListInteractor.swift b/novawallet/Modules/AssetList/AssetListInteractor.swift index ca267d827c..1ba5ae43c4 100644 --- a/novawallet/Modules/AssetList/AssetListInteractor.swift +++ b/novawallet/Modules/AssetList/AssetListInteractor.swift @@ -215,23 +215,8 @@ extension AssetListInteractor { _ changes: [DataProviderChange], accountId: AccountId ) { - let initialItems = assetBalanceIdMapping.values.reduce( - into: [ChainAssetId: [AssetLock]]() - ) { accum, assetBalanceId in - guard assetBalanceId.accountId == accountId else { - return - } - - let chainAssetId = ChainAssetId( - chainId: assetBalanceId.chainId, - assetId: assetBalanceId.assetId - ) - - accum[chainAssetId] = locks[chainAssetId] - } - locks = changes.reduce( - into: initialItems + into: locks ) { accum, change in switch change { case let .insert(lock), let .update(lock): From 8cf2321a0c46f363f1ca496b5952f29937708683 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 27 Sep 2022 09:38:26 +0500 Subject: [PATCH 46/52] fix locks in total header --- novawallet/Common/Model/AssetBalance.swift | 1 + novawallet/Modules/AssetList/AssetListPresenter.swift | 2 +- .../Modules/Locks/LocksBalanceViewModelFactory.swift | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/novawallet/Common/Model/AssetBalance.swift b/novawallet/Common/Model/AssetBalance.swift index eb87b6325e..6e3171623f 100644 --- a/novawallet/Common/Model/AssetBalance.swift +++ b/novawallet/Common/Model/AssetBalance.swift @@ -11,6 +11,7 @@ struct AssetBalance: Equatable { var totalInPlank: BigUInt { freeInPlank + reservedInPlank } var transferable: BigUInt { freeInPlank > frozenInPlank ? freeInPlank - frozenInPlank : 0 } + var locked: BigUInt { frozenInPlank + reservedInPlank } } extension AssetBalance: Identifiable { diff --git a/novawallet/Modules/AssetList/AssetListPresenter.swift b/novawallet/Modules/AssetList/AssetListPresenter.swift index 0206e19274..37c3da53e3 100644 --- a/novawallet/Modules/AssetList/AssetListPresenter.swift +++ b/novawallet/Modules/AssetList/AssetListPresenter.swift @@ -121,7 +121,7 @@ final class AssetListPresenter: AssetListBasePresenter { return AssetListAssetAccountPrice( assetInfo: asset.displayInfo, - balance: assetBalance.frozenInPlank, + balance: assetBalance.locked, price: priceData ) } diff --git a/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift index 0d91feebd8..4968fd0b1f 100644 --- a/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift +++ b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift @@ -62,14 +62,14 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { var lastPriceData: PriceData? for balance in balances { - guard let priceData = prices[balance.chainAssetId] else { - continue - } + let priceData = prices[balance.chainAssetId] ?? .zero() + guard let assetPrecision = chains[balance.chainAssetId.chainId]? .asset(for: balance.chainAssetId.assetId)? .precision else { continue } + let rate = Decimal(string: priceData.price) ?? 0.0 totalPrice += calculateAmount( @@ -83,7 +83,7 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { rate: rate ) locksPrice += calculateAmount( - from: balance.frozenInPlank + balance.reservedInPlank, + from: balance.locked, precision: assetPrecision, rate: rate ) From c311cf011c0d272c793371625cde520e9c1baddd Mon Sep 17 00:00:00 2001 From: Holyberry <666lynx666@mail.ru> Date: Tue, 27 Sep 2022 11:56:08 +0300 Subject: [PATCH 47/52] fix --- .../Modules/Locks/LocksBalanceViewModelFactory.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift index 4968fd0b1f..a09c056df0 100644 --- a/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift +++ b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift @@ -104,14 +104,16 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { ) } + let total = totalPrice + crowdloansTotalPrice let formattedTotal = formatPrice( - amount: totalPrice + crowdloansTotalPrice, + amount: total, priceData: lastPriceData, locale: locale ) let formattedTransferrable = formatPrice(amount: transferrablePrice, priceData: lastPriceData, locale: locale) + let totalLocks = locksPrice + crowdloansTotalPrice let formattedLocks = formatPrice( - amount: locksPrice + crowdloansTotalPrice, + amount: totalLocks, priceData: lastPriceData, locale: locale ) @@ -119,9 +121,9 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { total: formattedTotal, transferrable: formattedTransferrable, locks: formattedLocks, - totalPrice: totalPrice, + totalPrice: total, transferrablePrice: transferrablePrice, - locksPrice: locksPrice + locksPrice: totalLocks ) } From 7f84491713b4a3f09e2298d3ba53079748cb23b1 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 27 Sep 2022 21:04:31 +0500 Subject: [PATCH 48/52] fix assets total calculation --- .../AssetList/AssetListPresenter.swift | 9 +-- .../Base/AssetListBasePresenter+Model.swift | 69 ++++++++++++++----- .../Base/AssetListBasePresenter.swift | 53 ++++++-------- .../Models/AssetListAssetModel.swift | 23 ++++++- .../AssetsSearch/AssetsSearchPresenter.swift | 9 +-- 5 files changed, 100 insertions(+), 63 deletions(-) diff --git a/novawallet/Modules/AssetList/AssetListPresenter.swift b/novawallet/Modules/AssetList/AssetListPresenter.swift index 37c3da53e3..0b6e3a17ee 100644 --- a/novawallet/Modules/AssetList/AssetListPresenter.swift +++ b/novawallet/Modules/AssetList/AssetListPresenter.swift @@ -258,7 +258,7 @@ final class AssetListPresenter: AssetListBasePresenter { private func createGroupViewModel( from groupModel: AssetListGroupModel, maybePrices: [ChainAssetId: PriceData]?, - maybeCrowdloans: [ChainModel.Id: [CrowdloanContributionData]]?, + maybeCrowdloans _: [ChainModel.Id: [CrowdloanContributionData]]?, hidesZeroBalances: Bool ) -> AssetListGroupViewModel? { let chain = groupModel.chain @@ -292,12 +292,7 @@ final class AssetListPresenter: AssetListBasePresenter { } let assetInfoList: [AssetListAssetAccountInfo] = filteredAssets.map { asset in - createAssetAccountInfo( - from: asset, - chain: chain, - maybePrices: maybePrices, - maybeCrowdloans: maybeCrowdloans - ) + createAssetAccountInfo(from: asset, chain: chain, maybePrices: maybePrices) } return viewModelFactory.createGroupViewModel( diff --git a/novawallet/Modules/AssetList/Base/AssetListBasePresenter+Model.swift b/novawallet/Modules/AssetList/Base/AssetListBasePresenter+Model.swift index a3951e50f1..c2a6111841 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBasePresenter+Model.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBasePresenter+Model.swift @@ -1,5 +1,6 @@ import Foundation import RobinHood +import BigInt extension AssetListBasePresenter { func createGroupModel( @@ -7,7 +8,7 @@ extension AssetListBasePresenter { assets: [AssetListAssetModel] ) -> AssetListGroupModel { let value: Decimal = assets.reduce(0) { result, asset in - result + (asset.assetValue ?? 0) + result + (asset.totalValue ?? 0) } return AssetListGroupModel(chain: chain, chainValue: value) @@ -40,8 +41,8 @@ extension AssetListBasePresenter { let balance1 = (try? model1.balanceResult?.get()) ?? 0 let balance2 = (try? model2.balanceResult?.get()) ?? 0 - let assetValue1 = model1.assetValue ?? 0 - let assetValue2 = model2.assetValue ?? 0 + let assetValue1 = model1.totalValue ?? 0 + let assetValue2 = model2.totalValue ?? 0 if assetValue1 > 0, assetValue2 > 0 { return assetValue1 > assetValue2 @@ -89,6 +90,31 @@ extension AssetListBasePresenter { } }() + let crowdloanContributionsResult: Result? = { + do { + let allContributions = try crowdloansResult?.get() + + let contribution = allContributions?[chainModel.chainId]?.reduce(BigUInt(0)) { accum, contribution in + accum + contribution.amount + } + + return contribution.map { .success($0) } + } catch { + return .failure(error) + } + }() + + let maybeCrowdloanContributions: Decimal? = { + if let contributions = try? crowdloanContributionsResult?.get() { + return Decimal.fromSubstrateAmount( + contributions, + precision: Int16(bitPattern: assetModel.precision) + ) + } else { + return nil + } + }() + let maybePrice: Decimal? = { if let mapping = try? priceResult?.get(), let priceData = mapping[chainAssetId] { return Decimal(string: priceData.price) @@ -97,19 +123,28 @@ extension AssetListBasePresenter { } }() - if let balance = maybeBalance, let price = maybePrice { - let assetValue = balance * price - return AssetListAssetModel( - assetModel: assetModel, - balanceResult: balanceResult, - assetValue: assetValue - ) - } else { - return AssetListAssetModel( - assetModel: assetModel, - balanceResult: balanceResult, - assetValue: nil - ) - } + let balanceValue: Decimal? = { + if let balance = maybeBalance, let price = maybePrice { + return balance * price + } else { + return nil + } + }() + + let crowdloanContributionsValue: Decimal? = { + if let crowdloanContributions = maybeCrowdloanContributions, let price = maybePrice { + return crowdloanContributions * price + } else { + return nil + } + }() + + return AssetListAssetModel( + assetModel: assetModel, + balanceResult: balanceResult, + balanceValue: balanceValue, + crowdloanResult: crowdloanContributionsResult, + crowdloanValue: crowdloanContributionsValue + ) } } diff --git a/novawallet/Modules/AssetList/Base/AssetListBasePresenter.swift b/novawallet/Modules/AssetList/Base/AssetListBasePresenter.swift index 8adc8f3e76..0c19c683d5 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBasePresenter.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBasePresenter.swift @@ -16,6 +16,23 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { groups = Self.createGroupsDiffCalculator(from: []) } + private func updateAssetModels() { + for chain in allChains.values { + let models = chain.assets.map { asset in + createAssetModel(for: chain, assetModel: asset) + } + + let changes: [DataProviderChange] = models.map { model in + .update(newItem: model) + } + + groupLists[chain.chainId]?.apply(changes: changes) + + let groupModel = createGroupModel(from: chain, assets: models) + groups.apply(changes: [.update(newItem: groupModel)]) + } + } + func resetStorages() { allChains = [:] balanceResults = [:] @@ -54,8 +71,7 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { func createAssetAccountInfo( from asset: AssetListAssetModel, chain: ChainModel, - maybePrices: [ChainAssetId: PriceData]?, - maybeCrowdloans: [ChainModel.Id: [CrowdloanContributionData]]? + maybePrices: [ChainAssetId: PriceData]? ) -> AssetListAssetAccountInfo { let assetModel = asset.assetModel let chainAssetId = ChainAssetId(chainId: chain.chainId, assetId: assetModel.assetId) @@ -70,24 +86,10 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { priceData = nil } - let maybeBalance = try? asset.balanceResult?.get() - - let maybeContributions = maybeCrowdloans?[chain.chainId]?.reduce(BigUInt(0)) { result, contribution in - result + contribution.amount - } - - let totalBalance: BigUInt? - - if let balance = maybeBalance, let contribution = maybeContributions { - totalBalance = balance + contribution - } else { - totalBalance = maybeBalance ?? maybeContributions - } - return AssetListAssetAccountInfo( assetId: asset.assetModel.assetId, assetInfo: assetInfo, - balance: totalBalance, + balance: asset.totalAmount, priceData: priceData ) } @@ -99,20 +101,7 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { priceResult = result - for chain in allChains.values { - let models = chain.assets.map { asset in - createAssetModel(for: chain, assetModel: asset) - } - - let changes: [DataProviderChange] = models.map { model in - .update(newItem: model) - } - - groupLists[chain.chainId]?.apply(changes: changes) - - let groupModel = createGroupModel(from: chain, assets: models) - groups.apply(changes: [.update(newItem: groupModel)]) - } + updateAssetModels() } func didReceiveChainModelChanges(_ changes: [DataProviderChange]) { @@ -202,5 +191,7 @@ class AssetListBasePresenter: AssetListBaseInteractorOutputProtocol { func didReceiveCrowdloans(result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) { crowdloansResult = result + + updateAssetModels() } } diff --git a/novawallet/Modules/AssetList/Models/AssetListAssetModel.swift b/novawallet/Modules/AssetList/Models/AssetListAssetModel.swift index 380e6ab412..d221549e2f 100644 --- a/novawallet/Modules/AssetList/Models/AssetListAssetModel.swift +++ b/novawallet/Modules/AssetList/Models/AssetListAssetModel.swift @@ -7,5 +7,26 @@ struct AssetListAssetModel: Identifiable { let assetModel: AssetModel let balanceResult: Result? - let assetValue: Decimal? + let balanceValue: Decimal? + + let crowdloanResult: Result? + let crowdloanValue: Decimal? + + var totalAmount: BigUInt? { + let maybeBalanceAmount = try? balanceResult?.get() + let maybeCrowdloanContribution = try? crowdloanResult?.get() + if let balanceAmount = maybeBalanceAmount, let crowdloanAmount = maybeCrowdloanContribution { + return balanceAmount + crowdloanAmount + } else { + return maybeBalanceAmount ?? maybeCrowdloanContribution + } + } + + var totalValue: Decimal? { + if let balanceValue = balanceValue, let crowdloanValue = crowdloanValue { + return balanceValue + crowdloanValue + } else { + return balanceValue ?? crowdloanValue + } + } } diff --git a/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift b/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift index 6f972136cc..986774231a 100644 --- a/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift +++ b/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift @@ -130,19 +130,14 @@ final class AssetsSearchPresenter: AssetListBasePresenter { private func createGroupViewModel( from groupModel: AssetListGroupModel, maybePrices: [ChainAssetId: PriceData]?, - maybeCrowdloans: [ChainModel.Id: [CrowdloanContributionData]]? + maybeCrowdloans _: [ChainModel.Id: [CrowdloanContributionData]]? ) -> AssetListGroupViewModel? { let chain = groupModel.chain let assets = groupLists[chain.chainId]?.allItems ?? [] let assetInfoList: [AssetListAssetAccountInfo] = assets.map { asset in - createAssetAccountInfo( - from: asset, - chain: chain, - maybePrices: maybePrices, - maybeCrowdloans: maybeCrowdloans - ) + createAssetAccountInfo(from: asset, chain: chain, maybePrices: maybePrices) } return viewModelFactory.createGroupViewModel( From c122e03c2ee7d7c4763a3c182dd01b8acfcd09f3 Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 27 Sep 2022 22:16:12 +0500 Subject: [PATCH 49/52] fix tokens formattings --- .../Modules/Locks/LocksBalanceViewModelFactory.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift index 4968fd0b1f..56ef0f8337 100644 --- a/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift +++ b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift @@ -132,11 +132,12 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { prices: [ChainAssetId: PriceData], locale: Locale ) -> FormattedPlank? { - guard let assetPrecision = chains[chainAssetId.chainId]?.asset(for: chainAssetId.assetId)?.precision, - let utilityAsset = chains[chainAssetId.chainId]?.utilityAsset() else { + guard let chain = chains[chainAssetId.chainId], let asset = chain.asset(for: chainAssetId.assetId) else { return nil } + let assetPrecision = asset.precision + let priceData = prices[chainAssetId] let rate = priceData.map { Decimal(string: $0.price) ?? 0 } ?? 0 @@ -149,12 +150,12 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { let amount = calculateAmount( from: plank, - precision: utilityAsset.precision, + precision: assetPrecision, rate: nil ) let formattedAmount = formatAmount( amount, - assetDisplayInfo: utilityAsset.displayInfo, + assetDisplayInfo: ChainAsset(chain: chain, asset: asset).assetDisplayInfo, locale: locale ) From f7b66ee5c92885f337a5d4d3bbf5ebc7bab7db9b Mon Sep 17 00:00:00 2001 From: ERussel Date: Tue, 27 Sep 2022 23:27:13 +0500 Subject: [PATCH 50/52] hide total locks when no locks --- novawallet/Common/View/IconDetailsView.swift | 10 ++ .../AssetList/AssetListPresenter.swift | 98 +++++++++++++------ .../View/AssetListTotalBalanceCell.swift | 29 ++++-- 3 files changed, 101 insertions(+), 36 deletions(-) diff --git a/novawallet/Common/View/IconDetailsView.swift b/novawallet/Common/View/IconDetailsView.swift index 011ac69d51..7ff1e3a6c5 100644 --- a/novawallet/Common/View/IconDetailsView.swift +++ b/novawallet/Common/View/IconDetailsView.swift @@ -21,6 +21,16 @@ class IconDetailsView: UIView { return label }() + var hidesIcon: Bool { + get { + imageView.isHidden + } + + set { + imageView.isHidden = newValue + } + } + var mode: Mode = .iconDetails { didSet { applyLayout() diff --git a/novawallet/Modules/AssetList/AssetListPresenter.swift b/novawallet/Modules/AssetList/AssetListPresenter.swift index 0b6e3a17ee..e7a1caafa6 100644 --- a/novawallet/Modules/AssetList/AssetListPresenter.swift +++ b/novawallet/Modules/AssetList/AssetListPresenter.swift @@ -115,7 +115,7 @@ final class AssetListPresenter: AssetListBasePresenter { return nil } - guard case let .success(assetBalance) = balances[chainAssetId] else { + guard case let .success(assetBalance) = balances[chainAssetId], assetBalance.locked > 0 else { return nil } @@ -132,7 +132,26 @@ final class AssetListPresenter: AssetListBasePresenter { walletType: MetaAccountModelType, name: String ) { - var locks: [AssetListAssetAccountPrice] = [] + let crowdloans = crowdloansModel(prices: priceMapping) + let totalValue = createHeaderPriceState(from: priceMapping, crowdloans: crowdloans) + let totalLocks = createHeaderLockState(from: priceMapping, crowdloans: crowdloans) + + let viewModel = viewModelFactory.createHeaderViewModel( + from: name, + walletIdenticon: walletIdenticon, + walletType: walletType, + prices: totalValue, + locks: totalLocks, + locale: selectedLocale + ) + + view?.didReceiveHeader(viewModel: viewModel) + } + + private func createHeaderPriceState( + from priceMapping: [ChainAssetId: PriceData], + crowdloans: [AssetListAssetAccountPrice] + ) -> LoadableViewModelState<[AssetListAssetAccountPrice]> { var priceState: LoadableViewModelState<[AssetListAssetAccountPrice]> = .loaded(value: []) for (chainAssetId, priceData) in priceMapping { @@ -148,12 +167,6 @@ final class AssetListPresenter: AssetListBasePresenter { continue } priceState = .cached(value: items + [newItem.value]) - createAssetAccountPriceLock( - chainAssetId: chainAssetId, - priceData: priceData - ).map { - locks.append($0) - } case let .loaded(items): guard let newItem = createAssetAccountPrice( chainAssetId: chainAssetId, @@ -169,27 +182,43 @@ final class AssetListPresenter: AssetListBasePresenter { case let .right(item): priceState = .cached(value: items + [item]) } - createAssetAccountPriceLock( - chainAssetId: chainAssetId, - priceData: priceData - ).map { - locks.append($0) - } } } - let crowdloans = crowdloansModel(prices: priceMapping) - let totalLocks = locks + crowdloans - let viewModel = viewModelFactory.createHeaderViewModel( - from: name, - walletIdenticon: walletIdenticon, - walletType: walletType, - prices: priceState + crowdloans, - locks: totalLocks.isEmpty ? nil : totalLocks, - locale: selectedLocale - ) + return priceState + crowdloans + } - view?.didReceiveHeader(viewModel: viewModel) + private func createHeaderLockState( + from priceMapping: [ChainAssetId: PriceData], + crowdloans: [AssetListAssetAccountPrice] + ) -> [AssetListAssetAccountPrice]? { + guard checkNonZeroLocks() else { + return nil + } + + let locks: [AssetListAssetAccountPrice] = priceMapping.reduce(into: []) { accum, keyValue in + if let lock = createAssetAccountPriceLock(chainAssetId: keyValue.key, priceData: keyValue.value) { + accum.append(lock) + } + } + + return locks + crowdloans + } + + private func checkNonZeroLocks() -> Bool { + let locks = balances.map { (try? $0.value.get())?.locked ?? 0 } + + if locks.contains(where: { $0 > 0 }) { + return true + } + + let crowdloanContributions = (try? crowdloansResult?.get()) ?? [:] + + if crowdloanContributions.contains(where: { $0.value.contains(where: { $0.amount > 0 }) }) { + return true + } + + return false } private func calculateNftBalance(for chainAsset: ChainAsset) -> BigUInt { @@ -246,9 +275,15 @@ final class AssetListPresenter: AssetListBasePresenter { let chainAssetId = ChainAssetId(chainId: chainId, assetId: asset.assetId) let price = prices[chainAssetId] ?? .zero() + let contributedAmount = chainCrowdloans.reduce(0) { $0 + $1.amount } + + guard contributedAmount > 0 else { + return nil + } + return AssetListAssetAccountPrice( assetInfo: asset.displayInfo, - balance: chainCrowdloans.reduce(0) { $0 + $1.amount }, + balance: contributedAmount, price: price ) } @@ -425,12 +460,15 @@ extension AssetListPresenter: AssetListPresenterProtocol { } func didTapTotalBalance() { - guard let priceResult = priceResult, - let prices = try? priceResult.get(), - let locks = try? locksResult?.get(), - let crowdloans = try? crowdloansResult?.get() else { + guard + checkNonZeroLocks(), + let priceResult = priceResult, + let prices = try? priceResult.get(), + let locks = try? locksResult?.get(), + let crowdloans = try? crowdloansResult?.get() else { return } + wireframe.showBalanceBreakdown( from: view, prices: prices, diff --git a/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift b/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift index 0e24f4fd89..fc5f585ddd 100644 --- a/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift +++ b/novawallet/Modules/AssetList/View/AssetListTotalBalanceCell.swift @@ -20,9 +20,8 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { view.detailsLabel.textColor = R.color.colorTransparentText() view.detailsLabel.font = .regularSubheadline - view.imageView.image = R.image.iconInfoFilled()? - .withRenderingMode(.alwaysTemplate) - .tinted(with: R.color.colorWhite48()!) + view.imageView.image = R.image.iconInfoFilled()?.tinted(with: R.color.colorWhite48()!) + view.iconWidth = 16.0 view.spacing = 4.0 @@ -81,16 +80,34 @@ final class AssetListTotalBalanceCell: UICollectionViewCell { switch viewModel.amount { case let .loaded(value), let .cached(value): amountLabel.text = value - locksView.iconDetailsView.detailsLabel.text = viewModel.locksAmount - locksView.isHidden = viewModel.locksAmount == nil + + if let lockedAmount = viewModel.locksAmount { + setupStateWithLocks(amount: lockedAmount) + } else { + setupStateWithoutLocks() + } + stopLoadingIfNeeded() case .loading: amountLabel.text = "" - locksView.isHidden = true + setupStateWithoutLocks() startLoadingIfNeeded() } } + private func setupStateWithLocks(amount: String) { + locksView.isHidden = false + titleView.hidesIcon = false + + locksView.iconDetailsView.detailsLabel.text = amount + } + + private func setupStateWithoutLocks() { + locksView.iconDetailsView.detailsLabel.text = nil + locksView.isHidden = true + titleView.hidesIcon = true + } + private func setupLocalization() { titleView.detailsLabel.text = R.string.localizable.walletTotalBalance( preferredLanguages: locale.rLanguages From cdf0d087e6dc4ecb6cf3f75c65eef2d954db6c8a Mon Sep 17 00:00:00 2001 From: ERussel Date: Wed, 28 Sep 2022 00:30:17 +0500 Subject: [PATCH 51/52] refactoring --- novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift b/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift index 986774231a..931cc1edec 100644 --- a/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift +++ b/novawallet/Modules/AssetsSearch/AssetsSearchPresenter.swift @@ -114,10 +114,9 @@ final class AssetsSearchPresenter: AssetListBasePresenter { private func provideAssetsViewModel() { let maybePrices = try? priceResult?.get() - let maybeCrowdloans = try? crowdloansResult?.get() let viewModels: [AssetListGroupViewModel] = groups.allItems.compactMap { groupModel in - createGroupViewModel(from: groupModel, maybePrices: maybePrices, maybeCrowdloans: maybeCrowdloans) + createGroupViewModel(from: groupModel, maybePrices: maybePrices) } if viewModels.isEmpty, !balanceResults.isEmpty, balanceResults.count >= allChains.count { @@ -129,8 +128,7 @@ final class AssetsSearchPresenter: AssetListBasePresenter { private func createGroupViewModel( from groupModel: AssetListGroupModel, - maybePrices: [ChainAssetId: PriceData]?, - maybeCrowdloans _: [ChainModel.Id: [CrowdloanContributionData]]? + maybePrices: [ChainAssetId: PriceData]? ) -> AssetListGroupViewModel? { let chain = groupModel.chain From 383e765be8a1aa1d9382636052cce6c8bca14358 Mon Sep 17 00:00:00 2001 From: Gulnaz <666lynx666@mail.ru> Date: Thu, 29 Sep 2022 13:09:04 +0300 Subject: [PATCH 52/52] Update empty & error screens to have background behind empty & error states logos (#421) * added background * fixes * fix * added space * fix spacing * remove error and empty states * remove blurred background view * remove mock data * fix font --- novawallet.xcodeproj/project.pbxproj | 4 + .../View/ErrorView/ErrorStateView.swift | 9 +- .../CrowdloanListPresenter.swift | 28 +++---- .../CrowdloanListViewController.swift | 83 +++++++------------ .../CrowdloanListViewLayout.swift | 9 -- .../View/CrowdloanEmptyView.swift | 52 ++++++++++++ .../View/CrowdloanStatusSectionView.swift | 52 +++--------- .../ViewModel/CrowdloansViewModel.swift | 4 +- .../CrowdloansViewModelFactory.swift | 36 +++++++- .../Crowdloan/View/BlurredTableViewCell.swift | 29 ++++++- novawallet/en.lproj/Localizable.strings | 2 + novawallet/ru.lproj/Localizable.strings | 2 + 12 files changed, 183 insertions(+), 127 deletions(-) create mode 100644 novawallet/Modules/Crowdloan/CrowdloanList/View/CrowdloanEmptyView.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 5bf7e67198..cf3af5fd24 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -2172,6 +2172,7 @@ 88D997B228ABC90E006135A5 /* AboutCrowdloansView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D997B128ABC90E006135A5 /* AboutCrowdloansView.swift */; }; 88E1E896289C021F00C123A8 /* CurrencyCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E1E895289C021F00C123A8 /* CurrencyCollectionViewCell.swift */; }; 88E1E898289C024400C123A8 /* UIView+Create.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E1E897289C024400C123A8 /* UIView+Create.swift */; }; + 88E8CF5E28E3789600C90112 /* CrowdloanEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E8CF5D28E3789600C90112 /* CrowdloanEmptyView.swift */; }; 88F19DDE28D8D0A100F6E459 /* Either.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F19DDD28D8D0A100F6E459 /* Either.swift */; }; 88F19DE028D8D0F600F6E459 /* LoadableViewModelState+Addition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F19DDF28D8D0F600F6E459 /* LoadableViewModelState+Addition.swift */; }; 88F3A9FB9CEA464275F1115E /* ExportMnemonicViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47759907380BE9300E54DC78 /* ExportMnemonicViewFactory.swift */; }; @@ -4934,6 +4935,7 @@ 88D997B128ABC90E006135A5 /* AboutCrowdloansView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutCrowdloansView.swift; sourceTree = ""; }; 88E1E895289C021F00C123A8 /* CurrencyCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyCollectionViewCell.swift; sourceTree = ""; }; 88E1E897289C024400C123A8 /* UIView+Create.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Create.swift"; sourceTree = ""; }; + 88E8CF5D28E3789600C90112 /* CrowdloanEmptyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrowdloanEmptyView.swift; sourceTree = ""; }; 88F19DDD28D8D0A100F6E459 /* Either.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Either.swift; sourceTree = ""; }; 88F19DDF28D8D0F600F6E459 /* LoadableViewModelState+Addition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoadableViewModelState+Addition.swift"; sourceTree = ""; }; 88F7715F28BEA589008C028A /* YourWalletsIconDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourWalletsIconDetailsView.swift; sourceTree = ""; }; @@ -9384,6 +9386,7 @@ 849842EA26587AFA006BBB9F /* View */ = { isa = PBXGroup; children = ( + 88E8CF5D28E3789600C90112 /* CrowdloanEmptyView.swift */, F4F69E272731B0B200214542 /* CrowdloanTableHeaderView.swift */, 84BB3CF7267D276D00676FFE /* CrowdloanTableViewCell.swift */, 8498430226592D29006BBB9F /* CrowdloanStatusSectionView.swift */, @@ -14139,6 +14142,7 @@ 843E9B2F27C8B17F009C143A /* FileDownloadOperation.swift in Sources */, 8490152124ABC721008F705E /* WalletStaticImageViewModel.swift in Sources */, F408E9BE26B80FD30043CFE0 /* AnalyticsSectionHeader.swift in Sources */, + 88E8CF5E28E3789600C90112 /* CrowdloanEmptyView.swift in Sources */, 84C6801824D7053B00006BF5 /* BorderedSubtitleActionView.swift in Sources */, 84468A0B286663E500BCBE00 /* CrossChainTransferSetupPresenter.swift in Sources */, 84A2C90C24E192F50020D3B7 /* ShakeAnimator.swift in Sources */, diff --git a/novawallet/Common/View/ErrorView/ErrorStateView.swift b/novawallet/Common/View/ErrorView/ErrorStateView.swift index 9f182e376b..8fa3764cee 100644 --- a/novawallet/Common/View/ErrorView/ErrorStateView.swift +++ b/novawallet/Common/View/ErrorView/ErrorStateView.swift @@ -25,6 +25,8 @@ class ErrorStateView: UIView { return button }() + lazy var stackView = UIStackView(arrangedSubviews: [iconImageView, errorDescriptionLabel, retryButton]) + var locale = Locale.current { didSet { if locale != oldValue { @@ -47,13 +49,16 @@ class ErrorStateView: UIView { } private func setupLayout() { - let stackView = UIStackView(arrangedSubviews: [iconImageView, errorDescriptionLabel, retryButton]) stackView.axis = .vertical stackView.spacing = 16 stackView.alignment = .center addSubview(stackView) - stackView.snp.makeConstraints { $0.center.equalToSuperview() } + stackView.snp.makeConstraints { + $0.center.equalToSuperview() + $0.leading.top.greaterThanOrEqualToSuperview() + $0.trailing.bottom.lessThanOrEqualToSuperview() + } } private func applyLocalization() { diff --git a/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListPresenter.swift b/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListPresenter.swift index 259af92d16..9cce7911d1 100644 --- a/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListPresenter.swift +++ b/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListPresenter.swift @@ -46,12 +46,6 @@ final class CrowdloanListPresenter { self.localizationManager = localizationManager } - private func provideViewErrorState() { - let message = R.string.localizable - .commonErrorNoDataRetrieved(preferredLanguages: selectedLocale.rLanguages) - view?.didReceive(listState: .error(message: message)) - } - private func updateWalletSwitchView() { guard let wallet = wallet else { return @@ -73,7 +67,7 @@ final class CrowdloanListPresenter { guard case let .success(chain) = chainResult, let asset = chain.utilityAssets().first else { - provideViewErrorState() + provideViewError(chainAsset: nil) return } @@ -156,7 +150,7 @@ final class CrowdloanListPresenter { } guard case let .success(chain) = chainResult, let asset = chain.utilityAssets().first else { - provideViewErrorState() + provideViewError(chainAsset: nil) return } @@ -166,17 +160,11 @@ final class CrowdloanListPresenter { return } + let chainAsset = ChainAssetDisplayInfo(asset: asset.displayInfo, chain: chain.chainFormat) do { let crowdloans = try crowdloansResult.get() let priceData = try? priceDataResult?.get() ?? nil - - guard !crowdloans.isEmpty else { - view?.didReceive(listState: .empty) - return - } - let viewInfo = try viewInfoResult.get() - let chainAsset = ChainAssetDisplayInfo(asset: asset.displayInfo, chain: chain.chainFormat) let externalContributionsCount = externalContributions?.count ?? 0 let amount: Decimal? @@ -202,7 +190,7 @@ final class CrowdloanListPresenter { view?.didReceive(listState: .loaded(viewModel: viewModel)) } catch { - provideViewErrorState() + provideViewError(chainAsset: chainAsset) } } @@ -221,6 +209,14 @@ final class CrowdloanListPresenter { displayInfo: displayInfo ) } + + private func provideViewError(chainAsset: ChainAssetDisplayInfo?) { + let viewModel = viewModelFactory.createErrorViewModel( + chainAsset: chainAsset, + locale: selectedLocale + ) + view?.didReceive(listState: .loaded(viewModel: viewModel)) + } } extension CrowdloanListPresenter: CrowdloanListPresenterProtocol { diff --git a/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListViewController.swift b/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListViewController.swift index 3bab2a7b6f..92948b3513 100644 --- a/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListViewController.swift +++ b/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListViewController.swift @@ -76,6 +76,8 @@ final class CrowdloanListViewController: UIViewController, ViewHolder { rootView.tableView.registerClassForCell(YourContributionsTableViewCell.self) rootView.tableView.registerClassForCell(AboutCrowdloansTableViewCell.self) rootView.tableView.registerClassForCell(CrowdloanTableViewCell.self) + rootView.tableView.registerClassForCell(BlurredTableViewCell.self) + rootView.tableView.registerClassForCell(BlurredTableViewCell.self) rootView.tableView.registerHeaderFooterView(withClass: CrowdloanStatusSectionView.self) rootView.tableView.dataSource = self rootView.tableView.delegate = self @@ -107,19 +109,12 @@ final class CrowdloanListViewController: UIViewController, ViewHolder { switch state { case .loading: didStartLoading() - rootView.bringSubviewToFront(rootView.tableView) case .loaded: rootView.tableView.refreshControl?.endRefreshing() didStopLoading() - rootView.bringSubviewToFront(rootView.tableView) - case .empty, .error: - rootView.tableView.refreshControl?.endRefreshing() - didStopLoading() - rootView.bringSubviewToFront(rootView.statusView) } rootView.tableView.reloadData() - reloadEmptyState(animated: false) } @objc func actionRefresh() { @@ -140,7 +135,7 @@ extension CrowdloanListViewController: UITableViewDataSource { switch state { case let .loaded(viewModel): return viewModel.sections.count - case .loading, .empty, .error: + case .loading: return 0 } } @@ -154,10 +149,10 @@ extension CrowdloanListViewController: UITableViewDataSource { return cellViewModels.count case let .completed(_, cellViewModels): return cellViewModels.count - case .yourContributions, .about: + case .yourContributions, .about, .error, .empty: return 1 } - case .loading, .empty, .error: + case .loading: return 0 } } @@ -180,8 +175,25 @@ extension CrowdloanListViewController: UITableViewDataSource { let cell = tableView.dequeueReusableCellWithType(AboutCrowdloansTableViewCell.self)! cell.view.bind(model: model) return cell + case let .error(message): + let cell: BlurredTableViewCell = tableView.dequeueReusableCell(for: indexPath) + cell.view.errorDescriptionLabel.text = message + cell.view.delegate = self + cell.view.locale = selectedLocale + cell.applyStyle() + return cell + case .empty: + let cell: BlurredTableViewCell = tableView.dequeueReusableCell(for: indexPath) + let text = R.string.localizable + .crowdloanEmptyMessage_v3_9_1(preferredLanguages: selectedLocale.rLanguages) + cell.view.bind( + image: R.image.iconEmptyHistory(), + text: text + ) + cell.applyStyle() + return cell } - case .loading, .empty, .error: + case .loading: return UITableViewCell() } } @@ -218,6 +230,10 @@ extension CrowdloanListViewController: UITableViewDelegate { let headerView: CrowdloanStatusSectionView = tableView.dequeueReusableHeaderFooterView() headerView.bind(title: title, count: cells.count) return headerView + case let .empty(title): + let headerView: CrowdloanStatusSectionView = tableView.dequeueReusableHeaderFooterView() + headerView.bind(title: title, count: 0) + return headerView default: return nil } @@ -230,7 +246,7 @@ extension CrowdloanListViewController: UITableViewDelegate { let sectionModel = viewModel.sections[section] switch sectionModel { - case .active, .completed: + case .active, .completed, .empty: return UITableView.automaticDimension default: return 0.0 @@ -265,52 +281,11 @@ extension CrowdloanListViewController: Localizable { } } -extension CrowdloanListViewController: LoadableViewProtocol {} - -extension CrowdloanListViewController: EmptyStateViewOwnerProtocol { - var emptyStateDelegate: EmptyStateDelegate { self } - var emptyStateDataSource: EmptyStateDataSource { self } - var contentViewForEmptyState: UIView { rootView.statusView } -} - -extension CrowdloanListViewController: EmptyStateDataSource { - var viewForEmptyState: UIView? { - switch state { - case let .error(message): - let errorView = ErrorStateView() - errorView.errorDescriptionLabel.text = message - errorView.delegate = self - errorView.locale = selectedLocale - return errorView - case .empty: - let emptyView = EmptyStateView() - emptyView.image = R.image.iconEmptyHistory() - emptyView.title = R.string.localizable - .crowdloanEmptyMessage_v2_2_0(preferredLanguages: selectedLocale.rLanguages) - emptyView.titleColor = R.color.colorLightGray()! - emptyView.titleFont = .p2Paragraph - return emptyView - case .loading, .loaded: - return nil - } - } -} - -extension CrowdloanListViewController: EmptyStateDelegate { - var shouldDisplayEmptyState: Bool { - switch state { - case .error, .empty: - return true - case .loading, .loaded: - return false - } - } -} - extension CrowdloanListViewController: ErrorStateViewDelegate { func didRetry(errorView _: ErrorStateView) { presenter.refresh(shouldReset: true) } } +extension CrowdloanListViewController: LoadableViewProtocol {} extension CrowdloanListViewController: HiddableBarWhenPushed {} diff --git a/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListViewLayout.swift b/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListViewLayout.swift index e06426afc1..905e85ee20 100644 --- a/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListViewLayout.swift +++ b/novawallet/Modules/Crowdloan/CrowdloanList/CrowdloanListViewLayout.swift @@ -17,8 +17,6 @@ final class CrowdloanListViewLayout: UIView { return view }() - let statusView = UIView() - override init(frame: CGRect) { super.init(frame: frame) @@ -51,13 +49,6 @@ final class CrowdloanListViewLayout: UIView { addSubview(backgroundView) backgroundView.snp.makeConstraints { $0.edges.equalToSuperview() } - addSubview(statusView) - statusView.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview() - make.bottom.equalTo(safeAreaLayoutGuide) - make.top.equalTo(safeAreaLayoutGuide).inset(253) - } - addSubview(tableView) tableView.snp.makeConstraints { make in make.top.equalToSuperview() diff --git a/novawallet/Modules/Crowdloan/CrowdloanList/View/CrowdloanEmptyView.swift b/novawallet/Modules/Crowdloan/CrowdloanList/View/CrowdloanEmptyView.swift new file mode 100644 index 0000000000..4a952788a8 --- /dev/null +++ b/novawallet/Modules/Crowdloan/CrowdloanList/View/CrowdloanEmptyView.swift @@ -0,0 +1,52 @@ +import UIKit + +final class CrowdloanEmptyView: UIView { + var verticalSpacing: CGFloat = 8.0 { + didSet { + stackView.spacing = verticalSpacing + } + } + + let imageView = UIImageView() + + let titleLabel: UILabel = .create { + $0.numberOfLines = 0 + $0.textAlignment = .center + $0.backgroundColor = .clear + $0.textColor = R.color.colorWhite64() + $0.font = .p2Paragraph + } + + private lazy var stackView = UIStackView(arrangedSubviews: [ + imageView, + titleLabel + ]) + + 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() { + stackView.axis = .vertical + stackView.alignment = .center + stackView.spacing = verticalSpacing + + addSubview(stackView) + stackView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + func bind(image: UIImage?, text: String?) { + imageView.image = image + titleLabel.text = text + } +} diff --git a/novawallet/Modules/Crowdloan/CrowdloanList/View/CrowdloanStatusSectionView.swift b/novawallet/Modules/Crowdloan/CrowdloanList/View/CrowdloanStatusSectionView.swift index 7c9bcf734e..6fe6576cad 100644 --- a/novawallet/Modules/Crowdloan/CrowdloanList/View/CrowdloanStatusSectionView.swift +++ b/novawallet/Modules/Crowdloan/CrowdloanList/View/CrowdloanStatusSectionView.swift @@ -1,14 +1,16 @@ import UIKit final class CrowdloanStatusSectionView: UITableViewHeaderFooterView { - let titleLabel: UILabel = { - let label = UILabel() - label.textColor = R.color.colorWhite() - label.font = .h3Title - return label - }() + let titleLabel: UILabel = .create { + $0.textColor = R.color.colorWhite() + $0.font = .h3Title + } - private let countView = CountView() + let countView: BorderedLabelView = .create { + $0.titleLabel.textColor = R.color.colorWhite80() + $0.titleLabel.font = .semiBoldFootnote + $0.contentInsets = UIEdgeInsets(top: 2, left: 8, bottom: 3, right: 8) + } override init(reuseIdentifier: String?) { super.init(reuseIdentifier: reuseIdentifier) @@ -27,7 +29,7 @@ final class CrowdloanStatusSectionView: UITableViewHeaderFooterView { titleLabel.snp.makeConstraints { make in make.top.equalToSuperview().inset(24) make.leading.equalToSuperview().inset(20) - make.bottom.equalToSuperview().inset(8) + make.bottom.equalToSuperview().inset(16) } contentView.addSubview(countView) @@ -40,38 +42,6 @@ final class CrowdloanStatusSectionView: UITableViewHeaderFooterView { func bind(title: String, count: Int) { titleLabel.text = title - countView.countLabel.text = count.description - } -} - -private final class CountView: UIView { - let countLabel: UILabel = { - let label = UILabel() - label.textColor = R.color.colorWhite() - label.font = .p3Paragraph - return label - }() - - override func layoutSubviews() { - super.layoutSubviews() - - layer.cornerRadius = bounds.height / 2.0 - } - - override init(frame: CGRect) { - super.init(frame: frame) - - backgroundColor = R.color.colorWhite()?.withAlphaComponent(0.24) - - addSubview(countLabel) - countLabel.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview().inset(8) - make.bottom.top.equalToSuperview().inset(2) - } - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") + countView.titleLabel.text = count.description } } diff --git a/novawallet/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModel.swift b/novawallet/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModel.swift index bb85620020..cfb9573ddb 100644 --- a/novawallet/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModel.swift +++ b/novawallet/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModel.swift @@ -5,8 +5,6 @@ import CommonWallet enum CrowdloanListState { case loading case loaded(viewModel: CrowdloansViewModel) - case error(message: String) - case empty } struct CrowdloansViewModel { @@ -18,6 +16,8 @@ enum CrowdloansSection { case about(AboutCrowdloansView.Model) case active(String, [CrowdloanCellViewModel]) case completed(String, [CrowdloanCellViewModel]) + case error(message: String) + case empty(title: String) } enum CrowdloanDescViewModel { diff --git a/novawallet/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModelFactory.swift b/novawallet/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModelFactory.swift index b821bb5e92..d3bbd2f1f1 100644 --- a/novawallet/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModelFactory.swift +++ b/novawallet/Modules/Crowdloan/CrowdloanList/ViewModel/CrowdloansViewModelFactory.swift @@ -21,6 +21,11 @@ protocol CrowdloansViewModelFactoryProtocol { priceData: PriceData?, locale: Locale ) -> CrowdloansViewModel + + func createErrorViewModel( + chainAsset: ChainAssetDisplayInfo?, + locale: Locale + ) -> CrowdloansViewModel } final class CrowdloansViewModelFactory { @@ -338,6 +343,20 @@ extension CrowdloansViewModelFactory: CrowdloansViewModelFactoryProtocol { ) } + func createErrorViewModel( + chainAsset: ChainAssetDisplayInfo?, + locale: Locale + ) -> CrowdloansViewModel { + let message = R.string.localizable + .commonErrorNoDataRetrieved_v3_9_1(preferredLanguages: locale.rLanguages) + let errorSection = CrowdloansSection.error(message: message) + let aboutSection = createAboutSection(chainAsset: chainAsset, locale: locale) + return .init(sections: [ + aboutSection, + errorSection + ]) + } + func createViewModel( from crowdloans: [Crowdloan], viewInfo: CrowdloansViewInfo, @@ -347,6 +366,18 @@ extension CrowdloansViewModelFactory: CrowdloansViewModelFactoryProtocol { priceData: PriceData?, locale: Locale ) -> CrowdloansViewModel { + guard !crowdloans.isEmpty else { + let aboutSection = createAboutSection(chainAsset: chainAsset, locale: locale) + let activeTitle = R.string.localizable + .crowdloanActiveSection(preferredLanguages: locale.rLanguages) + let emptySection = CrowdloansSection.empty(title: activeTitle) + + return .init(sections: [ + aboutSection, + emptySection + ]) + } + let timeFormatter = TotalTimeFormatter() let quantityFormatter = NumberFormatter.quantity.localizableResource().value(for: locale) let tokenFormatter = amountFormatterFactory.createTokenFormatter( @@ -392,9 +423,10 @@ extension CrowdloansViewModelFactory: CrowdloansViewModelFactoryProtocol { return .init(sections: [contributionSection] + crowdloansSections) } - private func createAboutSection(chainAsset: ChainAssetDisplayInfo, locale: Locale) -> CrowdloansSection { + private func createAboutSection(chainAsset: ChainAssetDisplayInfo?, locale: Locale) -> CrowdloansSection { + let symbol = chainAsset?.asset.symbol ?? "" let description = R.string.localizable.crowdloanListSectionFormat_v2_2_0( - chainAsset.asset.symbol, + symbol, preferredLanguages: locale.rLanguages ) diff --git a/novawallet/Modules/Crowdloan/View/BlurredTableViewCell.swift b/novawallet/Modules/Crowdloan/View/BlurredTableViewCell.swift index f0a1abfad2..b8585e2493 100644 --- a/novawallet/Modules/Crowdloan/View/BlurredTableViewCell.swift +++ b/novawallet/Modules/Crowdloan/View/BlurredTableViewCell.swift @@ -10,6 +10,12 @@ class BlurredTableViewCell: UITableViewCell where TContentView: UI } } + var innerInsets: UIEdgeInsets = .zero { + didSet { + updateLayout() + } + } + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -35,7 +41,7 @@ class BlurredTableViewCell: UITableViewCell where TContentView: UI backgroundBlurView.addSubview(view) view.snp.makeConstraints { - $0.leading.top.trailing.bottom.equalToSuperview() + $0.edges.equalToSuperview().inset(innerInsets) } } @@ -43,6 +49,9 @@ class BlurredTableViewCell: UITableViewCell where TContentView: UI backgroundBlurView.snp.updateConstraints { $0.edges.equalToSuperview().inset(contentInsets) } + view.snp.updateConstraints { + $0.edges.equalToSuperview().inset(innerInsets) + } } } @@ -63,3 +72,21 @@ final class YourContributionsTableViewCell: BlurredTableViewCell + +extension BlurredTableViewCell where TContentView == ErrorStateView { + func applyStyle() { + view.errorDescriptionLabel.textColor = R.color.colorWhite64() + view.retryButton.titleLabel?.font = .semiBoldSubheadline + view.stackView.setCustomSpacing(0, after: view.iconImageView) + view.stackView.setCustomSpacing(8, after: view.errorDescriptionLabel) + contentInsets = .init(top: 8, left: 16, bottom: 0, right: 16) + innerInsets = .init(top: 4, left: 0, bottom: 16, right: 0) + } +} + +extension BlurredTableViewCell where TContentView == CrowdloanEmptyView { + func applyStyle() { + view.verticalSpacing = 0 + innerInsets = .init(top: 4, left: 0, bottom: 16, right: 0) + } +} diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index c4a098ffc8..2c9a8a70cc 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -288,6 +288,7 @@ "common.available.format" = "Available: %@"; "staking.rewards.learn.more" = "Learn more about rewards"; "common.error.no.data.retrieved" = "No data retrieved."; +"common.error.no.data.retrieved_v3_9_1" = "No data retrieved"; "staking.reward.payouts.empty.rewards" = "Perfect! All rewards are paid."; "staking.reward.details.validator" = "Validator"; "common.insufficient.balance" = "Insufficient balance"; @@ -575,6 +576,7 @@ "account.create.details_v2_2_0" = "Do not use clipboard or screenshots on your mobile device, try to find secure methods for backup (e.g. paper)"; "crowdloan.list.section.format_v2_2_0" = "Choose parachains to contribute your %@. You'll get back your contributed tokens, and if parachain wins a slot, you'll receive rewards after the end of the auction"; "crowdloan.empty.message_v2_2_0" = "Crowdloans will be displayed here"; +"crowdloan.empty.message_v3_9_1" = "Crowdloans information\nwill appear here when they start"; "common.existential.warning.message_v2_2_0" = "Your account will be removed from blockchain after this operation cause it makes total balance lower than minimal"; "wallet.send.existential.warning_v2_2_0" = "Your account will be removed from blockchain after transfer cause it makes total balance lower than minimal"; "account.backup.mnemonic.title" = "Write down the phrase and store it in a safe place"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 088f73e683..62af0c73c2 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -288,6 +288,7 @@ "common.available.format" = "Доступно: %@"; "staking.rewards.learn.more" = "Подробнее о вознаграждениях"; "common.error.no.data.retrieved" = "Данные не получены."; +"common.error.no.data.retrieved_v3_9_1" = "Данные не получены"; "staking.reward.payouts.empty.rewards" = "Прекрасно! Все вознаграждения выплачены."; "staking.reward.details.validator" = "Валидатор"; "common.insufficient.balance" = "Недостаточный баланс"; @@ -575,6 +576,7 @@ "account.create.details_v2_2_0" = "Не используйте буфер обмена или скриншоты на вашем мобильном устройстве, постарайтесь найти безопасные способы резервного копирования (например на бумагу)"; "crowdloan.list.section.format_v2_2_0" = "Выберите парачейны для внесения своих %@. Вы получите внесенные токены обратно, и если парачейн выиграет слот вы получите награду после окончания аукциона"; "crowdloan.empty.message_v2_2_0" = "Краудлоуны появятся здесь"; +"crowdloan.empty.message_v3_9_1" = "Информация о краудлоунах\nпоявится здесь, когда они начнутся"; "common.existential.warning.message_v2_2_0" = "Ваша учетная запись будет удалена из сети после операции, так как ваш баланс опустится ниже минимального"; "wallet.send.existential.warning_v2_2_0" = "Ваша учетная запись будет удалена из сети после перевода, так как опустит общий баланс ниже минимального"; "account.backup.mnemonic.title" = "Запишите фразу и храните её в надежном месте";