diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 32aee7b145..143dbfe9c8 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 003063A17B04BA6327EA355F /* ReferendumVotersProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6FE9E98CB265815986BE909 /* ReferendumVotersProtocols.swift */; }; 006BEDBD2F98FF54DB993D8C /* DAppAddFavoriteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 057EF626183878DD2C9E7BC7 /* DAppAddFavoriteViewController.swift */; }; 0075488169C69B97C0630EB8 /* AssetReceiveProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47042BBA5082B1EC1D017B56 /* AssetReceiveProtocols.swift */; }; + 0090AF084EEEA26E4018B1B3 /* NominationPoolSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 834314A1286CB91F8BB43F06 /* NominationPoolSearchViewController.swift */; }; 0119531FAE0D22EA9464F84D /* ParaStkYourCollatorsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 128EA612020D98F3B0D0FA96 /* ParaStkYourCollatorsProtocols.swift */; }; 012AE9F8BDA682C691B6F9FD /* ParitySignerWelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CF6E3E64F7608EF6C28399 /* ParitySignerWelcomeViewController.swift */; }; 0154E387EC40EF553FC7BD02 /* StackingRewardFiltersProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26BFF6789F0A11C5D870348 /* StackingRewardFiltersProtocols.swift */; }; @@ -27,14 +28,37 @@ 0678271BE1BA5BBC084F478A /* RecommendedValidatorListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57C624E71FCE0FFF8EAD5BA9 /* RecommendedValidatorListWireframe.swift */; }; 06FD6F5999D57B27B29C8738 /* ParaStkStakeConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037D3CE23FFD176F4F7DABC0 /* ParaStkStakeConfirmViewFactory.swift */; }; 0754911527A21957BD25A1DA /* CommonDelegationTracksViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD1EE26234E390E938D9311 /* CommonDelegationTracksViewFactory.swift */; }; + 07AB0BC861AA5F134DB9AC26 /* StartStakingInfoProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C92B3D5B314FB3EAE65FA471 /* StartStakingInfoProtocols.swift */; }; + 08999A79B34D287030887A7C /* StartStakingConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2876FA98B3E8F7EBCF5DEED0 /* StartStakingConfirmWireframe.swift */; }; 0909E06D5D06569554F70DD8 /* AssetsSearchInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44EAF50AAF6C23225E06C16C /* AssetsSearchInteractor.swift */; }; 09A6D92CE47636723DFC91F4 /* MessageSheetViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57535268534B154B42ED51CE /* MessageSheetViewFactory.swift */; }; 09AB6DE2D19F1FA36BF08288 /* StakingRebagConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D089568ABA686D509DF917 /* StakingRebagConfirmViewLayout.swift */; }; 0A44D28DF4BCF56131752F35 /* WalletConnectSessionDetailsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75ADB95DAB1F29E6A3FDD166 /* WalletConnectSessionDetailsPresenter.swift */; }; + 0A6114AACBAB1D55BC03F264 /* StartStakingInfoBaseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2BC4EFADD593325FF122765 /* StartStakingInfoBaseInteractor.swift */; }; 0AAFEFA17F249F4BEF051F6B /* ControllerAccountConfirmationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54FB887490A8B33890B4E0E4 /* ControllerAccountConfirmationPresenter.swift */; }; 0B2B9C6E2BA2E924D6A54F4B /* CrowdloanListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E78D69E8EBC3EB4D01F8EF /* CrowdloanListInteractor.swift */; }; 0B48B02E973CB304B765BBC9 /* ReferendumDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ABAD23C0039AFA8351C650 /* ReferendumDetailsProtocols.swift */; }; 0B65DAE0327678679CACE0B1 /* GovernanceDelegateInfoViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E894D4633D04AD4415CE1F2 /* GovernanceDelegateInfoViewFactory.swift */; }; + 0C12A2472AA1D96000C7FA49 /* SubqueryMultistakingTypeFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12A2462AA1D96000C7FA49 /* SubqueryMultistakingTypeFactory.swift */; }; + 0C12A2492AA35AE300C7FA49 /* RelaychainStakingValidatorFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12A2482AA35AE300C7FA49 /* RelaychainStakingValidatorFacade.swift */; }; + 0C13D2F52A7D2B440054BB6F /* DirectStakingRecommendationMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D2F42A7D2B440054BB6F /* DirectStakingRecommendationMediator.swift */; }; + 0C13D2F72A7D45F40054BB6F /* RelaychainStakingRestrictions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D2F62A7D45F40054BB6F /* RelaychainStakingRestrictions.swift */; }; + 0C13D2FA2A7D473B0054BB6F /* DirectStakingRestrictionsBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D2F92A7D473B0054BB6F /* DirectStakingRestrictionsBuilder.swift */; }; + 0C13D2FC2A7D4B220054BB6F /* RelaychainStakingRestrictionsBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D2FB2A7D4B220054BB6F /* RelaychainStakingRestrictionsBuilder.swift */; }; + 0C13D2FE2A7D4F500054BB6F /* PoolStakingRestrictionsBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D2FD2A7D4F500054BB6F /* PoolStakingRestrictionsBuilder.swift */; }; + 0C13D3002A7D50C10054BB6F /* PoolStakingRecommendationMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D2FF2A7D50C10054BB6F /* PoolStakingRecommendationMediator.swift */; }; + 0C13D3022A7D53C10054BB6F /* HybridStakingRecommendationMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3012A7D53C10054BB6F /* HybridStakingRecommendationMediator.swift */; }; + 0C13D3042A7D56CC0054BB6F /* StakingRecommendationMediatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3032A7D56CC0054BB6F /* StakingRecommendationMediatorFactory.swift */; }; + 0C13D3072A7FB92C0054BB6F /* NominationPoolsJoin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3062A7FB92C0054BB6F /* NominationPoolsJoin.swift */; }; + 0C13D3112A80CA9A0054BB6F /* StakingDataValidatorFactory+Plank.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3102A80CA9A0054BB6F /* StakingDataValidatorFactory+Plank.swift */; }; + 0C13D3132A80D06B0054BB6F /* StakingRecommendationValidationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3122A80D06B0054BB6F /* StakingRecommendationValidationFactory.swift */; }; + 0C13D3182A8216A10054BB6F /* NominationPoolsIconFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3172A8216A10054BB6F /* NominationPoolsIconFactory.swift */; }; + 0C13D31A2A8222D20054BB6F /* StartStakingConfirmInteractorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3192A8222D20054BB6F /* StartStakingConfirmInteractorError.swift */; }; + 0C13D31D2A82279D0054BB6F /* StartStakingDirectConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D31C2A82279D0054BB6F /* StartStakingDirectConfirmPresenter.swift */; }; + 0C13D31F2A8227AB0054BB6F /* StartStakingPoolConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D31E2A8227AB0054BB6F /* StartStakingPoolConfirmPresenter.swift */; }; + 0C13D3212A822B220054BB6F /* StartStakingDirectConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3202A822B220054BB6F /* StartStakingDirectConfirmWireframe.swift */; }; + 0C13D3242A823D810054BB6F /* StartStakingExtrinsicProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3232A823D810054BB6F /* StartStakingExtrinsicProxy.swift */; }; + 0C13D3262A8275400054BB6F /* StartStakingFeeIdFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C13D3252A82753F0054BB6F /* StartStakingFeeIdFactory.swift */; }; 0C154A0E2A45995500932C3F /* CompoundComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C154A0D2A45995500932C3F /* CompoundComparator.swift */; }; 0C17BD972A42F162004AF9E7 /* WalletViewModelObserverContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */; }; 0C17BD992A42F1BE004AF9E7 /* MoneyPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */; }; @@ -49,6 +73,16 @@ 0C29B5382A4C68A500E35C6D /* AnimationUpdatibleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C29B5372A4C68A500E35C6D /* AnimationUpdatibleView.swift */; }; 0C2AA829B5CB89B39E0FA95E /* CrowdloanContributionConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF01941105BCD02536538362 /* CrowdloanContributionConfirmProtocols.swift */; }; 0C2F86802A7119D400593C01 /* AuraSessionLengthOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F867F2A7119D400593C01 /* AuraSessionLengthOperationFactory.swift */; }; + 0C2F86822A7233DC00593C01 /* EraNominationPoolsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F86812A7233DC00593C01 /* EraNominationPoolsService.swift */; }; + 0C2F86842A72343800593C01 /* EraNominationPoolsServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F86832A72343800593C01 /* EraNominationPoolsServiceProtocol.swift */; }; + 0C2F86862A72352400593C01 /* NominationPoolModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F86852A72352400593C01 /* NominationPoolModel.swift */; }; + 0C2F86892A723E5400593C01 /* NominationPoolsOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F86882A723E5400593C01 /* NominationPoolsOperationFactory.swift */; }; + 0C2F868B2A725C3C00593C01 /* EraNominationPoolsChanged.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F868A2A725C3C00593C01 /* EraNominationPoolsChanged.swift */; }; + 0C2F868F2A725E4F00593C01 /* DefaultStakingRewardDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F868D2A725E4F00593C01 /* DefaultStakingRewardDestination.swift */; }; + 0C2F86932A72648D00593C01 /* ActiveNominationPoolsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F86922A72648D00593C01 /* ActiveNominationPoolsTests.swift */; }; + 0C2F86962A72807E00593C01 /* NominationPoolsRewardEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F86952A72807E00593C01 /* NominationPoolsRewardEngine.swift */; }; + 0C2F86982A728EE900593C01 /* NPoolsRewardEngineFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F86972A728EE900593C01 /* NPoolsRewardEngineFactory.swift */; }; + 0C2F869A2A72948100593C01 /* NominationPoolsApyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2F86992A72948100593C01 /* NominationPoolsApyTests.swift */; }; 0C3205BB2A8679F0002EB914 /* EvmGasPriceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205BA2A8679F0002EB914 /* EvmGasPriceProvider.swift */; }; 0C3205BE2A867A9C002EB914 /* EvmLegacyGasPriceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205BD2A867A9C002EB914 /* EvmLegacyGasPriceProvider.swift */; }; 0C3205C02A867DD6002EB914 /* EvmMaxPriorityGasPriceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3205BF2A867DD6002EB914 /* EvmMaxPriorityGasPriceProvider.swift */; }; @@ -76,28 +110,137 @@ 0C463FC82A58126A003E71C9 /* UIView+MotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C463FC72A58126A003E71C9 /* UIView+MotionEffect.swift */; }; 0C463FD02A592ACD003E71C9 /* PartialInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C463FCF2A592ACD003E71C9 /* PartialInterpolatingMotionEffect.swift */; }; 0C5364A02A4D6EB700990478 /* AssetListBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C53649F2A4D6EB700990478 /* AssetListBuilder.swift */; }; + 0C543E972AAB1B350035F45F /* ElectedAndPrefValidators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C543E962AAB1B350035F45F /* ElectedAndPrefValidators.swift */; }; 0C56B29DBA5245728AF7EDA4 /* GovernanceEditDelegationTracksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B18E8361691E548ABAB33EA4 /* GovernanceEditDelegationTracksViewController.swift */; }; 0C56B4FB2A4B0C320030F9C9 /* AssetListBaseBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C56B4FA2A4B0C320030F9C9 /* AssetListBaseBuilder.swift */; }; 0C56B4FD2A4B0CA90030F9C9 /* AssetListBuilderResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C56B4FC2A4B0CA90030F9C9 /* AssetListBuilderResult.swift */; }; + 0C59E8C92AA5C7EC001E11F3 /* ExternalAssetBalance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8C82AA5C7EC001E11F3 /* ExternalAssetBalance.swift */; }; + 0C59E8CD2AA5D673001E11F3 /* PooledBalanceUpdatingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8CC2AA5D673001E11F3 /* PooledBalanceUpdatingService.swift */; }; + 0C59E8CF2AA5D744001E11F3 /* PooledBalanceUpdatingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8CE2AA5D744001E11F3 /* PooledBalanceUpdatingState.swift */; }; + 0C59E8D12AA5FAC5001E11F3 /* PooledAssetBalance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8D02AA5FAC5001E11F3 /* PooledAssetBalance.swift */; }; + 0C59E8D32AA5FBE2001E11F3 /* PooledAssetBalanceMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8D22AA5FBE2001E11F3 /* PooledAssetBalanceMapper.swift */; }; + 0C59E8D52AA5FC96001E11F3 /* ExternalAssetBalanceMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8D42AA5FC96001E11F3 /* ExternalAssetBalanceMapper.swift */; }; + 0C59E8D82AA60490001E11F3 /* ExternalAssetBalanceStreambleSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8D72AA60490001E11F3 /* ExternalAssetBalanceStreambleSource.swift */; }; + 0C59E8DA2AA6085B001E11F3 /* ExternalAssetBalanceLocalSubscriptionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8D92AA6085B001E11F3 /* ExternalAssetBalanceLocalSubscriptionFactory.swift */; }; + 0C59E8DC2AA60C3B001E11F3 /* NSPredicate+ExternalAssetBalance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8DB2AA60C3B001E11F3 /* NSPredicate+ExternalAssetBalance.swift */; }; + 0C59E8DF2AA60DAB001E11F3 /* ExternalAssetBalanceServiceFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8DE2AA60DAB001E11F3 /* ExternalAssetBalanceServiceFactoryProtocol.swift */; }; + 0C59E8E12AA60FF0001E11F3 /* CrowdloanExternalServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8E02AA60FF0001E11F3 /* CrowdloanExternalServiceFactory.swift */; }; + 0C59E8E32AA61252001E11F3 /* NominationPoolExternalServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8E22AA61252001E11F3 /* NominationPoolExternalServiceFactory.swift */; }; + 0C59E8E52AA6191E001E11F3 /* ExternalAssetBalanceSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8E42AA6191E001E11F3 /* ExternalAssetBalanceSubscriber.swift */; }; + 0C59E8E72AA61933001E11F3 /* ExternalAssetBalanceSubscriptionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8E62AA61933001E11F3 /* ExternalAssetBalanceSubscriptionHandler.swift */; }; + 0C59E8EB2AA71C3E001E11F3 /* ExternalAssetBalanceIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8EA2AA71C3E001E11F3 /* ExternalAssetBalanceIntegrationTests.swift */; }; + 0C59E8ED2AA75C84001E11F3 /* HistoryPoolRewardContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8EC2AA75C84001E11F3 /* HistoryPoolRewardContext.swift */; }; + 0C59E8F02AA76361001E11F3 /* OperationDetailsDataProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8EF2AA76361001E11F3 /* OperationDetailsDataProviderProtocol.swift */; }; + 0C59E8F22AA76436001E11F3 /* OperationDetailsTransferProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8F12AA76436001E11F3 /* OperationDetailsTransferProvider.swift */; }; + 0C59E8F42AA7649E001E11F3 /* OperationDetailsBaseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8F32AA7649E001E11F3 /* OperationDetailsBaseProvider.swift */; }; + 0C59E8F62AA76772001E11F3 /* OperationDetailsExtrinsicProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8F52AA76772001E11F3 /* OperationDetailsExtrinsicProvider.swift */; }; + 0C59E8F82AA76833001E11F3 /* OperationDetailsContractProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8F72AA76833001E11F3 /* OperationDetailsContractProvider.swift */; }; + 0C59E8FA2AA76A4A001E11F3 /* OperationDetailsDirectStakingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8F92AA76A4A001E11F3 /* OperationDetailsDirectStakingProvider.swift */; }; + 0C59E8FC2AA76C4A001E11F3 /* OperationDetailsPoolStakingProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8FB2AA76C4A001E11F3 /* OperationDetailsPoolStakingProvider.swift */; }; + 0C59E8FE2AA773D6001E11F3 /* OperationDetailsDataProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C59E8FD2AA773D6001E11F3 /* OperationDetailsDataProviderFactory.swift */; }; + 0C626D1B2A8F519100CDAF4E /* StakingMainPresenterFactory+NominationPools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C626D1A2A8F519100CDAF4E /* StakingMainPresenterFactory+NominationPools.swift */; }; + 0C626D1D2A915E2F00CDAF4E /* StakingNominationPoolsStatics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C626D1C2A915E2F00CDAF4E /* StakingNominationPoolsStatics.swift */; }; + 0C626D1F2A92AA0F00CDAF4E /* NominationPoolsDataProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C626D1E2A92AA0F00CDAF4E /* NominationPoolsDataProviding.swift */; }; + 0C626D212A933A7D00CDAF4E /* StakingNPoolsViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C626D202A933A7D00CDAF4E /* StakingNPoolsViewModelFactory.swift */; }; + 0C66102B2A73816000E44634 /* StakingSharedStateFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C66102A2A73816000E44634 /* StakingSharedStateFactory.swift */; }; + 0C66102D2A73828800E44634 /* RelaychainStakingSharedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C66102C2A73828800E44634 /* RelaychainStakingSharedState.swift */; }; + 0C66102F2A78E9D700E44634 /* RelaychainStartStakingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C66102E2A78E9D700E44634 /* RelaychainStartStakingState.swift */; }; 0C6D66AB2A8C0B6700AAB988 /* BaseSigner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6D66AA2A8C0B6700AAB988 /* BaseSigner.swift */; }; + 0C6F0C9E2A69723B007170C6 /* StartStakingStateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6F0C9C2A69723B007170C6 /* StartStakingStateProtocol.swift */; }; + 0C77B55F2A83717000B5AE08 /* StaticValidatorListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C77B55E2A83717000B5AE08 /* StaticValidatorListViewController.swift */; }; + 0C77B5612A8371AA00B5AE08 /* StaticValidatorListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C77B5602A8371AA00B5AE08 /* StaticValidatorListProtocols.swift */; }; + 0C77B5632A83747200B5AE08 /* StaticValidatorListViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C77B5622A83747200B5AE08 /* StaticValidatorListViewLayout.swift */; }; + 0C77B5652A8374EA00B5AE08 /* StaticValidatorListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C77B5642A8374EA00B5AE08 /* StaticValidatorListPresenter.swift */; }; + 0C77B5672A837AC500B5AE08 /* StaticValidatorListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C77B5662A837AC500B5AE08 /* StaticValidatorListWireframe.swift */; }; + 0C77B5692A837D4000B5AE08 /* StaticValidatorListViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C77B5682A837D4000B5AE08 /* StaticValidatorListViewFactory.swift */; }; + 0C79C8922A7BD9BB00B171E3 /* SelectedStakingOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79C8912A7BD9BB00B171E3 /* SelectedStakingOption.swift */; }; + 0C79C8952A7BE01100B171E3 /* RelaychainStakingRecommendation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79C8942A7BE01100B171E3 /* RelaychainStakingRecommendation.swift */; }; + 0C79C8992A7BE46A00B171E3 /* AssetModel+Staking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79C8982A7BE46A00B171E3 /* AssetModel+Staking.swift */; }; + 0C79C89C2A7BE6A200B171E3 /* DirectStakingRecommendationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79C89B2A7BE6A200B171E3 /* DirectStakingRecommendationFactory.swift */; }; + 0C79C89E2A7BEF1200B171E3 /* NominationPoolRecommendationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79C89D2A7BEF1200B171E3 /* NominationPoolRecommendationFactory.swift */; }; + 0C79C8A02A7BF80700B171E3 /* RelaychainStakingRecommendationMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79C89F2A7BF80700B171E3 /* RelaychainStakingRecommendationMediator.swift */; }; + 0C7C88612A94A54E00DD96A1 /* StakingNPoolsViewModelFactory+Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7C88602A94A54E00DD96A1 /* StakingNPoolsViewModelFactory+Alerts.swift */; }; + 0C7C88642A94E09F00DD96A1 /* NPoolsPendingRewardDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7C88632A94E09F00DD96A1 /* NPoolsPendingRewardDataSource.swift */; }; + 0C7C88662A95030800DD96A1 /* SubqueryStakingType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7C88652A95030800DD96A1 /* SubqueryStakingType.swift */; }; + 0C7C88682A95563100DD96A1 /* StakingClaimableRewardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7C88672A95563100DD96A1 /* StakingClaimableRewardView.swift */; }; + 0C7C886A2A95591900DD96A1 /* StakingTotalRewardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7C88692A95591900DD96A1 /* StakingTotalRewardView.swift */; }; + 0C7C886C2A9622F800DD96A1 /* StakingSelectedEntityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7C886B2A9622F800DD96A1 /* StakingSelectedEntityViewModel.swift */; }; + 0C7C886E2A962B0D00DD96A1 /* StackAddressCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7C886D2A962B0D00DD96A1 /* StackAddressCell.swift */; }; + 0C7E7FAB2A9F27FB00596628 /* NominationPoolsRedeemCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7E7FAA2A9F27FB00596628 /* NominationPoolsRedeemCall.swift */; }; 0C83775D2A4EEB380072102D /* AssetListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C83775C2A4EEB380072102D /* AssetListState.swift */; }; + 0C893E6A2A65591C00781503 /* PoolsMultistakingUpdateService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C893E692A65591C00781503 /* PoolsMultistakingUpdateService.swift */; }; + 0C893E6D2A6562B400781503 /* NominationPools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C893E6C2A6562B400781503 /* NominationPools.swift */; }; + 0C893E6F2A65702A00781503 /* NominationPools+CodingPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C893E6E2A65702A00781503 /* NominationPools+CodingPath.swift */; }; 0C8A25592A553A6C0072882A /* KeyboardAppearanceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8A25582A553A6C0072882A /* KeyboardAppearanceState.swift */; }; + 0C9525E32A7AAB2A00BD724D /* StakingTimeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9525E22A7AAB2A00BD724D /* StakingTimeModel.swift */; }; + 0C9525E52A7AF82B00BD724D /* DirectStakingMinStakeBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9525E42A7AF82B00BD724D /* DirectStakingMinStakeBuilder.swift */; }; + 0C9525E72A7AFA2C00BD724D /* ValueResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9525E62A7AFA2C00BD724D /* ValueResolver.swift */; }; + 0C9525EB2A7B7F5000BD724D /* ChainModel+Additional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9525EA2A7B7F5000BD724D /* ChainModel+Additional.swift */; }; + 0C962F862AA859F200C0B551 /* TransactionHistoryLocalFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C962F852AA859F200C0B551 /* TransactionHistoryLocalFilter.swift */; }; + 0C962F882AA85C7F00C0B551 /* TransactionHistoryAccountPrefixFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C962F872AA85C7F00C0B551 /* TransactionHistoryAccountPrefixFilter.swift */; }; + 0C962F8A2AA8614500C0B551 /* TransactionHistoryLocalFilterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C962F892AA8614500C0B551 /* TransactionHistoryLocalFilterFactory.swift */; }; 0C9680F12A8A85BB006A411B /* TokenAddValidationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9680F02A8A85BB006A411B /* TokenAddValidationFactory.swift */; }; 0C9680F32A8AC2F2006A411B /* EvmTokenAddResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9680F22A8AC2F2006A411B /* EvmTokenAddResult.swift */; }; 0C9C642D2A8CE30A004DC078 /* SystemAccountValidating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C642C2A8CE30A004DC078 /* SystemAccountValidating.swift */; }; + 0C9C64302A8D6779004DC078 /* StakingNPoolsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C642F2A8D6779004DC078 /* StakingNPoolsPresenter.swift */; }; + 0C9C64322A8D67A0004DC078 /* StakingNPoolsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C64312A8D67A0004DC078 /* StakingNPoolsInteractor.swift */; }; + 0C9C64342A8D67AF004DC078 /* StakingNPoolsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C64332A8D67AF004DC078 /* StakingNPoolsWireframe.swift */; }; + 0C9C64362A8D67FB004DC078 /* StakingNPoolsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C64352A8D67FB004DC078 /* StakingNPoolsProtocols.swift */; }; + 0C9C64382A8D6949004DC078 /* NPoolsStakingSharedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C64372A8D6949004DC078 /* NPoolsStakingSharedState.swift */; }; + 0C9C643A2A8DF97E004DC078 /* StakingNPoolsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9C64392A8DF97E004DC078 /* StakingNPoolsError.swift */; }; 0C9ECB5A2A4A9AB400BDCA73 /* AssetListAccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9ECB592A4A9AB400BDCA73 /* AssetListAccountCell.swift */; }; 0CA307BC2F570941CD22C9AA /* ExportMnemonicConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF4688AF0658F8BB7A90C2BE /* ExportMnemonicConfirmViewFactory.swift */; }; 0CAC01552A52E0CC0069413E /* AssetListModelHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAC01542A52E0CC0069413E /* AssetListModelHelpers.swift */; }; 0CAC01572A52E1960069413E /* AssetListPresenterHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAC01562A52E1960069413E /* AssetListPresenterHelpers.swift */; }; + 0CAC44AA2A7A79C2001EDE61 /* StartStakingInfoParachainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAC44A92A7A79C2001EDE61 /* StartStakingInfoParachainWireframe.swift */; }; + 0CAC44AC2A7A7FFD001EDE61 /* RelaychainConsensusStateDepending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAC44AB2A7A7FFD001EDE61 /* RelaychainConsensusStateDepending.swift */; }; 0CB064682A403ADE00BFBA3F /* AmountDecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB064672A403ADE00BFBA3F /* AmountDecimal.swift */; }; 0CB0646A2A40572C00BFBA3F /* AmountInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB064692A40572C00BFBA3F /* AmountInputViewModel.swift */; }; + 0CB06E732A6800F500C7EC99 /* NominationPools+Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB06E722A6800F500C7EC99 /* NominationPools+Functions.swift */; }; + 0CB06E752A68139C00C7EC99 /* StakingDashboardNominationPoolMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB06E742A68139C00C7EC99 /* StakingDashboardNominationPoolMapper.swift */; }; + 0CB261D92A9893E500287305 /* NPoolsUnstakeBaseProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261D82A9893E500287305 /* NPoolsUnstakeBaseProtocols.swift */; }; + 0CB261DB2A98943800287305 /* NPoolsUnstakeBaseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261DA2A98943800287305 /* NPoolsUnstakeBaseError.swift */; }; + 0CB261DE2A989D2A00287305 /* NominationPoolsUnstakeLimits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261DD2A989D2A00287305 /* NominationPoolsUnstakeLimits.swift */; }; + 0CB261E02A98BEBD00287305 /* NPoolsUnstakeBaseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261DF2A98BEBD00287305 /* NPoolsUnstakeBaseInteractor.swift */; }; + 0CB261E22A9B215B00287305 /* NominationPoolUnstake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261E12A9B215B00287305 /* NominationPoolUnstake.swift */; }; + 0CB261E42A9BE31B00287305 /* NPoolsUnstakeBasePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261E32A9BE31B00287305 /* NPoolsUnstakeBasePresenter.swift */; }; + 0CB261E72A9C7C9D00287305 /* NPoolsUnstakeHintsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261E62A9C7C9D00287305 /* NPoolsUnstakeHintsFactory.swift */; }; + 0CB261EA2A9C940A00287305 /* NominationPoolsUnstakeOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261E92A9C940A00287305 /* NominationPoolsUnstakeOperationFactory.swift */; }; + 0CB261EF2A9E103900287305 /* NPoolsClaimRewardsStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261EE2A9E103900287305 /* NPoolsClaimRewardsStrategy.swift */; }; + 0CB261F12A9E149C00287305 /* NPoolsClaimRewardsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261F02A9E149C00287305 /* NPoolsClaimRewardsError.swift */; }; + 0CB261F32A9E182300287305 /* NominationPoolClaimRewards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261F22A9E182300287305 /* NominationPoolClaimRewards.swift */; }; + 0CB261F52A9E188300287305 /* NominationPoolsBondExtraCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261F42A9E188300287305 /* NominationPoolsBondExtraCall.swift */; }; + 0CB261F72A9E2D8400287305 /* StackSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261F62A9E2D8400287305 /* StackSwitchCell.swift */; }; + 0CB261F92A9F1F2200287305 /* NPoolsRedeemError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB261F82A9F1F2200287305 /* NPoolsRedeemError.swift */; }; 0CBC29C62A421B5000F7B1F7 /* StakingMainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC29C52A421B5000F7B1F7 /* StakingMainWireframe.swift */; }; 0CBC29C82A42AE2F00F7B1F7 /* StakingDashboardBuilderResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC29C72A42AE2F00F7B1F7 /* StakingDashboardBuilderResult.swift */; }; + 0CC2E55E2A6AB2B7004092E7 /* StashItemMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2E55D2A6AB2B7004092E7 /* StashItemMapper.swift */; }; + 0CC2E5602A6E44E7004092E7 /* NominationPoolsRemoteSubscriptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2E55F2A6E44E7004092E7 /* NominationPoolsRemoteSubscriptionService.swift */; }; + 0CC2E5622A6E5C43004092E7 /* NominationPoolsAccountUpdatingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2E5612A6E5C43004092E7 /* NominationPoolsAccountUpdatingService.swift */; }; + 0CC2E5642A6E5C72004092E7 /* NPoolsLocalSubscriptionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2E5632A6E5C72004092E7 /* NPoolsLocalSubscriptionFactory.swift */; }; + 0CC2E5662A6E64EC004092E7 /* NPoolsLocalStorageSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2E5652A6E64EC004092E7 /* NPoolsLocalStorageSubscriber.swift */; }; + 0CC2E5682A6E64FD004092E7 /* NPoolsLocalStorageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2E5672A6E64FD004092E7 /* NPoolsLocalStorageHandler.swift */; }; + 0CC2E56A2A6E6EBB004092E7 /* LocalStorageProviderObserving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC2E5692A6E6EBB004092E7 /* LocalStorageProviderObserving.swift */; }; + 0CC4CCF42A67C9C400F63041 /* Multistaking+NominationPools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC4CCF32A67C9C400F63041 /* Multistaking+NominationPools.swift */; }; + 0CC6C8D82AAB401200AD8D9B /* CustomValidatorsFullList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC6C8D72AAB401200AD8D9B /* CustomValidatorsFullList.swift */; }; 0CCE25212A44306200286709 /* TransactionHistoryPhishingFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCE25202A44306200286709 /* TransactionHistoryPhishingFilter.swift */; }; 0CD1F4D100ED82D137AB9834 /* ParaStkStakeSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2F1EEBF48485F02BF690A4 /* ParaStkStakeSetupViewController.swift */; }; + 0CE150502A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE1504F2A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift */; }; + 0CE150542A70EA2200B61CC1 /* NominationPoolsSyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE150532A70EA2200B61CC1 /* NominationPoolsSyncTests.swift */; }; 0CE550B32A49658700F0A7AC /* StakingDuration+Localizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE550B22A49658700F0A7AC /* StakingDuration+Localizable.swift */; }; 0CE550B62A49741400F0A7AC /* StakingUnbondSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE550B52A49741400F0A7AC /* StakingUnbondSetupTests.swift */; }; 0CE5BF4A6BC02563113DFDB8 /* GovernanceDelegateInfoInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF7E7AAE54BF5E6A35BDD29B /* GovernanceDelegateInfoInteractor.swift */; }; + 0CE629D62AA9B5E200E250BD /* BalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE629D42AA9B5E200E250BD /* BalanceViewModel.swift */; }; + 0CE629D72AA9B5E200E250BD /* BalanceViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE629D52AA9B5E200E250BD /* BalanceViewModelFactory.swift */; }; + 0CE629D92AA9B68C00E250BD /* AssetBalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE629D82AA9B68C00E250BD /* AssetBalanceViewModel.swift */; }; + 0CE629DD2AA9B6BF00E250BD /* RewardDestinationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE629DB2AA9B6BE00E250BD /* RewardDestinationViewModel.swift */; }; + 0CE629DE2AA9B6BF00E250BD /* RewardDestinationViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE629DC2AA9B6BE00E250BD /* RewardDestinationViewModelFactory.swift */; }; + 0CE629E02AA9B70200E250BD /* CalculatedReward.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE629DF2AA9B70200E250BD /* CalculatedReward.swift */; }; + 0CE629E22AA9BE3000E250BD /* StakingSetupTypeEntityFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE629E12AA9BE3000E250BD /* StakingSetupTypeEntityFacade.swift */; }; + 0CF193D12A843DA9003F12F6 /* StakingTypeBalanceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D02A843DA9003F12F6 /* StakingTypeBalanceFactory.swift */; }; + 0CF193D32A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D22A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift */; }; + 0CF193D52A861926003F12F6 /* PredefinedTimeShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D42A861925003F12F6 /* PredefinedTimeShortcut.swift */; }; + 0CF193D72A861D7E003F12F6 /* StartStakingInfoConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF193D62A861D7E003F12F6 /* StartStakingInfoConstants.swift */; }; 0D5245ED354CC52A842C85A0 /* TransferConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD8B98AB03AAF06AA891695 /* TransferConfirmViewLayout.swift */; }; 0D8213272889988B78188D9A /* DAppWalletAuthInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 337EC62037D657258BCBC02F /* DAppWalletAuthInteractor.swift */; }; 0DACB56C0BDD4C984FE3C15C /* AssetReceiveWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C1179A25C22AF0875A1ADCD /* AssetReceiveWireframe.swift */; }; @@ -113,6 +256,7 @@ 0FB6781AB0186A1ED474CAD6 /* StakingUnbondConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD348E749EC6A7E3BB069DE /* StakingUnbondConfirmProtocols.swift */; }; 1062C095BC566A1EA8DE1C06 /* CrowdloanContributionSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C71DEF78B69F017DF460AB7 /* CrowdloanContributionSetupViewController.swift */; }; 106CC4BFC48B6BFFF31434A9 /* LedgerWalletConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BC1402B34E341312ABB378 /* LedgerWalletConfirmPresenter.swift */; }; + 1180349875F35B4D4DD88A4C /* StakingTypeViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4752D80077E85563CF3AD5D /* StakingTypeViewFactory.swift */; }; 11C6F4CD5B167DE4E9E7F654 /* DAppPhishingWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518305BB475DE40E94DCBD5D /* DAppPhishingWireframe.swift */; }; 1232A714A96F937330FC0AFA /* GovernanceDelegateConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B718CE9C51158F87D37894BB /* GovernanceDelegateConfirmViewFactory.swift */; }; 1269C0103216CDBDA25A5101 /* ReferendumFullDetailsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = C80D934D47929D2331111AD7 /* ReferendumFullDetailsWireframe.swift */; }; @@ -121,13 +265,17 @@ 135CEEC5363BE34130958578 /* ControllerAccountConfirmationInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B8473AA386E1AD6F0F0C964 /* ControllerAccountConfirmationInteractor.swift */; }; 135E979B52DC1BD29A5FC389 /* ParaStkRedeemProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E1179D4A18F46C75B19CAC2 /* ParaStkRedeemProtocols.swift */; }; 13CF38563E1849EAF1B4E4B6 /* ParitySignerAddConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2CF76ABE7BC9A99724D393 /* ParitySignerAddConfirmViewFactory.swift */; }; + 13DE59F1804CD6761EBC26B9 /* NPoolsUnstakeConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793356FB65FB73CE7097C6F1 /* NPoolsUnstakeConfirmProtocols.swift */; }; + 141BF00B1B59940711773726 /* StakingSelectPoolWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6654DEA68D7ED47AE8E52206 /* StakingSelectPoolWireframe.swift */; }; 148748ACAE23B7D15144015B /* DAppAuthSettingsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F23EDFB699CAEEADC9263A0D /* DAppAuthSettingsViewFactory.swift */; }; 14DD3CD30D9D658961078037 /* ReferendumsFiltersViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025FA616E9CFA2BC7C364B74 /* ReferendumsFiltersViewFactory.swift */; }; 151722B1A7CC5B181A51869D /* StakingMoreOptionsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74E4A8D1E4FEA5936AA38B24 /* StakingMoreOptionsInteractor.swift */; }; 1550A6E8789263C0D734091A /* StakingUnbondSetupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC5083A5751A1A3CC95F4F6F /* StakingUnbondSetupWireframe.swift */; }; 15B079FA97C96327FD4A2E16 /* GovernanceYourDelegationsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7186BB5CB39B28A78D35D635 /* GovernanceYourDelegationsWireframe.swift */; }; 16098DABB1C9C058C1965F1D /* GovernanceUnlockSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FC9F0E50317E1583EA8E345 /* GovernanceUnlockSetupViewController.swift */; }; + 1628245885FF82F14BA09E5C /* NPoolsUnstakeConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D790172C70C98CAE984F7183 /* NPoolsUnstakeConfirmViewController.swift */; }; 1633E4E12AF8B5C16F141944 /* DAppAuthSettingsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 812BCD9B7B25BCA02E32452E /* DAppAuthSettingsInteractor.swift */; }; + 16359021D9683A59F293FA67 /* NPoolsUnstakeConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5294102607A80F62F9848 /* NPoolsUnstakeConfirmViewFactory.swift */; }; 163709FEE6203813261DD771 /* ReferendumVoteConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3531B386DEF40108C34E7232 /* ReferendumVoteConfirmViewLayout.swift */; }; 165FD10F3329A67373DA78C6 /* ReferendumsFiltersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9DFFBEFB45E0A03FAA142C3 /* ReferendumsFiltersViewController.swift */; }; 166EDC263AC503FF5963005F /* DAppSettingsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3789A955202DA7CCADE13E68 /* DAppSettingsProtocols.swift */; }; @@ -135,15 +283,19 @@ 16FAE3C58B34D700D8A7A217 /* DAppListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E6E41F045986002E1E26C12 /* DAppListWireframe.swift */; }; 1705C47629E3A63D18D39B9C /* TransactionHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAE0A2542AE83345BCCA549 /* TransactionHistoryViewController.swift */; }; 1710F415F6AC7BBC622F4BD2 /* ParaStkSelectCollatorsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC320A5EC0D18B6F443BB2E /* ParaStkSelectCollatorsInteractor.swift */; }; + 175CA0D71131FF37CF4A3CB9 /* NominationPoolSearchWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06D9A7B84C1B985D33E72D84 /* NominationPoolSearchWireframe.swift */; }; 1772735F89EFA931DF7420AD /* TokensManagePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC70DB97D2E9350022A899B /* TokensManagePresenter.swift */; }; 1795E946F1E386442E96E2BC /* StakingPayoutConfirmationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C0AC11A4A195BB697578CE /* StakingPayoutConfirmationPresenter.swift */; }; 179DDEB4F2431F02D60117BD /* GovernanceSelectTracksProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC7A05FDBA1F8B71C61001A4 /* GovernanceSelectTracksProtocols.swift */; }; 1812D5012A1765CB38D32A4A /* WalletsListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D6E67AD564867E121601F18 /* WalletsListPresenter.swift */; }; 187C300E406092FA5F682A61 /* LedgerPerformOperationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FD612D8F897463726CDD033 /* LedgerPerformOperationViewController.swift */; }; + 18AB038EB690E4912A003755 /* NPoolsClaimRewardsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50AFCEE78CDFC4FE3238E158 /* NPoolsClaimRewardsViewController.swift */; }; 19055A725FD9C18753B74A52 /* StakingRewardFiltersViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AF0DA16AC88A24E968490DF /* StakingRewardFiltersViewLayout.swift */; }; 19A29027666EB5388CBFAD61 /* StakingRewardDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D613E20E96E7BA5B8F4B9799 /* StakingRewardDetailsInteractor.swift */; }; + 19B35C4E96708405754B8EC5 /* NPoolsUnstakeSetupProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E599C8CB87A955659DAEBF /* NPoolsUnstakeSetupProtocols.swift */; }; 19D3739A3C7800A5A18DA41C /* LedgerNetworkSelectionInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56C015A20B918DF75C499FFF /* LedgerNetworkSelectionInteractor.swift */; }; 1A029717AD309487B70FFD02 /* ReferendumDetailsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2F90150AD2DD3CDF7F4EDA /* ReferendumDetailsViewFactory.swift */; }; + 1A339AB0E007BC2E87B36237 /* StartStakingInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5788D702D2B2203949329E /* StartStakingInfoViewController.swift */; }; 1B1402BB29CFF6D9FB944B2D /* CreateWatchOnlyViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9DD724F02DA0A174D875A8 /* CreateWatchOnlyViewLayout.swift */; }; 1B3E22DE61BAC810106A7D1A /* GovernanceDelegateSearchProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B1EE1E66B8325C47D8A404 /* GovernanceDelegateSearchProtocols.swift */; }; 1BEADE77C6236CB3BF719A47 /* CrowdloanContributionSetupViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C96E41F878ED0A0A6F469D3 /* CrowdloanContributionSetupViewFactory.swift */; }; @@ -151,6 +303,8 @@ 1C8D1041448B8FB9DD9BBCF1 /* YourWalletsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C5AF7E89A8C6CFF5AE03B1 /* YourWalletsProtocols.swift */; }; 1C9EA26D4E4BA6BAE147B374 /* GovernanceDelegateConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43F25AF006B7B19281AC7B1 /* GovernanceDelegateConfirmWireframe.swift */; }; 1D1DC32EFF13F41677A084B7 /* DAppOperationConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = E675B4C5BE36C0004564105B /* DAppOperationConfirmProtocols.swift */; }; + 1E1B60AC1FBF11673A70955C /* NPoolsUnstakeSetupInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D9C4A550C6DDF85491B8A0 /* NPoolsUnstakeSetupInteractor.swift */; }; + 1ECC51FC47422BF1450E0575 /* StakingSetupAmountInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8145FD81205F06EB6B8B51DA /* StakingSetupAmountInteractor.swift */; }; 1EE4FBB79EE6015D7D3EBDC1 /* ParitySignerTxScanWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F8B46E3BAB48A2D2E1D2EF4 /* ParitySignerTxScanWireframe.swift */; }; 1F205FE059A7099A1A7391EC /* GovernanceRevokeDelegationTracksViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36335FF0C6AEAB65EB46411A /* GovernanceRevokeDelegationTracksViewFactory.swift */; }; 1F45D221E855D5340572C243 /* GovernanceUnavailableTracksProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59BED0E6DBC4C21D9625740C /* GovernanceUnavailableTracksProtocols.swift */; }; @@ -173,6 +327,7 @@ 25993E2E536DE682E1DFC9AD /* ParaStkCollatorsSearchInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841C1EE99F5EA1713BA3F313 /* ParaStkCollatorsSearchInteractor.swift */; }; 25E4B008933E2EF7F2FAAA46 /* StakingMoreOptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2FE7131747C08259EB98AC /* StakingMoreOptionsViewController.swift */; }; 26533668754DB6C1DF2425AB /* TokenManageSingleViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D19485BAFCB2056BDC135441 /* TokenManageSingleViewFactory.swift */; }; + 265C6E00915F2F186551A67B /* NPoolsUnstakeSetupViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3246115B53EFE9461CD2F68B /* NPoolsUnstakeSetupViewFactory.swift */; }; 270C21973CB61F0BF3D2D1E3 /* CrowdloanListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02ACCC85B2CCF3D9392CA9B4 /* CrowdloanListProtocols.swift */; }; 2736BAABAE1389260A0B28D6 /* AssetListViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B754B68D6F1D1ED5C8577A5 /* AssetListViewFactory.swift */; }; 2793D406FD618A892D54EA84 /* CrowdloanContributionConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C92E3ED704CB0BBAB3A669F /* CrowdloanContributionConfirmViewLayout.swift */; }; @@ -222,6 +377,7 @@ 2F21134DE157A4B98ED309E2 /* AssetsSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611FBB25D55CF56F36026074 /* AssetsSearchViewController.swift */; }; 2F2ACE609F7423EDD0F06F30 /* ReferendumSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8D3BC1137332FA3CC1FEF2 /* ReferendumSearchViewController.swift */; }; 2F6FA089995FD12FB2AA814B /* ParitySignerWelcomePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45CB342D1F680257A548CF5 /* ParitySignerWelcomePresenter.swift */; }; + 2F73FA6B6061F343E2F033F0 /* StakingSelectPoolViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E553E3D45A7A759C917A4B2 /* StakingSelectPoolViewFactory.swift */; }; 2F95EEA6CBFDF483124ECF8F /* ParaStkUnstakePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CBA1296E4C6E04EC9C5CA98 /* ParaStkUnstakePresenter.swift */; }; 2FCB062A2D873BD72B795DB3 /* AssetSelectionPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A0A5EE9BE2862B085712A0 /* AssetSelectionPresenter.swift */; }; 3030DB7C067CC906E6794B1A /* StakingRewardFiltersWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81EA338A618A41D4192644E4 /* StakingRewardFiltersWireframe.swift */; }; @@ -233,6 +389,7 @@ 3187CF2169E709F25DFB4C0D /* TokensAddSelectNetworkProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90BA9C7259E59AC6CB422F82 /* TokensAddSelectNetworkProtocols.swift */; }; 319124D11B1376B430B6C2EF /* DAppAuthSettingsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B23BAD22C87DA5F324B44F /* DAppAuthSettingsWireframe.swift */; }; 32009DBB90D19ACD6D7B7A5C /* InAppUpdatesViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F65E56B70C5AF51A365C2BB /* InAppUpdatesViewFactory.swift */; }; + 3201805BF8FA78BDF9DA6328 /* StakingTypePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76E1F9D7AD364D8AD4CC721 /* StakingTypePresenter.swift */; }; 3229E306230161AA99B14BDD /* StakingRewardPayoutsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336395FFC4B2104A9651A2DE /* StakingRewardPayoutsViewFactory.swift */; }; 32428875321BF68F5DC47D52 /* ReferendumsFiltersPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEEF1360E04CD27CCC13E472 /* ReferendumsFiltersPresenter.swift */; }; 3250F2C0E12ED42A355853BE /* SelectValidatorsStartProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = EED9939B17C4224C8E153F8A /* SelectValidatorsStartProtocols.swift */; }; @@ -246,7 +403,6 @@ 3441DDC002503A0DC9A8A925 /* ReferendumSearchViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B179EA3EF793684717BA9D68 /* ReferendumSearchViewFactory.swift */; }; 347BBBBCC84CA155006FDCDB /* GovernanceSelectTracksViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3593D7650F5126266ED9FE84 /* GovernanceSelectTracksViewLayout.swift */; }; 34D6FF85BEA25EFD1D15D460 /* InAppUpdatesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15E09DD01C1CC61EA5CDED9C /* InAppUpdatesInteractor.swift */; }; - 352B75BEB10A48CC6CE64D4A /* Pods_novawalletAll_novawallet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1313BBDD3A28AC9786B5B00E /* Pods_novawalletAll_novawallet.framework */; }; 355476A5AECD2FFE4ED3DE39 /* MessageSheetViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A719A9FC28373296AB195CB /* MessageSheetViewLayout.swift */; }; 3592E885646B3ED9F2717412 /* GovernanceRevokeDelegationTracksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CAF58F7D0659E89B66B75E4 /* GovernanceRevokeDelegationTracksViewController.swift */; }; 35F9157CAA182493B2F0E1D3 /* ParaStkRedeemInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53B5C1C920BFDA8F5C9C89D9 /* ParaStkRedeemInteractor.swift */; }; @@ -256,18 +412,22 @@ 37CF597ACB2A32ABCEEFEE67 /* StakingRebagConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDB5CA76787B1D6EC83F07B /* StakingRebagConfirmViewController.swift */; }; 37E1E9782B9752BC50AF2476 /* YourValidatorListViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE8644A4F6DED808248A0FE /* YourValidatorListViewFactory.swift */; }; 37E229641DCDF64AC5AF1DCD /* DAppBrowserPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FF860B3465854DCBC02DFB3 /* DAppBrowserPresenter.swift */; }; + 37FB290D01ADAA67155C9755 /* StakingSelectPoolPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 060D52F9BFFB646BF9FCC968 /* StakingSelectPoolPresenter.swift */; }; 37FF873F9B2358FA137658D8 /* StakingMoreOptionsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F3F175AFC554763696CB2A /* StakingMoreOptionsPresenter.swift */; }; 384653967D27BD0F39F09255 /* GovernanceUnavailableTracksViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3526D1FB18C2615BE28E15C /* GovernanceUnavailableTracksViewLayout.swift */; }; 38D0977931828C7894579968 /* GovernanceUnlockSetupInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFA54AB88E24A2053F289D74 /* GovernanceUnlockSetupInteractor.swift */; }; 39218CF5AA701518BD3B0103 /* ExportMnemonicInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200C6B2C85846AED8CA9451A /* ExportMnemonicInteractor.swift */; }; 39373DDCEE7CB9D04C310BC9 /* GovernanceRevokeDelegationConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3F5511D2E7AC283FF021E8 /* GovernanceRevokeDelegationConfirmProtocols.swift */; }; 3983EDE80B9296F3A252BA03 /* StakingRewardFiltersViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F5CAA29A0D442151679EA8 /* StakingRewardFiltersViewFactory.swift */; }; + 39C1255EC6C5C7AC14680608 /* NPoolsUnstakeConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 397F057FD5B16A58E5F30F07 /* NPoolsUnstakeConfirmWireframe.swift */; }; 3A4743C7C74BE4F74F6390F6 /* MarkdownDescriptionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D087F5710630FCC968B65FB5 /* MarkdownDescriptionViewLayout.swift */; }; 3AD7635AFA1F7E66A3C00F56 /* ParitySignerAddressesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF715CEF29477B59119520F1 /* ParitySignerAddressesInteractor.swift */; }; + 3B1D2A0FCDDF1AAC32BFEE58 /* StakingSetupAmountWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71030ADF84393172807E434 /* StakingSetupAmountWireframe.swift */; }; 3B6F50061AD9FC31D6712D9F /* ParaStkCollatorsSearchProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CFD9A58CDDA60D9E1204078 /* ParaStkCollatorsSearchProtocols.swift */; }; 3B7EEC888C19F954B5EB1012 /* OnChainTransferSetupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9871C4FF3B05F6055AF82F14 /* OnChainTransferSetupWireframe.swift */; }; 3B87871B471FF8BA84DC7910 /* ParaStkYieldBoostStopWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 285212895DBAB0098F302DF9 /* ParaStkYieldBoostStopWireframe.swift */; }; 3BFD635E852E4D395025BEE8 /* ParaStkCollatorsSearchViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C602661DE4D6CAC482AF721 /* ParaStkCollatorsSearchViewFactory.swift */; }; + 3C3C98149DA3BDE3CE692F3C /* NominationPoolBondMoreConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA576BBCED647C22E93F0202 /* NominationPoolBondMoreConfirmWireframe.swift */; }; 3C6C738F4AB7AC6FEA290D59 /* WalletsChoosePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 525813EB768E636A397C00BB /* WalletsChoosePresenter.swift */; }; 3CA86739CB09801714B194BD /* PurchaseWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C52C6CD7112BF0E1E3A98CE /* PurchaseWireframe.swift */; }; 3CBE411F2E3CC40A0BA2572F /* StakingDashboardViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFCA1A3205F91128556F0F11 /* StakingDashboardViewLayout.swift */; }; @@ -294,11 +454,14 @@ 41B29C1C9239BB2DCB7903A7 /* SelectValidatorsStartViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4C391292F22D16427C77CD9 /* SelectValidatorsStartViewFactory.swift */; }; 41DE96F778AE909978775438 /* ParitySignerTxQrViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A76BDAB14EEA1C4E23B884E /* ParitySignerTxQrViewController.swift */; }; 41FA237A4AA56AC99322A040 /* ParitySignerAddConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E8ABCF1EB5064F40B697CC /* ParitySignerAddConfirmPresenter.swift */; }; + 4224A32768A37D48C7E599E7 /* NPoolsUnstakeConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080DF5C2C6DEC79D8324F084 /* NPoolsUnstakeConfirmInteractor.swift */; }; 42AF4E3B592AF7E40CAC13E0 /* TransactionHistoryWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB1A1934B76A5DCC65855EE1 /* TransactionHistoryWireframe.swift */; }; 42B79A8D0D9540C1D97D991C /* AccountConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 599FEDBD7E8B665F1A93BA70 /* AccountConfirmWireframe.swift */; }; 433A3C2B0D1E4BA5974D681B /* DAppAddFavoriteWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACF32611D345B87BCE29FE0 /* DAppAddFavoriteWireframe.swift */; }; 4346F9B838284C40D4F1058B /* CommonDelegationTracksProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627CE71CD8C7CE59B8AFBAD6 /* CommonDelegationTracksProtocols.swift */; }; 4387FBFF6D4EFF2E6F3A1A5A /* ParaStkYieldBoostStartViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE30B597680295FFB1B7220C /* ParaStkYieldBoostStartViewFactory.swift */; }; + 43914487914F1EAA9800D303 /* NominationPoolBondMoreConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ADA652B86975A2044ABB065 /* NominationPoolBondMoreConfirmViewController.swift */; }; + 43D58563868FA362F47B7D92 /* NPoolsClaimRewardsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E42BA40ACDEA2914DE6435 /* NPoolsClaimRewardsViewFactory.swift */; }; 43DB2CC9864CC7F5904A2DBC /* AdvancedWalletViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B243F6751E2277D9FC14481 /* AdvancedWalletViewFactory.swift */; }; 441FFD82C502D7300B79EE66 /* ReferendumVoteConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE767858B6CF5F6F7C7B418E /* ReferendumVoteConfirmProtocols.swift */; }; 4448B591D4A193DBC9E2E3BF /* AccountCreateInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A7B39A61DB0D2F0F1B1DBA1 /* AccountCreateInteractor.swift */; }; @@ -308,7 +471,9 @@ 454D41CC5C7CC2FDAB778026 /* CreateWatchOnlyInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D9C85AB0C9D53B522DCF3C5 /* CreateWatchOnlyInteractor.swift */; }; 46298240F3528B5C62AEC29E /* GovernanceUnlockSetupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 856BF961EACEB9703B2B37C7 /* GovernanceUnlockSetupWireframe.swift */; }; 473EDA64B1A18BA80189142D /* GovernanceRemoveVotesConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BB15F2A86E47DE30AE8107 /* GovernanceRemoveVotesConfirmProtocols.swift */; }; + 478D954AF83399843A2FAA8A /* NominationPoolBondMoreBasePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA85EE628D7029AC940DFA3 /* NominationPoolBondMoreBasePresenter.swift */; }; 47FA7B2E0D9A87E694DA9217 /* LedgerAccountConfirmationProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E4D8E59F0976D412FF0B10 /* LedgerAccountConfirmationProtocols.swift */; }; + 4825E3478A67C504E7E03936 /* Pods_novawalletTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D931049E1775C8D9C4EEC9D /* Pods_novawalletTests.framework */; }; 487A912B697604FE3367FAEC /* CrowdloanYourContributionsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDF20DCECDEA61E1BDE780B /* CrowdloanYourContributionsViewLayout.swift */; }; 4883346B78B82D2037DEBA56 /* GovernanceRemoveVotesConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE851B2BABA1043E73D12F73 /* GovernanceRemoveVotesConfirmViewLayout.swift */; }; 488E4467895040EA85FDCC79 /* ReferendumDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B727587201B9D6F91A28428A /* ReferendumDetailsViewController.swift */; }; @@ -337,11 +502,13 @@ 506F0D372BCC8302E513637C /* CrowdloanContributionConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA59CE2C7AE548ACA9D66FD7 /* CrowdloanContributionConfirmWireframe.swift */; }; 50758C9BBB27AE5732FF78BA /* StakingRewardPayoutsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFEBC03AB1841681427D38AF /* StakingRewardPayoutsViewController.swift */; }; 507AFE76D2D4EF2F739AE799 /* GovernanceDelegateInfoViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20E4FF68D8A9AD54E4F089BC /* GovernanceDelegateInfoViewLayout.swift */; }; + 5103B0A6919721E7E1284829 /* NPoolsUnstakeSetupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA6F8EC6245BA34F26AE276 /* NPoolsUnstakeSetupWireframe.swift */; }; 5188FF070CD05F92C93A5055 /* CreateWatchOnlyProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537CCA1F2667A51731C56C88 /* CreateWatchOnlyProtocols.swift */; }; 51FB88BCF778805D142DD8A9 /* WalletConnectProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60F6D1F16B35354844E435AC /* WalletConnectProtocols.swift */; }; 51FC48FA6FD4D2FB1781424D /* ReferralCrowdloanWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D51D60F19284936A6E9F47D /* ReferralCrowdloanWireframe.swift */; }; 52326E49B049C54434C95132 /* ParaStkCollatorsSearchWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53C2612BBF75CF0FBD91764E /* ParaStkCollatorsSearchWireframe.swift */; }; 529B87AC9E500CC2A503A859 /* LedgerInstructionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BF275FC06E8B06FA4D4719 /* LedgerInstructionsViewController.swift */; }; + 52C427F2C572B99D63EB9C21 /* StartStakingInfoBasePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E4D3F275296975CA39280D5 /* StartStakingInfoBasePresenter.swift */; }; 535E9CD08FCA2DA52D37A134 /* ParitySignerTxQrWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BA14E9659FC44A438FFEEB5 /* ParitySignerTxQrWireframe.swift */; }; 54110818BE6218276B4C55AF /* AssetsSettingsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC15D0B7B9F29E97FCECC1D2 /* AssetsSettingsViewLayout.swift */; }; 541C2290F9E2D4D11270F39A /* TokensAddSelectNetworkPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E3BDBC4DFACF9422ED6B5A /* TokensAddSelectNetworkPresenter.swift */; }; @@ -363,34 +530,42 @@ 58F385F41D42CC96373EDA42 /* TokensManageProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05AA59AFC801F52B79DDBBCF /* TokensManageProtocols.swift */; }; 58F693958EF69F59D7C9760E /* StakingRewardPayoutsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4191E0055768541F6A3D8A61 /* StakingRewardPayoutsInteractor.swift */; }; 590717389ECBA34FA08F247B /* GovernanceRemoveVotesConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5EB48ADB08405BF51F087D /* GovernanceRemoveVotesConfirmWireframe.swift */; }; + 594EA36463252924AB73475B /* NPoolsUnstakeSetupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2899566C339BA9562BE61F4F /* NPoolsUnstakeSetupPresenter.swift */; }; 5951458470EB3BEBC07FFE90 /* ReferendumsFiltersViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83D189355A12F42CBAAFF238 /* ReferendumsFiltersViewLayout.swift */; }; 59745D3C9602745E1417D2F6 /* AssetSelectionInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83AB0AD3A7CECD061611F60C /* AssetSelectionInteractor.swift */; }; 599A81A7D18BB70B6C650393 /* DAppAuthConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48994B2FA6FEE45A718252C /* DAppAuthConfirmViewFactory.swift */; }; 599CEA840B4E888B643AEB5E /* AssetsSettingsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FF46AB59967BF656E9EF1C /* AssetsSettingsViewFactory.swift */; }; 59A0AF440ABAAA459EF7D993 /* GovernanceYourDelegationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4462DCD832DB73AA78D44C /* GovernanceYourDelegationsViewController.swift */; }; + 59D03D8CB4AE60DE53130729 /* NPoolsRedeemViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EF6FA61F2B7E3B2ADD3200 /* NPoolsRedeemViewLayout.swift */; }; + 5AC2A8AD94278DFA4B68A718 /* NominationPoolSearchInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44AA632DE49B746BC38B959F /* NominationPoolSearchInteractor.swift */; }; 5B54978244C37502DD592486 /* NftListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A092ADC09DA0429548EBC08 /* NftListPresenter.swift */; }; 5B652F1E0040F68F835A2F1D /* AssetDetailsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEE05ECCFE3DD11A2EAAF495 /* AssetDetailsViewLayout.swift */; }; 5C796EF8ED29F564B5D1126B /* CrowdloanContributionConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F75722D2F921FD1C2D4105D /* CrowdloanContributionConfirmViewController.swift */; }; 5D532A958C5C961391177C4A /* DelegationListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29BF0B2686329E392C5DFB19 /* DelegationListProtocols.swift */; }; + 5D71EC71B3ED00D130C5985F /* StakingTypeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663DD0E9F1F758C06E7EC829 /* StakingTypeInteractor.swift */; }; 5DDD2206DF795CF205610455 /* AccountExportPasswordPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31226053044986BC828AA912 /* AccountExportPasswordPresenter.swift */; }; 5E34CFB3CAE24366E1A24B51 /* GovernanceDelegateConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19BFAC4CDCC826720CA1010A /* GovernanceDelegateConfirmViewLayout.swift */; }; 5E3B1E6B9E94848B186FD4D1 /* ReferendumDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AC65A04352405327BFE946 /* ReferendumDetailsInteractor.swift */; }; 5E621A350A6DDD78597CC9E5 /* CrowdloanYourContributionsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38A4CCD27081CF017AFCD18 /* CrowdloanYourContributionsWireframe.swift */; }; 5E6D69D84220119BA5362358 /* OperationDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F2670E15D23CF30931389E3 /* OperationDetailsProtocols.swift */; }; + 5E6F7AC179BE7C1FA1759270 /* StakingSetupAmountViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD71DD9CFF2B4BECCDEAF8C0 /* StakingSetupAmountViewFactory.swift */; }; 5ECC5F9E55FBC5CF2DD8664C /* GovernanceDelegateInfoWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C16637ADA10892106DD304 /* GovernanceDelegateInfoWireframe.swift */; }; 5F107F52BCEF8BA940800F88 /* AssetDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A479F3338BE7DA2C846023FA /* AssetDetailsViewController.swift */; }; 5FD7B3463822BC69AF5E3C72 /* ParaStkUnstakeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F711415F7C805D43E83644C6 /* ParaStkUnstakeViewController.swift */; }; 5FE687B860FC10AB08518A6E /* WalletHistoryFilterPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB9150FEC66FC503CF1BD1D0 /* WalletHistoryFilterPresenter.swift */; }; + 5FF13D27B596CD7B0CA20671 /* NominationPoolBondMoreSetupViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF2717F0CC0A4529C925814 /* NominationPoolBondMoreSetupViewLayout.swift */; }; 6003DF3EBB77510EFB70B4E4 /* MessageSheetProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C797A5B5863A026E84062AE /* MessageSheetProtocols.swift */; }; 60461B20A5DA9E9E3AF0BB84 /* WalletHistoryFilterWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78670B0926E92B75088D2D7B /* WalletHistoryFilterWireframe.swift */; }; + 60808D290AE02E3A284EC3E9 /* StartStakingConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71040ECB206855280257D4F2 /* StartStakingConfirmPresenter.swift */; }; 60B66AA63089FC2A3A701CF2 /* GovernanceSelectTracksViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6A61CD43EDD8791F6DA9581 /* GovernanceSelectTracksViewFactory.swift */; }; 60DBAD14156415802730C7D7 /* TokensManageViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36D2D8238F708E5D7D141A37 /* TokensManageViewFactory.swift */; }; 60FFEE5B386E82D70333BE80 /* CreateWatchOnlyPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3749525FED4CA4CD0DCDF5 /* CreateWatchOnlyPresenter.swift */; }; - 61B9688494251703A6373A1B /* StakingAmountWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6216F6F1B91F798F07695FB6 /* StakingAmountWireframe.swift */; }; 61E0DC83C1D60D677274D7CE /* AccountExportPasswordViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11575D8B4F64C2E805372A5 /* AccountExportPasswordViewFactory.swift */; }; + 621E843DCEA85A00B419926F /* StakingSelectPoolViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7F38FDB907F69A26B93B4E6 /* StakingSelectPoolViewController.swift */; }; 623474C49445578F030291B0 /* ParaStkStakeSetupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0B2C32E11E2F7F3D4A1D3AB /* ParaStkStakeSetupWireframe.swift */; }; 62649D3FB6AACB508872C67A /* GovernanceUnlockConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9638E6EDBA41A5772E0033AE /* GovernanceUnlockConfirmInteractor.swift */; }; 62B2298F132DB0CE0794DD7A /* MarkdownDescriptionWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E6825E525785A1A12C62E7 /* MarkdownDescriptionWireframe.swift */; }; + 62D28F231DED3F39B2C53F1F /* StakingSetupAmountProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73F89021BEE1F4576128305 /* StakingSetupAmountProtocols.swift */; }; 63185C6D67EAEB2867069AB9 /* ParitySignerWelcomeProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CBB8745F8C36BB107625E8F /* ParitySignerWelcomeProtocols.swift */; }; 640A79BD1335394818E70366 /* WalletHistoryFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6884DFC1AA1B995C21C274C /* WalletHistoryFilterViewController.swift */; }; 641D7CF89F37B1890516015E /* ParitySignerTxScanProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F10F130391C4B3652FE8F59 /* ParitySignerTxScanProtocols.swift */; }; @@ -422,6 +597,7 @@ 6BAF97802DB9C640515F47C7 /* StakingMainInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 003FA37F2B240C5D7605340D /* StakingMainInteractor.swift */; }; 6BBD025775841F8B055CA367 /* AssetsSearchViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475B5A735FE818842CB3C82C /* AssetsSearchViewFactory.swift */; }; 6C56AB4AE63AB2DC73DE98E0 /* AccountImportInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CC1FB277A878E9C9B7EAEB /* AccountImportInteractor.swift */; }; + 6C56D43878A36E0AB7451DF6 /* NominationPoolSearchProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31B8F292DA050D1D19B9F5F /* NominationPoolSearchProtocols.swift */; }; 6D315EFF2B664235D297674E /* AccountImportProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B29514E516CEAAB159851D95 /* AccountImportProtocols.swift */; }; 6D47EAB127FAB7559A9FA107 /* StakingPayoutConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BACB7E24BC87F9218DBBC4 /* StakingPayoutConfirmationViewController.swift */; }; 6D5851FB5F830D55EFDB8B7D /* StakingUnbondSetupProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23A74BDB54D503FA2BFBEF35 /* StakingUnbondSetupProtocols.swift */; }; @@ -431,6 +607,7 @@ 6D6C6FD2F13603BCE83CFC65 /* ExportMnemonicConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC566D6EACB81469B926611 /* ExportMnemonicConfirmInteractor.swift */; }; 6DC454C4BA27C98987F5DC52 /* WalletConnectSessionsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC34A44F63EFAE6E804BDE9 /* WalletConnectSessionsViewFactory.swift */; }; 6DEA5344FAD4C6A6E7CA989C /* AdvancedWalletViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A473EB36D61D5DF3BC69F7B9 /* AdvancedWalletViewLayout.swift */; }; + 6E58D665BB280CD332DC9F5E /* NPoolsRedeemProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6FC364054546921FA6A5D2B /* NPoolsRedeemProtocols.swift */; }; 6E873BD1428F104C4292FF58 /* ChainAddressDetailsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E1A4406DA1DCAA35F11C8F1 /* ChainAddressDetailsViewLayout.swift */; }; 6ECB27B386124F87382073FD /* DAppAddFavoriteProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1A00299D9B50045E1A1983 /* DAppAddFavoriteProtocols.swift */; }; 6ECD0116CD39D8F55D246864 /* SelectValidatorsConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B5626189788682A84D4E9D7 /* SelectValidatorsConfirmPresenter.swift */; }; @@ -442,6 +619,7 @@ 70C0E48EE41B4C7229F5946C /* DAppBrowserViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5278A5F4178922A240590334 /* DAppBrowserViewLayout.swift */; }; 71533ED31DD45841CA8296A3 /* ReferendumVoteConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD1CAB4467C76E4139ECB1B7 /* ReferendumVoteConfirmViewFactory.swift */; }; 716F0819BAB14322E34E416C /* CrowdloanYourContributionsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF4ED0EE2A3EF620DC51870B /* CrowdloanYourContributionsPresenter.swift */; }; + 71BA10F56250CF7F7418CDAB /* StakingTypeProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50ACC876575577A9D477A77 /* StakingTypeProtocols.swift */; }; 722C694F93836FAFEA0DBD93 /* DAppSettingsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6594A70C41C42049D7FAC692 /* DAppSettingsViewLayout.swift */; }; 72EA1D180E99C6C78B87B820 /* LedgerInstructionsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6747B9F68F9E92845122D8D2 /* LedgerInstructionsViewLayout.swift */; }; 72EF67BA5380D1CDBB73E23F /* ParaStkYieldBoostStartViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6475F9C6C6B095B9C5026CE9 /* ParaStkYieldBoostStartViewController.swift */; }; @@ -459,17 +637,35 @@ 766FE2FAB8509BF0F56EA3C0 /* ParaStkCollatorInfoProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F3B8502E5BF8CDD7ACE2DD0 /* ParaStkCollatorInfoProtocols.swift */; }; 76B0B7147181747A7CEDDDF6 /* GovernanceUnavailableTracksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E25CF67173500E0AC19387 /* GovernanceUnavailableTracksViewController.swift */; }; 76CF8508C6936FC9941F3C3E /* TokensManageAddProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C995D640129977CAB05982EC /* TokensManageAddProtocols.swift */; }; + 770F57882A8A2CE0005FD7C1 /* StakingSelectPoolViewStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770F57872A8A2CE0005FD7C1 /* StakingSelectPoolViewStyles.swift */; }; + 770F578B2A8A48FF005FD7C1 /* ButtonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 770F578A2A8A48FF005FD7C1 /* ButtonViewModel.swift */; }; + 77171CA82A98BBA10032B387 /* NominationPoolErrorPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77171CA72A98BBA10032B387 /* NominationPoolErrorPresentable.swift */; }; + 77171CAA2A98BC420032B387 /* NominationPoolDataValidatorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77171CA92A98BC420032B387 /* NominationPoolDataValidatorFactory.swift */; }; 77204EA42A1CD59100BBDE4A /* GenericBorderedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */; }; 77204EA62A1E0EAA00BBDE4A /* WalletConnectionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77204EA52A1E0EAA00BBDE4A /* WalletConnectionsView.swift */; }; 7725062C2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7725062B2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift */; }; + 7726CD552A9728D700CE9064 /* StakingTypeSelectedStakingViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7726CD542A9728D700CE9064 /* StakingTypeSelectedStakingViewModelFactory.swift */; }; 7728E58B2A123AEE007901E0 /* ReferendumsSearchOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7728E58A2A123AEE007901E0 /* ReferendumsSearchOperationFactory.swift */; }; 7728E58D2A123B42007901E0 /* SearchOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7728E58C2A123B42007901E0 /* SearchOperationFactory.swift */; }; 7728E58F2A123B70007901E0 /* ReferendumsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7728E58E2A123B70007901E0 /* ReferendumsState.swift */; }; 7728E5912A1324A2007901E0 /* ReferendumsSearchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7728E5902A1324A2007901E0 /* ReferendumsSearchManager.swift */; }; - 7728E5932A13290D007901E0 /* GenericLens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7728E5922A13290D007901E0 /* GenericLens.swift */; }; + 772B1C7D2A93837800A19061 /* StartStakingCustomValidatorListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 772B1C7C2A93837800A19061 /* StartStakingCustomValidatorListWireframe.swift */; }; 7731E9C42A14DA3F0085B5FF /* BorderedActionControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7731E9C32A14DA3F0085B5FF /* BorderedActionControlView.swift */; }; + 7738FB6A2A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7738FB692A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift */; }; 774A481129F8BFB70094635B /* OperationAuthPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 774A481029F8BFB70094635B /* OperationAuthPresentable.swift */; }; 7756927D2A20B88200220756 /* TokenOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7756927C2A20B88200220756 /* TokenOperation.swift */; }; + 775F194D2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775F194C2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift */; }; + 775F19512A5811FA009915B6 /* StartStakingParachainInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775F19502A5811FA009915B6 /* StartStakingParachainInteractor.swift */; }; + 775F19532A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 775F19522A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift */; }; + 77799ADD2A74219A00B7E564 /* ButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799ADC2A74219A00B7E564 /* ButtonState.swift */; }; + 77799ADF2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799ADE2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift */; }; + 77799AE52A792AE700B7E564 /* StakingTypeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799AE42A792AE700B7E564 /* StakingTypeViewModel.swift */; }; + 77799AE72A792B6F00B7E564 /* StakingTypeAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799AE62A792B6F00B7E564 /* StakingTypeAccountViewModel.swift */; }; + 77799AE92A7C99D200B7E564 /* StakingTypeViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799AE82A7C99D200B7E564 /* StakingTypeViewModelFactory.swift */; }; + 77799AEC2A7CFB5700B7E564 /* PoolStakingTypeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799AEB2A7CFB5700B7E564 /* PoolStakingTypeViewModel.swift */; }; + 77799AEE2A7CFB6A00B7E564 /* DirectStakingTypeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799AED2A7CFB6A00B7E564 /* DirectStakingTypeViewModel.swift */; }; + 77799AF02A7CFB7C00B7E564 /* ValidatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799AEF2A7CFB7C00B7E564 /* ValidatorViewModel.swift */; }; + 77799AF22A7CFB8D00B7E564 /* PoolAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77799AF12A7CFB8D00B7E564 /* PoolAccountViewModel.swift */; }; 777BD86029F9730F004969A2 /* ReferendumsFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 777BD85F29F9730F004969A2 /* ReferendumsFilterViewModel.swift */; }; 777BD86229F97322004969A2 /* ReferendumsFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 777BD86129F97322004969A2 /* ReferendumsFilter.swift */; }; 777BD86429F979DA004969A2 /* SelectableFilterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 777BD86329F979DA004969A2 /* SelectableFilterCell.swift */; }; @@ -477,6 +673,10 @@ 777BD86829FA3376004969A2 /* ReferendumsSettingsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 777BD86729FA3376004969A2 /* ReferendumsSettingsCell.swift */; }; 777BD86A29FA3D5E004969A2 /* ReferendumsFilter+match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 777BD86929FA3D5E004969A2 /* ReferendumsFilter+match.swift */; }; 778210862A6588D100256E78 /* DiffableDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778210852A6588D100256E78 /* DiffableDataStore.swift */; }; + 77864F4C2A6AC5A100FA7BA7 /* TitleHorizontalMultiValueView+Bind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77864F4B2A6AC5A100FA7BA7 /* TitleHorizontalMultiValueView+Bind.swift */; }; + 77895C9F2A8F5D40006870FB /* NominationPoolSearchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77895C9E2A8F5D40006870FB /* NominationPoolSearchManager.swift */; }; + 77895CA12A8F7360006870FB /* NominationPoolSearchOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77895CA02A8F7360006870FB /* NominationPoolSearchOperationFactory.swift */; }; + 77895CA32A8F8CFD006870FB /* NominationPoolsFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77895CA22A8F8CFD006870FB /* NominationPoolsFilters.swift */; }; 778D979B2A24D1D8002BA681 /* BaseAssetsSearchViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778D979A2A24D1D8002BA681 /* BaseAssetsSearchViewLayout.swift */; }; 778D979D2A24D21B002BA681 /* AssetsOperationViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778D979C2A24D21B002BA681 /* AssetsOperationViewLayout.swift */; }; 778D979F2A24D248002BA681 /* SearchViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778D979E2A24D248002BA681 /* SearchViewProtocol.swift */; }; @@ -490,6 +690,7 @@ 7796C7092A17D24A00D56094 /* UILayoutPriority+Custom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7796C7082A17D24A00D56094 /* UILayoutPriority+Custom.swift */; }; 779A8F992A04BAC000BE31B3 /* StakingRewardActionControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779A8F982A04BAC000BE31B3 /* StakingRewardActionControl.swift */; }; 779A8F9B2A050C4400BE31B3 /* StakingRewardDateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779A8F9A2A050C4400BE31B3 /* StakingRewardDateCell.swift */; }; + 779C8BE82AA1DD1B001A4A3C /* NominationPoolsBondMoreHintsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779C8BE72AA1DD1B001A4A3C /* NominationPoolsBondMoreHintsFactory.swift */; }; 77A0B2ED2A3B7F3300CBF653 /* DAppCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A0B2EC2A3B7F3300CBF653 /* DAppCollectionViewCell.swift */; }; 77A0B2EF2A3B85B700CBF653 /* BlurBackgroundCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A0B2EE2A3B85B700CBF653 /* BlurBackgroundCollectionReusableView.swift */; }; 77A0B2F32A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A0B2F22A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift */; }; @@ -508,9 +709,15 @@ 77A6F5CF2A31C4D4004AFD1A /* Web3TransferRecipientRepositoryFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5CE2A31C4D4004AFD1A /* Web3TransferRecipientRepositoryFactory.swift */; }; 77A6F5D22A31DB8C004AFD1A /* JsonCanonicalizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5D12A31DB8C004AFD1A /* JsonCanonicalizer.swift */; }; 77A6F5D52A31E046004AFD1A /* JsonCanonicalizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A6F5D42A31E046004AFD1A /* JsonCanonicalizerTests.swift */; }; + 77AB55592AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */; }; + 77AB555B2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */; }; + 77AB555D2AA24BA90058814E /* OperationDetailsPoolRewardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */; }; 77CB33CE2A38780700B6709A /* structures_output.json in Resources */ = {isa = PBXBuildFile; fileRef = 77CB33CD2A38780700B6709A /* structures_output.json */; }; 77CB33D22A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CB33D12A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift */; }; 77CB33D72A3998FD00B6709A /* Array+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CB33D62A3998FC00B6709A /* Array+Sort.swift */; }; + 77CC82A32A984BC3002D022F /* StartStakingSelectedValidatorsListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CC82A22A984BC3002D022F /* StartStakingSelectedValidatorsListWireframe.swift */; }; + 77CC82A52A984EDA002D022F /* UINavigaionController+Pop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CC82A42A984EDA002D022F /* UINavigaionController+Pop.swift */; }; + 77CC82A72A986CF1002D022F /* StakingSelectValidatorsDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77CC82A62A986CF1002D022F /* StakingSelectValidatorsDelegate.swift */; }; 77E0DC9E2A6940C400D03724 /* Calendar+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E0DC9D2A6940C400D03724 /* Calendar+Helpers.swift */; }; 77E255672A16145500B644C3 /* StakingRewardsFilterMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E255662A16145500B644C3 /* StakingRewardsFilterMapper.swift */; }; 77E255692A16148000B644C3 /* StakingRewardsFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E255682A16148000B644C3 /* StakingRewardsFilter.swift */; }; @@ -525,6 +732,32 @@ 77ED167C2A0CF42E00E1FC8C /* StakingRewardFiltersPeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77ED167B2A0CF42E00E1FC8C /* StakingRewardFiltersPeriod.swift */; }; 77ED167E2A0D0AE900E1FC8C /* Lenses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77ED167D2A0D0AE900E1FC8C /* Lenses.swift */; }; 77ED16802A0D6E9A00E1FC8C /* TableViewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77ED167F2A0D6E9A00E1FC8C /* TableViewModels.swift */; }; + 77EFFC8A2A6E7A24009E28F8 /* AccountExistense.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EFFC882A6E7A24009E28F8 /* AccountExistense.swift */; }; + 77EFFC8D2A6EECFD009E28F8 /* StakingAmountViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EFFC8C2A6EECFD009E28F8 /* StakingAmountViewModelFactory.swift */; }; + 77EFFC8F2A714C21009E28F8 /* StakingTypeBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EFFC8E2A714C21009E28F8 /* StakingTypeBannerView.swift */; }; + 77EFFC912A7276F1009E28F8 /* StakingTypeAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EFFC902A7276F1009E28F8 /* StakingTypeAccountView.swift */; }; + 77EFFC932A72A288009E28F8 /* StakingTypeBaseBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77EFFC922A72A288009E28F8 /* StakingTypeBaseBannerView.swift */; }; + 77F033932A814296006BC67E /* GenericStakingTypeAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F033922A814296006BC67E /* GenericStakingTypeAccountView.swift */; }; + 77F033952A8142B0006BC67E /* StakingTypeValidatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F033942A8142B0006BC67E /* StakingTypeValidatorView.swift */; }; + 77F033972A8142D1006BC67E /* StakingSetupAmountStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F033962A8142D1006BC67E /* StakingSetupAmountStyles.swift */; }; + 77F033992A8142E5006BC67E /* DirectStakingTypeAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F033982A8142E5006BC67E /* DirectStakingTypeAccountViewModel.swift */; }; + 77F0339B2A814505006BC67E /* StakingSelectionMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F0339A2A814505006BC67E /* StakingSelectionMethod.swift */; }; + 77F0339D2A837AB3006BC67E /* StakingTypeSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F0339C2A837AB3006BC67E /* StakingTypeSelection.swift */; }; + 77F033A22A84E00F006BC67E /* StakingPoolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F033A12A84E00F006BC67E /* StakingPoolView.swift */; }; + 77F033A42A84E028006BC67E /* StakingSelectPoolListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F033A32A84E028006BC67E /* StakingSelectPoolListHeaderView.swift */; }; + 77F033A62A84EAC3006BC67E /* StakingPoolTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F033A52A84EAC3006BC67E /* StakingPoolTableViewCell.swift */; }; + 77F033A82A86136D006BC67E /* StakingSelectPoolViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F033A72A86136D006BC67E /* StakingSelectPoolViewModelFactory.swift */; }; + 77F085A407EDCCF906FD6E22 /* StartStakingInfoWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D65686560E2E6C18A5C34CB /* StartStakingInfoWireframe.swift */; }; + 77F1893E2A4996FC00E8B933 /* ParagraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F1893D2A4996FC00E8B933 /* ParagraphView.swift */; }; + 77F189402A49972300E8B933 /* UILabel+bind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F1893F2A49972300E8B933 /* UILabel+bind.swift */; }; + 77F189442A49974A00E8B933 /* UITextView+bind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F189432A49974A00E8B933 /* UITextView+bind.swift */; }; + 77F189472A49BD6700E8B933 /* StartStakingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F189462A49BD6700E8B933 /* StartStakingViewModel.swift */; }; + 77F189492A4A299800E8B933 /* StartStakingViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F189482A4A299800E8B933 /* StartStakingViewModelFactory.swift */; }; + 77F9FB072A9D96E900820625 /* NominationPoolBondMoreSetupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F9FB062A9D96E900820625 /* NominationPoolBondMoreSetupPresenter.swift */; }; + 77F9FB092A9D971000820625 /* NominationPoolBondMoreSetupInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F9FB082A9D971000820625 /* NominationPoolBondMoreSetupInteractor.swift */; }; + 77F9FB0B2A9D97A100820625 /* NominationPoolBondMoreSetupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F9FB0A2A9D97A100820625 /* NominationPoolBondMoreSetupWireframe.swift */; }; + 77F9FB0D2A9D9C5600820625 /* NominationPoolBondMoreBaseProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F9FB0C2A9D9C5600820625 /* NominationPoolBondMoreBaseProtocols.swift */; }; + 78D63EC7EC5F7427335A025E /* NPoolsClaimRewardsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A7302440137F083F7AEC64E /* NPoolsClaimRewardsViewLayout.swift */; }; 78D94A761EFECED60F38232D /* CustomValidatorListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 270B309EC85D8897A4ADD98A /* CustomValidatorListViewController.swift */; }; 78E0B6963A8D0A07E742232C /* GovernanceEditDelegationTracksWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3B84FA1F22CC12B16C79AE /* GovernanceEditDelegationTracksWireframe.swift */; }; 790129F3CB6AEA611639E886 /* ParaStkUnstakeViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5467B9B6AEDB33F565D130A1 /* ParaStkUnstakeViewFactory.swift */; }; @@ -551,7 +784,6 @@ 800FCAF66DC8A24020D16A9C /* AccountExportPasswordInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194C9BFEE9BA8C9E448D79AA /* AccountExportPasswordInteractor.swift */; }; 80175BD9EE66BCE4016E7F28 /* GovernanceDelegateConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA3C305F49A4E609C7A5C14 /* GovernanceDelegateConfirmViewController.swift */; }; 8027EA456C0C13F6DA73D540 /* MoonbeamTermsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837E9D1F8096A0E9CA0E0CEB /* MoonbeamTermsPresenter.swift */; }; - 80E265DD62D96597E4EAA44A /* Pods_novawalletTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D79EAD799CBB1ABB9541A232 /* Pods_novawalletTests.framework */; }; 811096BAAA6BD237DF2769EA /* ReferendumVoteSetupViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF755EE09598254BB5E59CC2 /* ReferendumVoteSetupViewFactory.swift */; }; 81544BD01F6AD0197588D3C5 /* OperationDetailsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC6917929E4A752B79FE554 /* OperationDetailsWireframe.swift */; }; 81ADC94E1CC47A2C6F0F1BEA /* GovernanceEditDelegationTracksProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F32F1F0F6F985195CD19EDDB /* GovernanceEditDelegationTracksProtocols.swift */; }; @@ -562,6 +794,7 @@ 8329367F06C83BA7A0B12A34 /* CommonDelegationTracksPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7235097E09C94005B091B4 /* CommonDelegationTracksPresenter.swift */; }; 832B616B89972C96D98023DB /* ChangeWatchOnlyViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54289A8A9354D5DDA15F0E1 /* ChangeWatchOnlyViewFactory.swift */; }; 835AE15B1F58CB87F775272F /* DelegateVotedReferendaPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5544826542EC3A92FCB2B1D7 /* DelegateVotedReferendaPresenter.swift */; }; + 838D584B803A5A7BCBAD9395 /* StartStakingConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28FAB72B50D8C736AF67A39 /* StartStakingConfirmProtocols.swift */; }; 83A98B972B3EA69B357E5002 /* ControllerAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B556386CEEB76C95ED59897 /* ControllerAccountViewController.swift */; }; 83DCE6BEEAD957D9C7588DB5 /* ParitySignerTxScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 817E84903F4A2CD5333518DE /* ParitySignerTxScanViewController.swift */; }; 84002A972990491A00A80672 /* GovernanceRemoveVotesInteractorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84002A962990491A00A80672 /* GovernanceRemoveVotesInteractorError.swift */; }; @@ -751,7 +984,6 @@ 841E5563282E9EF400C8438F /* ParachainStakingCommonData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E5562282E9EF400C8438F /* ParachainStakingCommonData.swift */; }; 841E5565282EA76C00C8438F /* ParachainStakingBaseState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E5564282EA76C00C8438F /* ParachainStakingBaseState.swift */; }; 841E5567282EAC1000C8438F /* ParachainStakingInitState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E5566282EAC1000C8438F /* ParachainStakingInitState.swift */; }; - 841E5569282EAC2600C8438F /* ParachainStakingNoStakingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E5568282EAC2600C8438F /* ParachainStakingNoStakingState.swift */; }; 841E556B282EAC3600C8438F /* ParachainStakingDelegatorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E556A282EAC3600C8438F /* ParachainStakingDelegatorState.swift */; }; 841E556D282EC50700C8438F /* ParaStkStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E556C282EC50700C8438F /* ParaStkStateMachine.swift */; }; 841E6AF625EC12100007DDFE /* PreparedNomination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E6AF525EC12100007DDFE /* PreparedNomination.swift */; }; @@ -859,14 +1091,12 @@ 842898D1265A955A002D5D65 /* ImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842898D0265A955A002D5D65 /* ImageViewModel.swift */; }; 842A735E27DB2EC4006EE1EA /* OperationDetailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842A735D27DB2EC4006EE1EA /* OperationDetailsModel.swift */; }; 842A736227DB3032006EE1EA /* OperationExtrinsicModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842A736127DB3032006EE1EA /* OperationExtrinsicModel.swift */; }; - 842A736427DB31A3006EE1EA /* OperationRewardModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842A736327DB31A3006EE1EA /* OperationRewardModel.swift */; }; - 842A736627DB485E006EE1EA /* OperationSlashModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842A736527DB485E006EE1EA /* OperationSlashModel.swift */; }; + 842A736427DB31A3006EE1EA /* OperationRewardOrSlashModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842A736327DB31A3006EE1EA /* OperationRewardOrSlashModel.swift */; }; 842A736827DB4883006EE1EA /* OperationTransferModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842A736727DB4883006EE1EA /* OperationTransferModel.swift */; }; 842A736B27DB7A2E006EE1EA /* OperationDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842A736A27DB7A2E006EE1EA /* OperationDetailsViewModel.swift */; }; 842A736D27DB7B5E006EE1EA /* OperationTransferViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842A736C27DB7B5E006EE1EA /* OperationTransferViewModel.swift */; }; 842A736F27DB7E57006EE1EA /* OperationExtrinsicViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842A736E27DB7E57006EE1EA /* OperationExtrinsicViewModel.swift */; }; - 842A737127DB7EF1006EE1EA /* OperationSlashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842A737027DB7EF1006EE1EA /* OperationSlashViewModel.swift */; }; - 842A737327DB7F75006EE1EA /* OperationRewardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842A737227DB7F75006EE1EA /* OperationRewardViewModel.swift */; }; + 842A737327DB7F75006EE1EA /* OperationRewardOrSlashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842A737227DB7F75006EE1EA /* OperationRewardOrSlashViewModel.swift */; }; 842A737527DB8338006EE1EA /* OperationDetailsViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842A737427DB8338006EE1EA /* OperationDetailsViewModelFactory.swift */; }; 842A737727DC7AEB006EE1EA /* NetworkViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842A737627DC7AEB006EE1EA /* NetworkViewModelFactory.swift */; }; 842A737927DC7CEF006EE1EA /* DisplayAddressViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842A737827DC7CEF006EE1EA /* DisplayAddressViewModelFactory.swift */; }; @@ -937,7 +1167,6 @@ 843074F928BF6201009D463B /* NoAccountSupportPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843074F828BF6201009D463B /* NoAccountSupportPresentable.swift */; }; 8430AACC2602249B005B1066 /* InitialStakingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8430AACB2602249B005B1066 /* InitialStakingState.swift */; }; 8430AAD42602285B005B1066 /* StakingStateCommonData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8430AAD32602285B005B1066 /* StakingStateCommonData.swift */; }; - 8430AADC26022C58005B1066 /* NoStashState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8430AADB26022C58005B1066 /* NoStashState.swift */; }; 8430AAE126022CA1005B1066 /* BaseStakingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8430AAE026022CA1005B1066 /* BaseStakingState.swift */; }; 8430AAE926022F69005B1066 /* StashState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8430AAE826022F69005B1066 /* StashState.swift */; }; 8430AAF12602306A005B1066 /* BondedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8430AAF02602306A005B1066 /* BondedState.swift */; }; @@ -1148,7 +1377,6 @@ 8448F7A22882ABF50080CEA9 /* CustomSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8448F7A12882ABF50080CEA9 /* CustomSearchView.swift */; }; 8448F7A42882E21E0080CEA9 /* SearchMatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8448F7A32882E21E0080CEA9 /* SearchMatch.swift */; }; 8448F7A6288314250080CEA9 /* AssetListAssetsViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8448F7A5288314250080CEA9 /* AssetListAssetsViewModelFactory.swift */; }; - 8449660A25E15ECA00F2E9F5 /* RewardDestinationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8449660925E15ECA00F2E9F5 /* RewardDestinationViewModel.swift */; }; 844A539529BF54BA00C77111 /* XcmPalletMetadataQueryFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844A539429BF54BA00C77111 /* XcmPalletMetadataQueryFactory.swift */; }; 844A539729BF606D00C77111 /* BlockchainWeightFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844A539629BF606D00C77111 /* BlockchainWeightFactory.swift */; }; 844ADE7528CA31BA00EE29F7 /* ParaStkYieldBoostPeriodViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844ADE7428CA31BA00EE29F7 /* ParaStkYieldBoostPeriodViewModel.swift */; }; @@ -1363,8 +1591,6 @@ 8463781F2979DACB0034162B /* ReferendumsActivityViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8463781E2979DACB0034162B /* ReferendumsActivityViewModelFactory.swift */; }; 8463A6F925E2F82E003B8160 /* CDSingleValue+CoreDataCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8463A6F825E2F82E003B8160 /* CDSingleValue+CoreDataCodable.swift */; }; 8463A70325E2FCD0003B8160 /* WeakWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8463A70225E2FCD0003B8160 /* WeakWrapper.swift */; }; - 8463A71225E30C95003B8160 /* BalanceViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8463A71125E30C95003B8160 /* BalanceViewModelFactory.swift */; }; - 8463A71A25E3116A003B8160 /* BalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8463A71925E3116A003B8160 /* BalanceViewModel.swift */; }; 8463A71F25E39E07003B8160 /* StorageProviderSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8463A71E25E39E07003B8160 /* StorageProviderSource.swift */; }; 8463A72525E3A82A003B8160 /* DataProviderProxyTrigger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8463A72425E3A82A003B8160 /* DataProviderProxyTrigger.swift */; }; 8463A72D25E3A8E1003B8160 /* ChainStorageDecodedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8463A72C25E3A8E1003B8160 /* ChainStorageDecodedItem.swift */; }; @@ -1571,10 +1797,8 @@ 8477DAA32888329800129B45 /* watchOnlyPreset.json in Resources */ = {isa = PBXBuildFile; fileRef = 8477DAA22888329800129B45 /* watchOnlyPreset.json */; }; 8477DAA6288832CB00129B45 /* WatchOnlyPresetRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477DAA5288832CB00129B45 /* WatchOnlyPresetRepository.swift */; }; 84786DA825F9F58E0089DFF7 /* EraValidatorService+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84786DA725F9F58E0089DFF7 /* EraValidatorService+Fetch.swift */; }; - 84786E1025FA20D30089DFF7 /* StakingAccountResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84786E0F25FA20D30089DFF7 /* StakingAccountResolver.swift */; }; 84786E1525FA57B90089DFF7 /* StakingLedger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84786E1425FA57B90089DFF7 /* StakingLedger.swift */; }; 84786E1A25FA6A470089DFF7 /* StashItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84786E1925FA6A470089DFF7 /* StashItem.swift */; }; - 84786E1F25FA6C390089DFF7 /* CDStashItem+CoreDataCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84786E1E25FA6C390089DFF7 /* CDStashItem+CoreDataCodable.swift */; }; 84786E2425FBA2A50089DFF7 /* StakingAccountSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84786E2325FBA2A50089DFF7 /* StakingAccountSubscription.swift */; }; 8479607E283B60AA0084E779 /* ActionLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8479607D283B60AA0084E779 /* ActionLoadingView.swift */; }; 84796080283B63240084E779 /* LoadableActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8479607F283B63240084E779 /* LoadableActionView.swift */; }; @@ -1598,7 +1822,6 @@ 847A25CA28D85204006AC9F5 /* ReferendumInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A25C928D85204006AC9F5 /* ReferendumInfo.swift */; }; 847A6C0B28817E4000477F77 /* AssetListBaseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847A6C0A28817E4000477F77 /* AssetListBaseInteractor.swift */; }; 847ABE3128532E1B00851218 /* ConsesusType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847ABE3028532E1B00851218 /* ConsesusType.swift */; }; - 847ABE332853333A00851218 /* StakingSharedState+Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847ABE322853333A00851218 /* StakingSharedState+Duration.swift */; }; 847C15BF2A0CFE0D003F3FF8 /* WalletConnectErrorPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847C15BE2A0CFE0D003F3FF8 /* WalletConnectErrorPresentable.swift */; }; 847C9620255340F2002D288F /* ExportGenericViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847C961F255340F2002D288F /* ExportGenericViewController.swift */; }; 847C962825534134002D288F /* ExportGenericProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847C962725534134002D288F /* ExportGenericProtocols.swift */; }; @@ -1849,7 +2072,6 @@ 8493D0DF26FE7D7400A28008 /* ChainRepositoryFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8493D0DE26FE7D7400A28008 /* ChainRepositoryFactory.swift */; }; 8493D0E126FF4F5000A28008 /* ScaleCoder+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8493D0E026FF4F5000A28008 /* ScaleCoder+Extension.swift */; }; 8493D0E326FF571D00A28008 /* PriceProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8493D0E226FF571D00A28008 /* PriceProviderFactory.swift */; }; - 8493D3E62705994200157009 /* StakingSharedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8493D3E52705994200157009 /* StakingSharedState.swift */; }; 8493D3E927059B6700157009 /* StakingServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8493D3E827059B6700157009 /* StakingServiceFactory.swift */; }; 8493FF38291A59D800F09F1B /* ReferendumMetadataMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8493FF37291A59D800F09F1B /* ReferendumMetadataMapper.swift */; }; 8494424A265306BD0016E7BD /* ChangeRewardDestinationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84944249265306BD0016E7BD /* ChangeRewardDestinationViewModel.swift */; }; @@ -2274,8 +2496,6 @@ 84C2063E28D1E8CE006D0D52 /* AssetBalanceChanged.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C2063D28D1E8CE006D0D52 /* AssetBalanceChanged.swift */; }; 84C2064028D1EAD2006D0D52 /* AccountAssetBalanceTrigger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C2063F28D1EAD2006D0D52 /* AccountAssetBalanceTrigger.swift */; }; 84C2802126F541DE006E8014 /* WebSocketEngine+Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C2802026F541DE006E8014 /* WebSocketEngine+Connection.swift */; }; - 84C2F27725E296CD0050A4AD /* RewardDestinationViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C2F27625E296CD0050A4AD /* RewardDestinationViewModelFactory.swift */; }; - 84C2F27D25E297350050A4AD /* CalculatedReward.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C2F27C25E297350050A4AD /* CalculatedReward.swift */; }; 84C34204283124C600156569 /* StakingParachainWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C34203283124C600156569 /* StakingParachainWireframe.swift */; }; 84C3420728314D9600156569 /* ParaStkScheduledRequestsQueryFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C3420628314D9600156569 /* ParaStkScheduledRequestsQueryFactory.swift */; }; 84C342092831645800156569 /* EraCountdownDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C342082831645800156569 /* EraCountdownDisplay.swift */; }; @@ -2389,7 +2609,6 @@ 84D1110E26B931C20016D962 /* ChainModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D1110D26B931C20016D962 /* ChainModel.swift */; }; 84D1111126B932480016D962 /* AssetModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D1111026B932480016D962 /* AssetModel.swift */; }; 84D1111326B932C40016D962 /* ChainNodeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D1111226B932C40016D962 /* ChainNodeModel.swift */; }; - 84D17ECC2803F7EF00F7BAFF /* StakingAmountLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D17ECB2803F7EF00F7BAFF /* StakingAmountLayout.swift */; }; 84D17ECE2804290700F7BAFF /* RadioSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D17ECD2804290700F7BAFF /* RadioSelectorView.swift */; }; 84D17ED628053D6D00F7BAFF /* DAppFavorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D17ED528053D6D00F7BAFF /* DAppFavorite.swift */; }; 84D17ED828053E3200F7BAFF /* DAppFavoriteMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D17ED728053E3200F7BAFF /* DAppFavoriteMapper.swift */; }; @@ -2575,7 +2794,6 @@ 84E8BA2A2A00EF4C00FD9F40 /* XcmBaseMetadataQueryFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E8BA292A00EF4C00FD9F40 /* XcmBaseMetadataQueryFactory.swift */; }; 84E90BA128D0B51000529633 /* CheckboxControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E90BA028D0B51000529633 /* CheckboxControlView.swift */; }; 84E9A05028F000AB00551DC4 /* ReferendumMetadataLocal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E9A04F28F000AB00551DC4 /* ReferendumMetadataLocal.swift */; }; - 84EA0B2A25E579DF00AFB0DC /* AssetBalanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EA0B2925E579DF00AFB0DC /* AssetBalanceViewModel.swift */; }; 84EB6C4E281999E100CFD8B2 /* PayoutTimeViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EB6C4D281999E100CFD8B2 /* PayoutTimeViewModelFactory.swift */; }; 84EBA4F027AD26A5000AEEAD /* AssetBalanceId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EBA4EF27AD26A5000AEEAD /* AssetBalanceId.swift */; }; 84EBAB06265DC24C0015E446 /* CrowdloanContributionViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EBAB05265DC24C0015E446 /* CrowdloanContributionViewModelFactory.swift */; }; @@ -2781,6 +2999,7 @@ 8582395FEF296771447439FF /* AssetsSearchWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6746DDB8F277A968E6B25332 /* AssetsSearchWireframe.swift */; }; 85A093F6055DDD2E2E9253F2 /* ControllerAccountProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F829E7F8B39EE7D977001510 /* ControllerAccountProtocols.swift */; }; 86EB789787B731691B36C827 /* OnChainTransferSetupPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1A2F7E5E278FDCC89FE097 /* OnChainTransferSetupPresenter.swift */; }; + 873FAB6E5CAD1FD4D02737D0 /* NominationPoolBondMoreSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C9473AB7D1FE4A27403078 /* NominationPoolBondMoreSetupViewController.swift */; }; 879D493C025963619CFADF4F /* GovernanceUnlockSetupProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4642DD5186EFA940518CCB4 /* GovernanceUnlockSetupProtocols.swift */; }; 87F7556E02F6F5BB6F1B1AEA /* ParitySignerTxQrViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A5DCA28ABF42D342BBDF9A /* ParitySignerTxQrViewLayout.swift */; }; 880059D828EEBC0200E87B9B /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880059D728EEBC0200E87B9B /* SliderView.swift */; }; @@ -2800,10 +3019,7 @@ 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 */; }; 880CC0A829E7F13B008C7F65 /* EquilibriumAccountBalancesSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880CC0A729E7F13B008C7F65 /* EquilibriumAccountBalancesSubscription.swift */; }; 880CC0AA29E7F151008C7F65 /* EquilibriumLocksSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880CC0A929E7F151008C7F65 /* EquilibriumLocksSubscription.swift */; }; 880CC0AC29E7F168008C7F65 /* EquilibriumReservedSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880CC0AB29E7F168008C7F65 /* EquilibriumReservedSubscription.swift */; }; @@ -3007,7 +3223,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 */; }; + 88CD321028E2137300542F0D /* ExternalBalanceContribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88CD320F28E2137200542F0D /* ExternalBalanceContribution.swift */; }; 88D02FE32942EA2200E26390 /* PayButtonsRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D02FE22942EA2200E26390 /* PayButtonsRow.swift */; }; 88D02FE52942EA7800E26390 /* AssetDetailsStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D02FE42942EA7800E26390 /* AssetDetailsStyles.swift */; }; 88D02FE82942EB1A00E26390 /* AssetDetailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D02FE72942EB1A00E26390 /* AssetDetailsModel.swift */; }; @@ -3064,7 +3280,6 @@ 88FB7DCF295071B100784E08 /* ContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FB7DCE295071B100784E08 /* ContainerViewController.swift */; }; 88FB7DD12950720800784E08 /* ContainerProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FB7DD02950720800784E08 /* ContainerProtocols.swift */; }; 88FB7DD32950723100784E08 /* NovaWalletViewModelObserverContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FB7DD22950723100784E08 /* NovaWalletViewModelObserverContainer.swift */; }; - 88FB7DD72951B1AF00784E08 /* WalletHistoryFilter+CallCodingPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FB7DD62951B1AF00784E08 /* WalletHistoryFilter+CallCodingPath.swift */; }; 88FF5C7C29C8360400D1CB5D /* Caip19+ParseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FF5C7B29C8360400D1CB5D /* Caip19+ParseError.swift */; }; 88FF5C7F29C8364500D1CB5D /* Caip2+ParseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FF5C7E29C8364500D1CB5D /* Caip2+ParseError.swift */; }; 8916E9179CF5409E65D1B3A6 /* NftDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A46EE888D60C1538A0A3EFC /* NftDetailsProtocols.swift */; }; @@ -3095,11 +3310,14 @@ 91530F7301CA39654E008580 /* DAppBrowserInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15A4DFCC4236D25A3D59F809 /* DAppBrowserInteractor.swift */; }; 91A1286763617DE022BD495F /* LedgerInstructionsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1B08ACC71BE679A48A7B66E /* LedgerInstructionsPresenter.swift */; }; 921E4891E85C0DC6FDD8A0D0 /* CrowdloanContributionConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1366336078BCA34EFB4C6FF9 /* CrowdloanContributionConfirmInteractor.swift */; }; + 924BADB89E7FA2DC54BF1A02 /* NPoolsClaimRewardsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5F1F933F624B01855AA3BA5 /* NPoolsClaimRewardsInteractor.swift */; }; 93434E8E407A6C63D8862A21 /* AssetSelectionProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DECC58C93DB18E79A03B5A0 /* AssetSelectionProtocols.swift */; }; 934F229F4E5A588D5AF2A093 /* TokensAddSelectNetworkViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E800814C025B38C87CC282D /* TokensAddSelectNetworkViewFactory.swift */; }; 9358E048B1AA0F71F519101E /* GovernanceDelegateConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27FB20961F2F221A96624A6 /* GovernanceDelegateConfirmInteractor.swift */; }; + 93B64E378DDDCC7F20FF78A2 /* NPoolsRedeemPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D256E075B9F6E26225C20B8C /* NPoolsRedeemPresenter.swift */; }; 93EB8C73108944E9C576936C /* ReferendumVotersPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9990DF2F0214CD51E5388CE /* ReferendumVotersPresenter.swift */; }; 940DA38E4586A27D7F3E0C67 /* ParitySignerAddressesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3280014154DCC105757E317C /* ParitySignerAddressesViewController.swift */; }; + 948FE60822DFC49A0BD5740B /* NominationPoolBondMoreBaseWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4009AE7EF95E9CE1EB88B8 /* NominationPoolBondMoreBaseWireframe.swift */; }; 94B0F0C84AF74B3CD7223C3A /* AccountConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7306D50F278F6CC90DC88F27 /* AccountConfirmPresenter.swift */; }; 94B234EE404088B077DB6411 /* DAppListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10F3CCA5BA57C68D5AE2B42F /* DAppListProtocols.swift */; }; 9565BEB636E6D386B0C0FBE5 /* StakingPayoutConfirmationViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE0492B98AB9C1540846B39 /* StakingPayoutConfirmationViewFactory.swift */; }; @@ -3122,11 +3340,14 @@ 99A4B2A357ADEA45EFF515A5 /* AccountExportPasswordProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC404A4071AF571FAC4C1994 /* AccountExportPasswordProtocols.swift */; }; 9A6A55297F41DAE45071BF57 /* ExportSeedInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A865455F8FC60413A6CB8A44 /* ExportSeedInteractor.swift */; }; 9A940CBA3F309D438945A90C /* ControllerAccountConfirmationProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA49A762B2FBB66FD6A55FC /* ControllerAccountConfirmationProtocols.swift */; }; + 9AA6B442A40A0F8C991D0A12 /* NominationPoolBondMoreConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D3243E13077BFD9DAC8FFC /* NominationPoolBondMoreConfirmPresenter.swift */; }; 9ACF0A5CCB50CEDD97671EDE /* AssetReceiveViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73DE88ED69EF6E4F4F6612D8 /* AssetReceiveViewLayout.swift */; }; 9B4BE26140C63E07C256CC97 /* StakingRedeemProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E4B0600AFFB96A75CF98755 /* StakingRedeemProtocols.swift */; }; 9B4F0484B81BBF8DFA618599 /* AccountCreateViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9636093217ABE05A7FAC9B9 /* AccountCreateViewFactory.swift */; }; 9B6CD060F0EB77C162D90D3E /* ChainAddressDetailsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781FA4C896AF31B4035AFB38 /* ChainAddressDetailsViewFactory.swift */; }; 9BADFCBF3AF5186094DB8D67 /* DAppTxDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB4A14C99D151B41F61F474 /* DAppTxDetailsInteractor.swift */; }; + 9C223E4BF19F7314A9E6F1CA /* NominationPoolSearchViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686A91FF92C89FE8937EF5A /* NominationPoolSearchViewLayout.swift */; }; + 9D509AD640B01CAB872E0E71 /* NominationPoolSearchViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15AC3897EC736F5096949BBC /* NominationPoolSearchViewFactory.swift */; }; 9D5926790B055C56FB74B282 /* AccountManagementProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B5072E250B7277F605855B3 /* AccountManagementProtocols.swift */; }; 9DE1757D047A4D1E97913774 /* GovernanceUnlockConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D3FE2CE7F9F2836755DBA63 /* GovernanceUnlockConfirmProtocols.swift */; }; 9DED20EB20A872E682CB402A /* ReferendumFullDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F6DA24E0481A3678F2EF809 /* ReferendumFullDetailsProtocols.swift */; }; @@ -3141,14 +3362,16 @@ A05E466D47CCB956E318A39F /* StakingDashboardViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E807E9E12A130C50E8FFDF /* StakingDashboardViewFactory.swift */; }; A07A987DE3047AF1A786D511 /* DAppListViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11BB49F165F64A7EF6418EB4 /* DAppListViewLayout.swift */; }; A090FF206B56A0E465C62072 /* CrowdloanListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86F7A369E31DCB9ABD556EE9 /* CrowdloanListPresenter.swift */; }; + A0EFC9C4C6F0AE9AFDA9A3EA /* NominationPoolSearchPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1090AF9D9F18846E5A73F73C /* NominationPoolSearchPresenter.swift */; }; A14308E2633921838166C843 /* ParaStkUnstakeConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F2450A063F4D66EFDF6B8A /* ParaStkUnstakeConfirmViewFactory.swift */; }; - A1621946D1A3383E275225CD /* Pods_novawalletAll_novawalletIntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 86F9063B2DF46E7B65B5248E /* Pods_novawalletAll_novawalletIntegrationTests.framework */; }; A1FA23B8A833B6896104ABA6 /* GovernanceSelectTracksWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE82B42E77A241D336A5B65F /* GovernanceSelectTracksWireframe.swift */; }; A265CC9857E951EB71E5E831 /* WalletsListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DBACA1AB17E90565F133C19 /* WalletsListInteractor.swift */; }; A2BE8967FC1609D61E4131BE /* ParaStkYieldBoostStopViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2B438707EA6C81C48EAB4CE /* ParaStkYieldBoostStopViewController.swift */; }; A2F7908210A0398EDBBA89BD /* NftDetailsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3CC9CAB00B604CD1AC5B4D8 /* NftDetailsViewFactory.swift */; }; A32E1373E3671D518FFC3BC2 /* YourValidatorListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B90CEC70F101AA25A4C00021 /* YourValidatorListViewController.swift */; }; A3BDFA01A32B6C7463E6EFFA /* GovernanceUnlockConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513A449CCF5A417B67B7067D /* GovernanceUnlockConfirmPresenter.swift */; }; + A3FD763479AAB9290A612A1C /* StakingSelectPoolViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 939D6CB66B9D5B86FEE5256E /* StakingSelectPoolViewLayout.swift */; }; + A428E022070EF536D4B0B5EC /* StartStakingConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6589230FD54838BFCF6E3FD8 /* StartStakingConfirmViewFactory.swift */; }; A44CA3E6BB506841948AB2D1 /* ReferendumsFiltersWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 215C03EE9C9EE024F952CB1C /* ReferendumsFiltersWireframe.swift */; }; A5153C322938579FA145742A /* DAppWalletAuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B675B6727754E3727CBC7BE /* DAppWalletAuthViewController.swift */; }; A5880E3789BC9E30835BDCC7 /* TransferSetupViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8202B83B2DF36439CB6449C6 /* TransferSetupViewFactory.swift */; }; @@ -3156,9 +3379,11 @@ A714CEAF7A86292E8D679056 /* ParaStkStakeSetupViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7955BBDC93BC07D069B8F /* ParaStkStakeSetupViewFactory.swift */; }; A748D64F6048192E16E5BE44 /* ParaStkUnstakeConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD36AD8C414F8973CDA8A0F /* ParaStkUnstakeConfirmViewLayout.swift */; }; A8115BE0BFA6558B46CEA101 /* ParaStkCollatorInfoPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C96F37D7166820D6C9CC62 /* ParaStkCollatorInfoPresenter.swift */; }; + A83ED3518F2FD1C7B1C48D9E /* StartStakingInfoViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D4A8C06D94C748124E6AA5 /* StartStakingInfoViewLayout.swift */; }; A871B6ABACAE8A811010F792 /* StakingPayoutConfirmationWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5ACCFD31E14AF4D30955288 /* StakingPayoutConfirmationWireframe.swift */; }; A8F69AC9D7294E7DCBA50470 /* SelectValidatorsStartPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B6244A9538B39AFCD3A6F3A /* SelectValidatorsStartPresenter.swift */; }; A97F32D057BFEFBCC478A09C /* DAppTxDetailsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A028BD1A81CA95BE0DB66031 /* DAppTxDetailsViewFactory.swift */; }; + A99428A540CDA6B359220477 /* StartStakingConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB617C1637B277B83E4C4914 /* StartStakingConfirmViewController.swift */; }; A9A4846AB91BCB88E0416E38 /* CommonDelegationTracksViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A0E36C6D08351DFF7263E5 /* CommonDelegationTracksViewLayout.swift */; }; AA3AE7900853DFB4D6FC3F96 /* DelegateVotedReferendaViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399700B22225DD916DFACAF9 /* DelegateVotedReferendaViewFactory.swift */; }; AB27EE4EE30A06D8E7B8EDB4 /* ReferendumSearchPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1626234F0BF7E20D351CB2 /* ReferendumSearchPresenter.swift */; }; @@ -3167,7 +3392,10 @@ AB5EA0348C8E8C40FCA9DC86 /* WalletConnectSessionsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52CC2C3981590DCEDE3ABBEC /* WalletConnectSessionsWireframe.swift */; }; ABA3D873BBECB7F4BD670872 /* ExportSeedPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFB278373745C20822442686 /* ExportSeedPresenter.swift */; }; AC904E313DC15AE40C927946 /* DAppAddFavoriteInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96BE36DA0A2310660A43FF5B /* DAppAddFavoriteInteractor.swift */; }; + ACE725ECAB92169CC13E788D /* NominationPoolBondMoreConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D6B3F74819B5D45324F714 /* NominationPoolBondMoreConfirmInteractor.swift */; }; AD1920A8DE1C9A4D6502473F /* GovernanceRemoveVotesConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6606F6FCB40D5EDA2CDFA84F /* GovernanceRemoveVotesConfirmViewController.swift */; }; + AD36A601830C69DA003B3B01 /* StakingSelectPoolProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5A2FE372A88ECF23B620D2 /* StakingSelectPoolProtocols.swift */; }; + AD3D8EA1D79D3E5E5B625CF7 /* StakingTypeWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D99FECD7EDCFE3A23E1AEAF /* StakingTypeWireframe.swift */; }; AD877F7F77F9CB862DC7D5B3 /* DelegationReferendumVotersWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B13D65B93E65B5112272962 /* DelegationReferendumVotersWireframe.swift */; }; AD9CA7391ACF85B767D7784C /* LedgerDiscoverWalletCreateWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46BDF9947BE6366712E454DD /* LedgerDiscoverWalletCreateWireframe.swift */; }; AE1000F226679886004753B7 /* ChangeTargetsRecommendationWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE1000F126679886004753B7 /* ChangeTargetsRecommendationWireframe.swift */; }; @@ -3291,6 +3519,7 @@ B062216285AFF6AA1E1E78D3 /* GovernanceUnavailableTracksPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1768C3415D7D9CEEA8A8C700 /* GovernanceUnavailableTracksPresenter.swift */; }; B071927DF8DD5C3CA84494BA /* RecommendedValidatorListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F61D8973ADEB461DE2AD3E13 /* RecommendedValidatorListViewController.swift */; }; B09F155D14D146377FB2952A /* StakingRebagConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E005960E331E8882A8C6FD /* StakingRebagConfirmWireframe.swift */; }; + B19BA6D7071BC7BE1EFFDE6D /* NPoolsClaimRewardsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12E4AA5C56575FD3ABA7693 /* NPoolsClaimRewardsProtocols.swift */; }; B1B0F4818510EB082ACA83AB /* MoonbeamTermsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 313CDB5EB70518DEC1BDB392 /* MoonbeamTermsViewLayout.swift */; }; B1BB78684B059A113AB3AD30 /* DAppPhishingViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AE1C39767C4CBE3229089D /* DAppPhishingViewFactory.swift */; }; B1CCC5B7BF30F6ACA309B112 /* StakingRedeemViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7F5F9B54BE4234C5682BDE /* StakingRedeemViewController.swift */; }; @@ -3301,10 +3530,14 @@ B316F0D2BDF0F44AD27F58E0 /* MoonbeamTermsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4F2B1A2A919663ABEE3367A /* MoonbeamTermsWireframe.swift */; }; B317AB093D99677D292121C4 /* YourWalletsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84ED3A7899C88876AB3DCA5F /* YourWalletsViewController.swift */; }; B322E542E7B9D5EAB745546E /* TransactionHistoryViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7DAF20C447065DA5467696 /* TransactionHistoryViewLayout.swift */; }; + B33EBF5821B995FE21424705 /* NPoolsUnstakeConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12C822BAC40271396E36ACBB /* NPoolsUnstakeConfirmViewLayout.swift */; }; B3E567D46F54E8E735792FE1 /* ParaStkYieldBoostSetupViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC8958927F940B9B638EF89D /* ParaStkYieldBoostSetupViewFactory.swift */; }; B409644ED1E20062A3EA0316 /* DAppTxDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BCDAB970C17F9798AC79B08 /* DAppTxDetailsViewController.swift */; }; B42652A7AB4244F19473C7C5 /* DAppWalletAuthProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDC898FE48131F4FB8E64B8 /* DAppWalletAuthProtocols.swift */; }; + B42ACF24D3F01C44B856C0E7 /* NominationPoolBondMoreConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E8ACEB2D157E376D53C6DE /* NominationPoolBondMoreConfirmViewFactory.swift */; }; B51AD1836313CE26F369ED3F /* CustomValidatorListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96D540DFC00C25D8F73CFDC3 /* CustomValidatorListWireframe.swift */; }; + B556794F1316E47FF2FDB3E7 /* Pods_novawalletAll_novawalletIntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 89CA10C284A0C9853E043A9D /* Pods_novawalletAll_novawalletIntegrationTests.framework */; }; + B5998094F5FFAC894512CD12 /* StakingSetupAmountViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD38E012F7837AC692CA2C41 /* StakingSetupAmountViewLayout.swift */; }; B61457C5248F3B0E88A7990E /* ParaStkRebondPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9F49B3B261FBA0B568A5320 /* ParaStkRebondPresenter.swift */; }; B65F3304C17AF5D15F0EA150 /* TransactionHistoryPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C67AB2EC2E4E8CB4358CA13 /* TransactionHistoryPresenter.swift */; }; B6D4C073B3F984FE0348B7D4 /* ParaStkYieldBoostStartInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0988D8EC0768B03BA7C55612 /* ParaStkYieldBoostStartInteractor.swift */; }; @@ -3316,20 +3549,25 @@ B8349266F061AEFFA9802237 /* TokenManageSingleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADBC3405EEB1F8941FB19BAA /* TokenManageSingleViewController.swift */; }; B8DC27C3CAE9846A5E56B4EE /* GovernanceDelegateSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3903A0A93FB8068071F9BD /* GovernanceDelegateSetupViewController.swift */; }; B8FD43D2A80BD41D5F6FE14E /* GovernanceDelegateSetupProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 123EC3706DD70AC132ECEACB /* GovernanceDelegateSetupProtocols.swift */; }; + B959E423181234E82B0695DF /* NPoolsRedeemInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A084C41472C2E504575C709B /* NPoolsRedeemInteractor.swift */; }; BA7AEE82627CFC0AFD69B299 /* RecommendedValidatorListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2580363AC3E4A9CD40256E /* RecommendedValidatorListPresenter.swift */; }; BAEBD54874957F0C78D01BE6 /* StakingDashboardInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 879470FAF62A152E52DF4845 /* StakingDashboardInteractor.swift */; }; BAF044619C83E31EEEDA0BCB /* GovernanceRevokeDelegationConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92381CBE193B3317F477D377 /* GovernanceRevokeDelegationConfirmViewController.swift */; }; BB29490A4E8472A7DB781BC4 /* TransferOnChainConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D385A8FCB0E6F3F6B6872F01 /* TransferOnChainConfirmPresenter.swift */; }; + BB48754C001B791A6ACA16A4 /* StakingTypeViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E66B662DD15424D2B383A7 /* StakingTypeViewLayout.swift */; }; BBCDADFE12B41BCF0D076DF4 /* TokensManageAddInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE992187F6F3899568F3DAE /* TokensManageAddInteractor.swift */; }; BCC42525D716872B7E6CD72B /* TokenManageSingleProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 674601F0C47EF05F7D35A592 /* TokenManageSingleProtocols.swift */; }; + BD02676BEB6F51E2A325EAD9 /* NominationPoolBondMoreConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F5A8E5D1C5A7F0B5B1570B9 /* NominationPoolBondMoreConfirmViewLayout.swift */; }; + BD556407702A75D66B73A55C /* NominationPoolBondMoreSetupViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C760EDD8CDD8073063D76D /* NominationPoolBondMoreSetupViewFactory.swift */; }; BD571417BD18C711B76E1D62 /* ExportSeedWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B4C1B5D56DB69BA0AECF731 /* ExportSeedWireframe.swift */; }; BDAD1DD6A88406F606E2A70D /* TransactionHistoryProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2CCB36A1265AA89D345B3CE /* TransactionHistoryProtocols.swift */; }; BE301A0F2286CCEF6A02D341 /* LocksPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26F9E9C70B0C14570AB783AF /* LocksPresenter.swift */; }; + BE3BCEF256B1B24D702A9869 /* StakingTypeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3930902D540DB0B9A2CFD21C /* StakingTypeViewController.swift */; }; BE3F6213B26F35EB6324DBD8 /* ControllerAccountWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB9EDB05686DF11958145E1 /* ControllerAccountWireframe.swift */; }; BE8CF97B6EA62C75277B78AA /* MoonbeamTermsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D8F02830944DBAF72D8A41 /* MoonbeamTermsProtocols.swift */; }; BE966FC878B738FC9DE1D296 /* AssetReceiveViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C5C524FD0E3E7E5113F325D /* AssetReceiveViewFactory.swift */; }; BEA539EE97A287868FD8BE46 /* AssetSelectionViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9622C6C3102EF12BEE78D63D /* AssetSelectionViewFactory.swift */; }; - BEE36A5554B026BD7BCD3199 /* StakingAmountPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B47961B2254E8A4D8EC588 /* StakingAmountPresenter.swift */; }; + BF31A2900BA28194047D2219 /* NPoolsUnstakeSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2864B5E1A5AFC6A8C1B4B9E5 /* NPoolsUnstakeSetupViewController.swift */; }; BFC8C5A2C95D6EDF97D73732 /* ParaStkCollatorsSearchPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18EBB92677F15F1E41762DE4 /* ParaStkCollatorsSearchPresenter.swift */; }; C01C5F1C8CB67B0D5CBE9FB1 /* StakingMainPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502D42F4A480889BA226CAD3 /* StakingMainPresenter.swift */; }; C0A7710415B9C9BA496320E7 /* ParaStkYieldBoostSetupProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2437D345C3D9B12AEE1E28 /* ParaStkYieldBoostSetupProtocols.swift */; }; @@ -3348,6 +3586,7 @@ C317BBF2F1815F0A8D937428 /* DelegateVotedReferendaViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E86B1DF9D9E8E5DD8DC49DE /* DelegateVotedReferendaViewLayout.swift */; }; C32B85D65D0290A577BFC85F /* ParaStkRebondViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97910F70D80502419BCBB398 /* ParaStkRebondViewFactory.swift */; }; C39CB534B2EE7ABD6D6085BA /* TransactionHistoryViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A937F85FE35340EDA131C2EC /* TransactionHistoryViewFactory.swift */; }; + C49AC521056CBDB5451B1CDC /* NominationPoolBondMoreBaseInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84033496FC259BCA5420D52B /* NominationPoolBondMoreBaseInteractor.swift */; }; C4A4D40A08DAB4A71C21C1A8 /* StakingRedeemInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 560C48D7A83F51F001622D71 /* StakingRedeemInteractor.swift */; }; C5B07E59C0B00CAD1D0D2DFD /* ReferendumSearchWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72EF3BEC3EA07548C10A87FD /* ReferendumSearchWireframe.swift */; }; C5B6C00F8B0E3D89CBF1A8DB /* ParaStkSelectCollatorsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2336E4CAF4A1F627C39093FF /* ParaStkSelectCollatorsViewFactory.swift */; }; @@ -3359,6 +3598,7 @@ C6E5671768DA68535DA5B1C7 /* ControllerAccountConfirmationViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02DBCA4A63A5E52E3739374 /* ControllerAccountConfirmationViewFactory.swift */; }; C729BF3E60E6825AEED11383 /* ParaStkRedeemViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AD16D5FA4115F2A525BDE4F /* ParaStkRedeemViewController.swift */; }; C74B44F382EDAE5CB5A8468F /* ParaStkUnstakeConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99C973B203F72D6233718CD4 /* ParaStkUnstakeConfirmProtocols.swift */; }; + C7C6236CA43E934F2A4EC779 /* Pods_novawalletAll_novawallet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0CC41A9A168AF0F1FBE5F799 /* Pods_novawalletAll_novawallet.framework */; }; C7D77690E10875CF1856EBA1 /* StakingRewardPayoutsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7911693957DFAF141EBDAFEC /* StakingRewardPayoutsProtocols.swift */; }; C7E68F5B6EC7B21B8797F874 /* GovernanceEditDelegationTracksPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FF467EBB295AC2A3DDA4492 /* GovernanceEditDelegationTracksPresenter.swift */; }; C7E96E4185084C3E8F226E97 /* GovernanceRevokeDelegationTracksPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F99FF35A16A80005A264E5F /* GovernanceRevokeDelegationTracksPresenter.swift */; }; @@ -3370,13 +3610,14 @@ C9931414951375760E5D1C57 /* OperationDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BCCD837A377C237C18B117E /* OperationDetailsViewController.swift */; }; CA3C4729115D875D0C80A3E8 /* TokensManageWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA3E7F90F95A6886798A995 /* TokensManageWireframe.swift */; }; CA8D91616B55AF9A02215FBB /* LedgerPerformOperationViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A399F229B59A854FEA6D91 /* LedgerPerformOperationViewLayout.swift */; }; + CB2E57FC45AC1E2980E3492E /* NPoolsUnstakeSetupViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE86742207CDAD889A01B40A /* NPoolsUnstakeSetupViewLayout.swift */; }; CB48D6AC528CF9AAE8766CC5 /* LedgerNetworkSelectionViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B03974ECD8AEF39FDCA277D7 /* LedgerNetworkSelectionViewFactory.swift */; }; CC1723581BAC50E8B9E6DE48 /* AssetsSearchPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF8D2EF3141D2AD73D272E20 /* AssetsSearchPresenter.swift */; }; CC545DF80038901FA06FDD58 /* SelectValidatorsConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8B0940B2CB25AD9C36206E /* SelectValidatorsConfirmViewController.swift */; }; CC73053BB60A041A0D3ABFB0 /* NftDetailsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE0641B2354E6F236CB9A132 /* NftDetailsPresenter.swift */; }; + CC8C6FFB98086AFBE38BDB82 /* StakingSelectPoolInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86F7ACFB151C31B3A5044DCD /* StakingSelectPoolInteractor.swift */; }; CCF5A7CED175D5E43B2C9971 /* StakingUnbondSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC17D12DCD0CDAF0BC13D80D /* StakingUnbondSetupViewController.swift */; }; CD4240B756F20C338A8B3589 /* LedgerInstructionsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AE3AE09F98E37C15BA87A1 /* LedgerInstructionsWireframe.swift */; }; - CD76A6513A708051857FD480 /* StakingAmountProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 999C15317E0B4FC67B9C17C5 /* StakingAmountProtocols.swift */; }; CD9359A2720F2EE1D4E09DF6 /* DAppTxDetailsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 219B9B1D97460F022D40D63E /* DAppTxDetailsWireframe.swift */; }; CDAB179209D12B81430E377C /* LedgerAccountConfirmationViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A695CA303926DFB5D54E309 /* LedgerAccountConfirmationViewLayout.swift */; }; CDB78A5A733E4A4F1A2C48C8 /* AssetSelectionWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7AD1285797131E836CD994B /* AssetSelectionWireframe.swift */; }; @@ -3416,8 +3657,8 @@ D7D91A2CACE6FE12AE634BEF /* DAppWalletAuthViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16A05911AC400226DD29AE20 /* DAppWalletAuthViewLayout.swift */; }; D83B47B07C0D40A327AC44F7 /* CustomValidatorListViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F52B8815D6AF5E69B145D245 /* CustomValidatorListViewFactory.swift */; }; D840B64C33EF47E723905378 /* OperationDetailsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5F05632A6635A54A9CDA7FC /* OperationDetailsViewFactory.swift */; }; - D8581E5440A19D977E17BFDE /* StakingAmountViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39D54DC9992CF9CB6699AA3 /* StakingAmountViewFactory.swift */; }; D886425A55425810AD070AB5 /* ControllerAccountConfirmationWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = C96C3B5ABF4A8124848EFD17 /* ControllerAccountConfirmationWireframe.swift */; }; + D8918818FD97B52EB9DA941E /* NominationPoolBondMoreSetupProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 725529DC6B70BF5B091B3748 /* NominationPoolBondMoreSetupProtocols.swift */; }; D8CB6639857FE917719EF0AF /* WalletConnectSessionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ADD21F058AE84F533353158 /* WalletConnectSessionsViewController.swift */; }; D9046DBA27451ED700C29F2E /* ParallelContributionSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9046DB927451ED700C29F2E /* ParallelContributionSource.swift */; }; D9046DBC27453D5C00C29F2E /* ParallelContributionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9046DBB27453D5C00C29F2E /* ParallelContributionResponse.swift */; }; @@ -3437,7 +3678,9 @@ DB20B86D7BDE3788947ED9A4 /* ParaStkYieldBoostStartViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31A3D4E3894582CB49013F0 /* ParaStkYieldBoostStartViewLayout.swift */; }; DB37394BD4880A81E22C7172 /* TokensAddSelectNetworkWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = E765FD856EB60BBF344205F3 /* TokensAddSelectNetworkWireframe.swift */; }; DB37BAF11845A4E5067E07C7 /* TransferConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0912F5E8BA170342D52F7D38 /* TransferConfirmViewController.swift */; }; + DB8AD60FE397F764623A566F /* StartStakingConfirmViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9AB4A7C57057ECFA1DA03E /* StartStakingConfirmViewLayout.swift */; }; DBA436B3B1C90965FE8F9B79 /* YourValidatorListPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0533F9E531CDFB721D697769 /* YourValidatorListPresenter.swift */; }; + DC02C1C18DC5C03F5A006C81 /* StartStakingInfoViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 694E15BA7E0C59E46D0A6612 /* StartStakingInfoViewFactory.swift */; }; DC2867A7DC1415052D090C53 /* ParitySignerWelcomeViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DEDFCF115BB1015C928F3B2 /* ParitySignerWelcomeViewLayout.swift */; }; DC682E96D056C069902B9C31 /* DAppSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53235E51143C6E93303E30FE /* DAppSearchViewController.swift */; }; DCD804D03231F9923FE1624C /* AssetDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F04A75E7196E62F45CA9369D /* AssetDetailsInteractor.swift */; }; @@ -3472,6 +3715,7 @@ E5528BE6A0BD2D2C53C6F5F8 /* DAppWalletAuthPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C56F2A76BA8745F1F708D4 /* DAppWalletAuthPresenter.swift */; }; E5DC2660D78D3CC9FC48E748 /* LedgerAccountConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A04CDB05A013ED57D3DEA3 /* LedgerAccountConfirmationViewController.swift */; }; E5F3DF66415E54AE04D0C9A9 /* StakingMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A05EB4FAF2FDE7DECEA93E4 /* StakingMainViewController.swift */; }; + E62D4B6812A8A8518A4B59D9 /* StakingSetupAmountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6708EF7D7F70868969EFADA9 /* StakingSetupAmountViewController.swift */; }; E65FDA8BF9DBE7F50AE9D733 /* ChangeWatchOnlyWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 994E31F4AE945AF2DE98539C /* ChangeWatchOnlyWireframe.swift */; }; E6AB0111B3E1297242D5DBDE /* DelegationReferendumVotersViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E83916DF00BA41516B94304E /* DelegationReferendumVotersViewFactory.swift */; }; E6D05825C7512E3CD560B39F /* AssetReceiveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB56DD69C433B473E65C7EB /* AssetReceiveViewController.swift */; }; @@ -3490,6 +3734,7 @@ EB20C6B406155664B981BA94 /* GovernanceYourDelegationsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D46DF4E6D8DAF6913474DED5 /* GovernanceYourDelegationsInteractor.swift */; }; EB376E61CD1C39AC148DE80C /* NftListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A60B27D3A045E0DEF23775 /* NftListViewController.swift */; }; EB5F587A71CCE1F0F86154CF /* ControllerAccountViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002A29AE58EB53E915330490 /* ControllerAccountViewFactory.swift */; }; + EB877554208E91A80985F1E5 /* NPoolsRedeemViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 862545D6BD501914AE04D776 /* NPoolsRedeemViewController.swift */; }; EB8FCA00BD20C63D578D6F80 /* TransferSetupProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1521715CA2071056FCE9D360 /* TransferSetupProtocols.swift */; }; EB9D8D22AA13BF12F845856B /* ReferralCrowdloanProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 953E21C32079A8051A0EE964 /* ReferralCrowdloanProtocols.swift */; }; EBC1107125C55A65D4E21417 /* DAppAuthSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B213C270130EDCF51303BFBE /* DAppAuthSettingsPresenter.swift */; }; @@ -3503,7 +3748,6 @@ EDD5551608E7ACDDBBC054C4 /* ParaStkRebondProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A0FFF412031C1373EBE2B8 /* ParaStkRebondProtocols.swift */; }; EE0DFA5851AEF99D3C2DBDDD /* ParaStkStakeConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335E8C17DCB794733476AAE3 /* ParaStkStakeConfirmViewController.swift */; }; EEDDE41F8445C0CB2E99AFE4 /* ParaStkYieldBoostStartPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3089A0A7C992300CE839A050 /* ParaStkYieldBoostStartPresenter.swift */; }; - EF02C9661F03C8EF58182997 /* StakingAmountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB76FEC075A6FE1D246BA5DD /* StakingAmountViewController.swift */; }; EFEB65B229DB34B4B526003B /* ParaStkStakeConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CDC7A44F6B01FE389F34C3A /* ParaStkStakeConfirmInteractor.swift */; }; EFF8F905CE4E8A212FE79EE4 /* ParaStkYourCollatorsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5E099C1E3DC3730DD503BE /* ParaStkYourCollatorsViewController.swift */; }; F022F1444E0F75CCA42F4648 /* YourValidatorListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31780E84948D7FE632ECB02 /* YourValidatorListProtocols.swift */; }; @@ -3515,10 +3759,10 @@ F0C3DB0CEE1975626B0014A8 /* StakingUnbondConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C34D496D0F57E685237B3A7 /* StakingUnbondConfirmInteractor.swift */; }; F0C3DCA3CD4F850C16406716 /* GovernanceDelegateSearchViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C197897ED8B6E5547C64CD2D /* GovernanceDelegateSearchViewFactory.swift */; }; F1BED07F67119E1BD052952A /* ReferendumVoteSetupWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B2C456A80FA66E6F140814 /* ReferendumVoteSetupWireframe.swift */; }; - F20C8D17ABF18B7104E14394 /* StakingAmountInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 312DE7ADA5ABC3214AD3D4AD /* StakingAmountInteractor.swift */; }; F27AAD7BC84793FA63027F8C /* AssetsSettingsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA134C6DB56DE1DFBA1B88B4 /* AssetsSettingsInteractor.swift */; }; F332FA8C330A16C3894B6542 /* WalletConnectSessionDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FA94E5B74194AABA482F2FD /* WalletConnectSessionDetailsProtocols.swift */; }; F35B520D7955A70588AB593C /* ReferendumVoteConfirmWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C784EDFB9227884E30FBE6E /* ReferendumVoteConfirmWireframe.swift */; }; + F3719F7C2AD0B75FC271DCE9 /* NPoolsRedeemViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B13FCAD745A663807219B1 /* NPoolsRedeemViewFactory.swift */; }; F382BF4F8C3C46C7C21DE5C0 /* ParaStkUnstakeConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86DCB6F3977BDE1BDC7BC3F9 /* ParaStkUnstakeConfirmPresenter.swift */; }; F3BB50CCA38C9B47FDBEDF53 /* ReferendumVotersInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23CC3812E4DFC26484324D57 /* ReferendumVotersInteractor.swift */; }; F3D2AC37709EAF088A594B73 /* AccountManagementViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FCA2DD3A8898D64CBC9F97 /* AccountManagementViewController.swift */; }; @@ -3636,20 +3880,27 @@ F5F74CD4110DB94902AA836A /* ParaStkRebondViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F21FBC9BE578A300A77E9C5 /* ParaStkRebondViewLayout.swift */; }; F63A83EA1CA85D7A43103098 /* DAppOperationConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BB7BFB2477E9F3893126931 /* DAppOperationConfirmViewFactory.swift */; }; F65CC130E018157C0778B074 /* DelegationListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF16BF87D3DEA31E6003C969 /* DelegationListWireframe.swift */; }; + F690C666F5A95D0C8A48BD45 /* NPoolsClaimRewardsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B213965350582497E2F86E26 /* NPoolsClaimRewardsPresenter.swift */; }; F6F73F4862779FE0B58A7931 /* ParaStkRebondInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1BCCE09BB15106FDC02495 /* ParaStkRebondInteractor.swift */; }; F75C2AAA76C49EDB8174B590 /* ParaStkCollatorFiltersWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038B1C35376B62CFE28C403D /* ParaStkCollatorFiltersWireframe.swift */; }; F78EA110179B6D75DDF53F8B /* AccountCreateWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACAEDA02F409FE23749A1551 /* AccountCreateWireframe.swift */; }; + F7D3092FDF42D9356654D85A /* StartStakingConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AA546FF04D7C9F741125567 /* StartStakingConfirmInteractor.swift */; }; F7EB8F835CFA7FC949EF4C22 /* YourValidatorListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 906C55FC079AF6112AF0745B /* YourValidatorListWireframe.swift */; }; + F7FB7376B1F3918B3751DAA2 /* NPoolsUnstakeConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D9131BF410C62A93646CA0A /* NPoolsUnstakeConfirmPresenter.swift */; }; F83EA1F4ECE14C390C0B287F /* StakingUnbondSetupInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D101339CC1292531CC4DB0AC /* StakingUnbondSetupInteractor.swift */; }; F85E4E18D7D535538D52B950 /* ParaStkSelectCollatorsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE545FC135A5D1C49CE3770 /* ParaStkSelectCollatorsViewLayout.swift */; }; F85F1BCAD47F0596FBFBA110 /* ParaStkRebondWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2B583DB30C6C818B0F952D /* ParaStkRebondWireframe.swift */; }; F88D85C73094F6A1FC494D87 /* DAppSearchWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A191B92AD171FDDDD8C30E2 /* DAppSearchWireframe.swift */; }; F8C0CA3DDBCB5E509295F099 /* MarkdownDescriptionViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF9ED27CF12B7DA8B1378CF /* MarkdownDescriptionViewFactory.swift */; }; F92E73C24AB577F37B35649E /* WalletConnectSessionDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B14339E7BE9FE045C6A2AB52 /* WalletConnectSessionDetailsInteractor.swift */; }; + F9E6E306BCE32992EA9ABF3E /* NominationPoolBondMoreConfirmProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894A6BB613EA991A09976B30 /* NominationPoolBondMoreConfirmProtocols.swift */; }; + FA0E6F6A12CA290C7079AC6C /* StakingSetupAmountPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F80E6C0C3AC88D7CDB8204F /* StakingSetupAmountPresenter.swift */; }; FA62AACACA15CB04275DE957 /* ParaStkYourCollatorsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A2FD02269432066884F5AF /* ParaStkYourCollatorsInteractor.swift */; }; FA894DFA8EEBB0B4562CD788 /* LedgerAccountConfirmationViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 392B5AA43C68E640C9FDEE04 /* LedgerAccountConfirmationViewFactory.swift */; }; FB405A41F9B89097016D4C78 /* ChainAddressDetailsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 245DED717B5B3FC380C24E3D /* ChainAddressDetailsWireframe.swift */; }; FBB0A83A6F48E4E0EE15E11A /* DelegateVotedReferendaInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E09466B1FD637D4649D7CED /* DelegateVotedReferendaInteractor.swift */; }; + FCBFD26B960C4837646A0D86 /* NPoolsClaimRewardsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF389223A781CA2088C7A4DD /* NPoolsClaimRewardsWireframe.swift */; }; + FD322DC05438902ED369E8FA /* NPoolsRedeemWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A651A934B32A7978850D639 /* NPoolsRedeemWireframe.swift */; }; FD43B68CFBD5C3497B446F53 /* ChangeWatchOnlyProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAB80E4CA0D5FA56612318A2 /* ChangeWatchOnlyProtocols.swift */; }; FDE2CA45061C620567AC329C /* DAppBrowserViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 677CE34BFAB45122C57095F6 /* DAppBrowserViewFactory.swift */; }; FED2AE2928BD8A4301138CAC /* DAppSettingsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCA43AABAC7555C2E648442F /* DAppSettingsViewFactory.swift */; }; @@ -3687,6 +3938,7 @@ 0100701AA69652CB91ACBD97 /* AssetListInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetListInteractor.swift; sourceTree = ""; }; 01A9D9D13EC9D921A2C8FB6D /* DAppAuthConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAuthConfirmWireframe.swift; sourceTree = ""; }; 01B2EBE8C02491A06121705A /* GovernanceUnlockConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnlockConfirmViewController.swift; sourceTree = ""; }; + 01D9C4A550C6DDF85491B8A0 /* NPoolsUnstakeSetupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeSetupInteractor.swift; sourceTree = ""; }; 024B7E67C0603C53981EC394 /* ReferendumVoteSetupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVoteSetupInteractor.swift; sourceTree = ""; }; 025FA616E9CFA2BC7C364B74 /* ReferendumsFiltersViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumsFiltersViewFactory.swift; sourceTree = ""; }; 02ACCC85B2CCF3D9392CA9B4 /* CrowdloanListProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanListProtocols.swift; sourceTree = ""; }; @@ -3703,9 +3955,12 @@ 0533F9E531CDFB721D697769 /* YourValidatorListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourValidatorListPresenter.swift; sourceTree = ""; }; 057EF626183878DD2C9E7BC7 /* DAppAddFavoriteViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAddFavoriteViewController.swift; sourceTree = ""; }; 05AA59AFC801F52B79DDBBCF /* TokensManageProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManageProtocols.swift; sourceTree = ""; }; + 060D52F9BFFB646BF9FCC968 /* StakingSelectPoolPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSelectPoolPresenter.swift; sourceTree = ""; }; 06268C2A9EAC65722D6E8947 /* InAppUpdatesViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = InAppUpdatesViewLayout.swift; sourceTree = ""; }; + 06D9A7B84C1B985D33E72D84 /* NominationPoolSearchWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolSearchWireframe.swift; sourceTree = ""; }; 07D08AF2C744DF2073702499 /* DAppListViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppListViewFactory.swift; sourceTree = ""; }; 07D165A41A0F3F0EC1926175 /* StakingMoreOptionsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionsProtocols.swift; sourceTree = ""; }; + 080DF5C2C6DEC79D8324F084 /* NPoolsUnstakeConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeConfirmInteractor.swift; sourceTree = ""; }; 0912F5E8BA170342D52F7D38 /* TransferConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferConfirmViewController.swift; sourceTree = ""; }; 0988D8EC0768B03BA7C55612 /* ParaStkYieldBoostStartInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStartInteractor.swift; sourceTree = ""; }; 09E4E9A052F9F04A92F158D6 /* GovernanceDelegateSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateSetupWireframe.swift; sourceTree = ""; }; @@ -3715,6 +3970,28 @@ 0B5072E250B7277F605855B3 /* AccountManagementProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountManagementProtocols.swift; sourceTree = ""; }; 0B556386CEEB76C95ED59897 /* ControllerAccountViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountViewController.swift; sourceTree = ""; }; 0B62C2CBCFF1865A1CA0F1B4 /* LedgerNetworkSelectionProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerNetworkSelectionProtocols.swift; sourceTree = ""; }; + 0BA85EE628D7029AC940DFA3 /* NominationPoolBondMoreBasePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreBasePresenter.swift; sourceTree = ""; }; + 0C04290B2A67A42A00C3583A /* SubstrateDataModel17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel17.xcdatamodel; sourceTree = ""; }; + 0C12A2462AA1D96000C7FA49 /* SubqueryMultistakingTypeFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubqueryMultistakingTypeFactory.swift; sourceTree = ""; }; + 0C12A2482AA35AE300C7FA49 /* RelaychainStakingValidatorFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaychainStakingValidatorFacade.swift; sourceTree = ""; }; + 0C13D2F42A7D2B440054BB6F /* DirectStakingRecommendationMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectStakingRecommendationMediator.swift; sourceTree = ""; }; + 0C13D2F62A7D45F40054BB6F /* RelaychainStakingRestrictions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaychainStakingRestrictions.swift; sourceTree = ""; }; + 0C13D2F92A7D473B0054BB6F /* DirectStakingRestrictionsBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectStakingRestrictionsBuilder.swift; sourceTree = ""; }; + 0C13D2FB2A7D4B220054BB6F /* RelaychainStakingRestrictionsBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaychainStakingRestrictionsBuilder.swift; sourceTree = ""; }; + 0C13D2FD2A7D4F500054BB6F /* PoolStakingRestrictionsBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoolStakingRestrictionsBuilder.swift; sourceTree = ""; }; + 0C13D2FF2A7D50C10054BB6F /* PoolStakingRecommendationMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoolStakingRecommendationMediator.swift; sourceTree = ""; }; + 0C13D3012A7D53C10054BB6F /* HybridStakingRecommendationMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HybridStakingRecommendationMediator.swift; sourceTree = ""; }; + 0C13D3032A7D56CC0054BB6F /* StakingRecommendationMediatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRecommendationMediatorFactory.swift; sourceTree = ""; }; + 0C13D3062A7FB92C0054BB6F /* NominationPoolsJoin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsJoin.swift; sourceTree = ""; }; + 0C13D3102A80CA9A0054BB6F /* StakingDataValidatorFactory+Plank.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StakingDataValidatorFactory+Plank.swift"; sourceTree = ""; }; + 0C13D3122A80D06B0054BB6F /* StakingRecommendationValidationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRecommendationValidationFactory.swift; sourceTree = ""; }; + 0C13D3172A8216A10054BB6F /* NominationPoolsIconFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsIconFactory.swift; sourceTree = ""; }; + 0C13D3192A8222D20054BB6F /* StartStakingConfirmInteractorError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingConfirmInteractorError.swift; sourceTree = ""; }; + 0C13D31C2A82279D0054BB6F /* StartStakingDirectConfirmPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingDirectConfirmPresenter.swift; sourceTree = ""; }; + 0C13D31E2A8227AB0054BB6F /* StartStakingPoolConfirmPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingPoolConfirmPresenter.swift; sourceTree = ""; }; + 0C13D3202A822B220054BB6F /* StartStakingDirectConfirmWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingDirectConfirmWireframe.swift; sourceTree = ""; }; + 0C13D3232A823D810054BB6F /* StartStakingExtrinsicProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingExtrinsicProxy.swift; sourceTree = ""; }; + 0C13D3252A82753F0054BB6F /* StartStakingFeeIdFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingFeeIdFactory.swift; sourceTree = ""; }; 0C154A0D2A45995500932C3F /* CompoundComparator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompoundComparator.swift; sourceTree = ""; }; 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletViewModelObserverContainer.swift; sourceTree = ""; }; 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyPresentable.swift; sourceTree = ""; }; @@ -3731,6 +4008,17 @@ 0C2B3C9875FDA7EE8D168900 /* ParaStkYieldBoostSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostSetupWireframe.swift; sourceTree = ""; }; 0C2B583DB30C6C818B0F952D /* ParaStkRebondWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkRebondWireframe.swift; sourceTree = ""; }; 0C2F867F2A7119D400593C01 /* AuraSessionLengthOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuraSessionLengthOperationFactory.swift; sourceTree = ""; }; + 0C2F86812A7233DC00593C01 /* EraNominationPoolsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraNominationPoolsService.swift; sourceTree = ""; }; + 0C2F86832A72343800593C01 /* EraNominationPoolsServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraNominationPoolsServiceProtocol.swift; sourceTree = ""; }; + 0C2F86852A72352400593C01 /* NominationPoolModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolModel.swift; sourceTree = ""; }; + 0C2F86882A723E5400593C01 /* NominationPoolsOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsOperationFactory.swift; sourceTree = ""; }; + 0C2F868A2A725C3C00593C01 /* EraNominationPoolsChanged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraNominationPoolsChanged.swift; sourceTree = ""; }; + 0C2F868C2A725E4F00593C01 /* AccountExistense.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountExistense.swift; sourceTree = ""; }; + 0C2F868D2A725E4F00593C01 /* DefaultStakingRewardDestination.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultStakingRewardDestination.swift; sourceTree = ""; }; + 0C2F86922A72648D00593C01 /* ActiveNominationPoolsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveNominationPoolsTests.swift; sourceTree = ""; }; + 0C2F86952A72807E00593C01 /* NominationPoolsRewardEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsRewardEngine.swift; sourceTree = ""; }; + 0C2F86972A728EE900593C01 /* NPoolsRewardEngineFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsRewardEngineFactory.swift; sourceTree = ""; }; + 0C2F86992A72948100593C01 /* NominationPoolsApyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsApyTests.swift; sourceTree = ""; }; 0C3205BA2A8679F0002EB914 /* EvmGasPriceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmGasPriceProvider.swift; sourceTree = ""; }; 0C3205BD2A867A9C002EB914 /* EvmLegacyGasPriceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmLegacyGasPriceProvider.swift; sourceTree = ""; }; 0C3205BF2A867DD6002EB914 /* EvmMaxPriorityGasPriceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmMaxPriorityGasPriceProvider.swift; sourceTree = ""; }; @@ -3760,32 +4048,147 @@ 0C463FC72A58126A003E71C9 /* UIView+MotionEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+MotionEffect.swift"; sourceTree = ""; }; 0C463FCF2A592ACD003E71C9 /* PartialInterpolatingMotionEffect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PartialInterpolatingMotionEffect.swift; sourceTree = ""; }; 0C53649F2A4D6EB700990478 /* AssetListBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListBuilder.swift; sourceTree = ""; }; + 0C543E962AAB1B350035F45F /* ElectedAndPrefValidators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElectedAndPrefValidators.swift; sourceTree = ""; }; 0C56B4FA2A4B0C320030F9C9 /* AssetListBaseBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListBaseBuilder.swift; sourceTree = ""; }; 0C56B4FC2A4B0CA90030F9C9 /* AssetListBuilderResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListBuilderResult.swift; sourceTree = ""; }; + 0C59E8C72AA5C3F4001E11F3 /* SubstrateDataModel19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel19.xcdatamodel; sourceTree = ""; }; + 0C59E8C82AA5C7EC001E11F3 /* ExternalAssetBalance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalAssetBalance.swift; sourceTree = ""; }; + 0C59E8CC2AA5D673001E11F3 /* PooledBalanceUpdatingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PooledBalanceUpdatingService.swift; sourceTree = ""; }; + 0C59E8CE2AA5D744001E11F3 /* PooledBalanceUpdatingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PooledBalanceUpdatingState.swift; sourceTree = ""; }; + 0C59E8D02AA5FAC5001E11F3 /* PooledAssetBalance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PooledAssetBalance.swift; sourceTree = ""; }; + 0C59E8D22AA5FBE2001E11F3 /* PooledAssetBalanceMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PooledAssetBalanceMapper.swift; sourceTree = ""; }; + 0C59E8D42AA5FC96001E11F3 /* ExternalAssetBalanceMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalAssetBalanceMapper.swift; sourceTree = ""; }; + 0C59E8D72AA60490001E11F3 /* ExternalAssetBalanceStreambleSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalAssetBalanceStreambleSource.swift; sourceTree = ""; }; + 0C59E8D92AA6085B001E11F3 /* ExternalAssetBalanceLocalSubscriptionFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalAssetBalanceLocalSubscriptionFactory.swift; sourceTree = ""; }; + 0C59E8DB2AA60C3B001E11F3 /* NSPredicate+ExternalAssetBalance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPredicate+ExternalAssetBalance.swift"; sourceTree = ""; }; + 0C59E8DE2AA60DAB001E11F3 /* ExternalAssetBalanceServiceFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalAssetBalanceServiceFactoryProtocol.swift; sourceTree = ""; }; + 0C59E8E02AA60FF0001E11F3 /* CrowdloanExternalServiceFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanExternalServiceFactory.swift; sourceTree = ""; }; + 0C59E8E22AA61252001E11F3 /* NominationPoolExternalServiceFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolExternalServiceFactory.swift; sourceTree = ""; }; + 0C59E8E42AA6191E001E11F3 /* ExternalAssetBalanceSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalAssetBalanceSubscriber.swift; sourceTree = ""; }; + 0C59E8E62AA61933001E11F3 /* ExternalAssetBalanceSubscriptionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalAssetBalanceSubscriptionHandler.swift; sourceTree = ""; }; + 0C59E8EA2AA71C3E001E11F3 /* ExternalAssetBalanceIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalAssetBalanceIntegrationTests.swift; sourceTree = ""; }; + 0C59E8EC2AA75C84001E11F3 /* HistoryPoolRewardContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryPoolRewardContext.swift; sourceTree = ""; }; + 0C59E8EF2AA76361001E11F3 /* OperationDetailsDataProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationDetailsDataProviderProtocol.swift; sourceTree = ""; }; + 0C59E8F12AA76436001E11F3 /* OperationDetailsTransferProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationDetailsTransferProvider.swift; sourceTree = ""; }; + 0C59E8F32AA7649E001E11F3 /* OperationDetailsBaseProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationDetailsBaseProvider.swift; sourceTree = ""; }; + 0C59E8F52AA76772001E11F3 /* OperationDetailsExtrinsicProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationDetailsExtrinsicProvider.swift; sourceTree = ""; }; + 0C59E8F72AA76833001E11F3 /* OperationDetailsContractProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationDetailsContractProvider.swift; sourceTree = ""; }; + 0C59E8F92AA76A4A001E11F3 /* OperationDetailsDirectStakingProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationDetailsDirectStakingProvider.swift; sourceTree = ""; }; + 0C59E8FB2AA76C4A001E11F3 /* OperationDetailsPoolStakingProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationDetailsPoolStakingProvider.swift; sourceTree = ""; }; + 0C59E8FD2AA773D6001E11F3 /* OperationDetailsDataProviderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationDetailsDataProviderFactory.swift; sourceTree = ""; }; + 0C626D1A2A8F519100CDAF4E /* StakingMainPresenterFactory+NominationPools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StakingMainPresenterFactory+NominationPools.swift"; sourceTree = ""; }; + 0C626D1C2A915E2F00CDAF4E /* StakingNominationPoolsStatics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingNominationPoolsStatics.swift; sourceTree = ""; }; + 0C626D1E2A92AA0F00CDAF4E /* NominationPoolsDataProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsDataProviding.swift; sourceTree = ""; }; + 0C626D202A933A7D00CDAF4E /* StakingNPoolsViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingNPoolsViewModelFactory.swift; sourceTree = ""; }; + 0C66102A2A73816000E44634 /* StakingSharedStateFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingSharedStateFactory.swift; sourceTree = ""; }; + 0C66102C2A73828800E44634 /* RelaychainStakingSharedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaychainStakingSharedState.swift; sourceTree = ""; }; + 0C66102E2A78E9D700E44634 /* RelaychainStartStakingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaychainStartStakingState.swift; sourceTree = ""; }; 0C6D66AA2A8C0B6700AAB988 /* BaseSigner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseSigner.swift; sourceTree = ""; }; + 0C6F0C9C2A69723B007170C6 /* StartStakingStateProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartStakingStateProtocol.swift; sourceTree = ""; }; + 0C77B55E2A83717000B5AE08 /* StaticValidatorListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticValidatorListViewController.swift; sourceTree = ""; }; + 0C77B5602A8371AA00B5AE08 /* StaticValidatorListProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticValidatorListProtocols.swift; sourceTree = ""; }; + 0C77B5622A83747200B5AE08 /* StaticValidatorListViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticValidatorListViewLayout.swift; sourceTree = ""; }; + 0C77B5642A8374EA00B5AE08 /* StaticValidatorListPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticValidatorListPresenter.swift; sourceTree = ""; }; + 0C77B5662A837AC500B5AE08 /* StaticValidatorListWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticValidatorListWireframe.swift; sourceTree = ""; }; + 0C77B5682A837D4000B5AE08 /* StaticValidatorListViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticValidatorListViewFactory.swift; sourceTree = ""; }; 0C797A5B5863A026E84062AE /* MessageSheetProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MessageSheetProtocols.swift; sourceTree = ""; }; + 0C79C8912A7BD9BB00B171E3 /* SelectedStakingOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedStakingOption.swift; sourceTree = ""; }; + 0C79C8942A7BE01100B171E3 /* RelaychainStakingRecommendation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaychainStakingRecommendation.swift; sourceTree = ""; }; + 0C79C8982A7BE46A00B171E3 /* AssetModel+Staking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetModel+Staking.swift"; sourceTree = ""; }; + 0C79C89B2A7BE6A200B171E3 /* DirectStakingRecommendationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectStakingRecommendationFactory.swift; sourceTree = ""; }; + 0C79C89D2A7BEF1200B171E3 /* NominationPoolRecommendationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolRecommendationFactory.swift; sourceTree = ""; }; + 0C79C89F2A7BF80700B171E3 /* RelaychainStakingRecommendationMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaychainStakingRecommendationMediator.swift; sourceTree = ""; }; + 0C7C88602A94A54E00DD96A1 /* StakingNPoolsViewModelFactory+Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StakingNPoolsViewModelFactory+Alerts.swift"; sourceTree = ""; }; + 0C7C88632A94E09F00DD96A1 /* NPoolsPendingRewardDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsPendingRewardDataSource.swift; sourceTree = ""; }; + 0C7C88652A95030800DD96A1 /* SubqueryStakingType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubqueryStakingType.swift; sourceTree = ""; }; + 0C7C88672A95563100DD96A1 /* StakingClaimableRewardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingClaimableRewardView.swift; sourceTree = ""; }; + 0C7C88692A95591900DD96A1 /* StakingTotalRewardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingTotalRewardView.swift; sourceTree = ""; }; + 0C7C886B2A9622F800DD96A1 /* StakingSelectedEntityViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingSelectedEntityViewModel.swift; sourceTree = ""; }; + 0C7C886D2A962B0D00DD96A1 /* StackAddressCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackAddressCell.swift; sourceTree = ""; }; + 0C7E7FAA2A9F27FB00596628 /* NominationPoolsRedeemCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsRedeemCall.swift; sourceTree = ""; }; 0C83775C2A4EEB380072102D /* AssetListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListState.swift; sourceTree = ""; }; + 0C893E692A65591C00781503 /* PoolsMultistakingUpdateService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoolsMultistakingUpdateService.swift; sourceTree = ""; }; + 0C893E6C2A6562B400781503 /* NominationPools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPools.swift; sourceTree = ""; }; + 0C893E6E2A65702A00781503 /* NominationPools+CodingPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NominationPools+CodingPath.swift"; sourceTree = ""; }; 0C8A25582A553A6C0072882A /* KeyboardAppearanceState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardAppearanceState.swift; sourceTree = ""; }; + 0C9525E22A7AAB2A00BD724D /* StakingTimeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingTimeModel.swift; sourceTree = ""; }; + 0C9525E42A7AF82B00BD724D /* DirectStakingMinStakeBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectStakingMinStakeBuilder.swift; sourceTree = ""; }; + 0C9525E62A7AFA2C00BD724D /* ValueResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValueResolver.swift; sourceTree = ""; }; + 0C9525EA2A7B7F5000BD724D /* ChainModel+Additional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChainModel+Additional.swift"; sourceTree = ""; }; + 0C962F852AA859F200C0B551 /* TransactionHistoryLocalFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionHistoryLocalFilter.swift; sourceTree = ""; }; + 0C962F872AA85C7F00C0B551 /* TransactionHistoryAccountPrefixFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionHistoryAccountPrefixFilter.swift; sourceTree = ""; }; + 0C962F892AA8614500C0B551 /* TransactionHistoryLocalFilterFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionHistoryLocalFilterFactory.swift; sourceTree = ""; }; 0C9680F02A8A85BB006A411B /* TokenAddValidationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenAddValidationFactory.swift; sourceTree = ""; }; 0C9680F22A8AC2F2006A411B /* EvmTokenAddResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvmTokenAddResult.swift; sourceTree = ""; }; 0C9C642C2A8CE30A004DC078 /* SystemAccountValidating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemAccountValidating.swift; sourceTree = ""; }; + 0C9C642F2A8D6779004DC078 /* StakingNPoolsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingNPoolsPresenter.swift; sourceTree = ""; }; + 0C9C64312A8D67A0004DC078 /* StakingNPoolsInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingNPoolsInteractor.swift; sourceTree = ""; }; + 0C9C64332A8D67AF004DC078 /* StakingNPoolsWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingNPoolsWireframe.swift; sourceTree = ""; }; + 0C9C64352A8D67FB004DC078 /* StakingNPoolsProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingNPoolsProtocols.swift; sourceTree = ""; }; + 0C9C64372A8D6949004DC078 /* NPoolsStakingSharedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsStakingSharedState.swift; sourceTree = ""; }; + 0C9C64392A8DF97E004DC078 /* StakingNPoolsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingNPoolsError.swift; sourceTree = ""; }; 0C9ECB592A4A9AB400BDCA73 /* AssetListAccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListAccountCell.swift; sourceTree = ""; }; 0CAC01542A52E0CC0069413E /* AssetListModelHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListModelHelpers.swift; sourceTree = ""; }; 0CAC01562A52E1960069413E /* AssetListPresenterHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListPresenterHelpers.swift; sourceTree = ""; }; + 0CAC44A92A7A79C2001EDE61 /* StartStakingInfoParachainWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoParachainWireframe.swift; sourceTree = ""; }; + 0CAC44AB2A7A7FFD001EDE61 /* RelaychainConsensusStateDepending.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaychainConsensusStateDepending.swift; sourceTree = ""; }; 0CB064672A403ADE00BFBA3F /* AmountDecimal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountDecimal.swift; sourceTree = ""; }; 0CB064692A40572C00BFBA3F /* AmountInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountInputViewModel.swift; sourceTree = ""; }; + 0CB06E722A6800F500C7EC99 /* NominationPools+Functions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NominationPools+Functions.swift"; sourceTree = ""; }; + 0CB06E742A68139C00C7EC99 /* StakingDashboardNominationPoolMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingDashboardNominationPoolMapper.swift; sourceTree = ""; }; + 0CB261D82A9893E500287305 /* NPoolsUnstakeBaseProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeBaseProtocols.swift; sourceTree = ""; }; + 0CB261DA2A98943800287305 /* NPoolsUnstakeBaseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeBaseError.swift; sourceTree = ""; }; + 0CB261DD2A989D2A00287305 /* NominationPoolsUnstakeLimits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsUnstakeLimits.swift; sourceTree = ""; }; + 0CB261DF2A98BEBD00287305 /* NPoolsUnstakeBaseInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeBaseInteractor.swift; sourceTree = ""; }; + 0CB261E12A9B215B00287305 /* NominationPoolUnstake.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolUnstake.swift; sourceTree = ""; }; + 0CB261E32A9BE31B00287305 /* NPoolsUnstakeBasePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeBasePresenter.swift; sourceTree = ""; }; + 0CB261E62A9C7C9D00287305 /* NPoolsUnstakeHintsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeHintsFactory.swift; sourceTree = ""; }; + 0CB261E92A9C940A00287305 /* NominationPoolsUnstakeOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsUnstakeOperationFactory.swift; sourceTree = ""; }; + 0CB261EE2A9E103900287305 /* NPoolsClaimRewardsStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsClaimRewardsStrategy.swift; sourceTree = ""; }; + 0CB261F02A9E149C00287305 /* NPoolsClaimRewardsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsClaimRewardsError.swift; sourceTree = ""; }; + 0CB261F22A9E182300287305 /* NominationPoolClaimRewards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolClaimRewards.swift; sourceTree = ""; }; + 0CB261F42A9E188300287305 /* NominationPoolsBondExtraCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsBondExtraCall.swift; sourceTree = ""; }; + 0CB261F62A9E2D8400287305 /* StackSwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackSwitchCell.swift; sourceTree = ""; }; + 0CB261F82A9F1F2200287305 /* NPoolsRedeemError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsRedeemError.swift; sourceTree = ""; }; 0CBC29C52A421B5000F7B1F7 /* StakingMainWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMainWireframe.swift; sourceTree = ""; }; 0CBC29C72A42AE2F00F7B1F7 /* StakingDashboardBuilderResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingDashboardBuilderResult.swift; sourceTree = ""; }; + 0CC2E55C2A6AAFFD004092E7 /* SubstrateDataModel18.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel18.xcdatamodel; sourceTree = ""; }; + 0CC2E55D2A6AB2B7004092E7 /* StashItemMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StashItemMapper.swift; sourceTree = ""; }; + 0CC2E55F2A6E44E7004092E7 /* NominationPoolsRemoteSubscriptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsRemoteSubscriptionService.swift; sourceTree = ""; }; + 0CC2E5612A6E5C43004092E7 /* NominationPoolsAccountUpdatingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsAccountUpdatingService.swift; sourceTree = ""; }; + 0CC2E5632A6E5C72004092E7 /* NPoolsLocalSubscriptionFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsLocalSubscriptionFactory.swift; sourceTree = ""; }; + 0CC2E5652A6E64EC004092E7 /* NPoolsLocalStorageSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsLocalStorageSubscriber.swift; sourceTree = ""; }; + 0CC2E5672A6E64FD004092E7 /* NPoolsLocalStorageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NPoolsLocalStorageHandler.swift; sourceTree = ""; }; + 0CC2E5692A6E6EBB004092E7 /* LocalStorageProviderObserving.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalStorageProviderObserving.swift; sourceTree = ""; }; + 0CC41A9A168AF0F1FBE5F799 /* Pods_novawalletAll_novawallet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_novawalletAll_novawallet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0CC4CCF32A67C9C400F63041 /* Multistaking+NominationPools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Multistaking+NominationPools.swift"; sourceTree = ""; }; + 0CC6C8D72AAB401200AD8D9B /* CustomValidatorsFullList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomValidatorsFullList.swift; sourceTree = ""; }; 0CCE25202A44306200286709 /* TransactionHistoryPhishingFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionHistoryPhishingFilter.swift; sourceTree = ""; }; 0CDFFCC54A504417F4ACE7AA /* NftListInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftListInteractor.swift; sourceTree = ""; }; + 0CE1504F2A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsPoolSubscriptionService.swift; sourceTree = ""; }; + 0CE150532A70EA2200B61CC1 /* NominationPoolsSyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsSyncTests.swift; sourceTree = ""; }; 0CE550B22A49658700F0A7AC /* StakingDuration+Localizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StakingDuration+Localizable.swift"; sourceTree = ""; }; 0CE550B52A49741400F0A7AC /* StakingUnbondSetupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingUnbondSetupTests.swift; sourceTree = ""; }; + 0CE629D42AA9B5E200E250BD /* BalanceViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceViewModel.swift; sourceTree = ""; }; + 0CE629D52AA9B5E200E250BD /* BalanceViewModelFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceViewModelFactory.swift; sourceTree = ""; }; + 0CE629D82AA9B68C00E250BD /* AssetBalanceViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetBalanceViewModel.swift; sourceTree = ""; }; + 0CE629DB2AA9B6BE00E250BD /* RewardDestinationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RewardDestinationViewModel.swift; sourceTree = ""; }; + 0CE629DC2AA9B6BE00E250BD /* RewardDestinationViewModelFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RewardDestinationViewModelFactory.swift; sourceTree = ""; }; + 0CE629DF2AA9B70200E250BD /* CalculatedReward.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalculatedReward.swift; sourceTree = ""; }; + 0CE629E12AA9BE3000E250BD /* StakingSetupTypeEntityFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingSetupTypeEntityFacade.swift; sourceTree = ""; }; + 0CF193D02A843DA9003F12F6 /* StakingTypeBalanceFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StakingTypeBalanceFactory.swift; sourceTree = ""; }; + 0CF193D22A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedStakingViewModelFactory.swift; sourceTree = ""; }; + 0CF193D42A861925003F12F6 /* PredefinedTimeShortcut.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PredefinedTimeShortcut.swift; sourceTree = ""; }; + 0CF193D62A861D7E003F12F6 /* StartStakingInfoConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoConstants.swift; sourceTree = ""; }; 0D3FE2CE7F9F2836755DBA63 /* GovernanceUnlockConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnlockConfirmProtocols.swift; sourceTree = ""; }; + 0D65686560E2E6C18A5C34CB /* StartStakingInfoWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingInfoWireframe.swift; sourceTree = ""; }; 0D6E67AD564867E121601F18 /* WalletsListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsListPresenter.swift; sourceTree = ""; }; 0D9C85AB0C9D53B522DCF3C5 /* CreateWatchOnlyInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateWatchOnlyInteractor.swift; sourceTree = ""; }; + 0E4D3F275296975CA39280D5 /* StartStakingInfoBasePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingInfoBasePresenter.swift; sourceTree = ""; }; 0EC18369BDAF9076681B6E3F /* InAppUpdatesWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = InAppUpdatesWireframe.swift; sourceTree = ""; }; 0FA94E5B74194AABA482F2FD /* WalletConnectSessionDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletConnectSessionDetailsProtocols.swift; sourceTree = ""; }; 1039AA3654461114FBB86844 /* DAppTxDetailsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppTxDetailsPresenter.swift; sourceTree = ""; }; + 1090AF9D9F18846E5A73F73C /* NominationPoolSearchPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolSearchPresenter.swift; sourceTree = ""; }; 10E27EB6FF31F9D247DEFABB /* ParaStkStakeConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkStakeConfirmViewLayout.swift; sourceTree = ""; }; 10F3CCA5BA57C68D5AE2B42F /* DAppListProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppListProtocols.swift; sourceTree = ""; }; 1139FB38E7D8D25A36726089 /* ParaStkStakeConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkStakeConfirmProtocols.swift; sourceTree = ""; }; @@ -3793,12 +4196,13 @@ 123EC3706DD70AC132ECEACB /* GovernanceDelegateSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateSetupProtocols.swift; sourceTree = ""; }; 128EA612020D98F3B0D0FA96 /* ParaStkYourCollatorsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYourCollatorsProtocols.swift; sourceTree = ""; }; 12BEE17030110A1531A231EE /* DelegationReferendumVotersPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegationReferendumVotersPresenter.swift; sourceTree = ""; }; - 1313BBDD3A28AC9786B5B00E /* Pods_novawalletAll_novawallet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_novawalletAll_novawallet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 12C822BAC40271396E36ACBB /* NPoolsUnstakeConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeConfirmViewLayout.swift; sourceTree = ""; }; 1366336078BCA34EFB4C6FF9 /* CrowdloanContributionConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmInteractor.swift; sourceTree = ""; }; 14383723F0B56C91A0B3016E /* MessageSheetWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MessageSheetWireframe.swift; sourceTree = ""; }; 14E3337CDD7C831AEAA4582F /* CustomValidatorListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CustomValidatorListPresenter.swift; sourceTree = ""; }; 1521715CA2071056FCE9D360 /* TransferSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferSetupProtocols.swift; sourceTree = ""; }; 15A4DFCC4236D25A3D59F809 /* DAppBrowserInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppBrowserInteractor.swift; sourceTree = ""; }; + 15AC3897EC736F5096949BBC /* NominationPoolSearchViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolSearchViewFactory.swift; sourceTree = ""; }; 15E09DD01C1CC61EA5CDED9C /* InAppUpdatesInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = InAppUpdatesInteractor.swift; sourceTree = ""; }; 15E1472817503D3FB44BDC46 /* GovernanceDelegateSetupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateSetupInteractor.swift; sourceTree = ""; }; 16A05911AC400226DD29AE20 /* DAppWalletAuthViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppWalletAuthViewLayout.swift; sourceTree = ""; }; @@ -3833,6 +4237,7 @@ 1E5CB64B91B35804B3671456 /* ControllerAccountPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountPresenter.swift; sourceTree = ""; }; 1F3A05E0F46351784030D1AA /* ChainAddressDetailsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainAddressDetailsPresenter.swift; sourceTree = ""; }; 1F7865BACFB8591F67D8EE06 /* TransferConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferConfirmProtocols.swift; sourceTree = ""; }; + 1FA6F8EC6245BA34F26AE276 /* NPoolsUnstakeSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeSetupWireframe.swift; sourceTree = ""; }; 1FF860B3465854DCBC02DFB3 /* DAppBrowserPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppBrowserPresenter.swift; sourceTree = ""; }; 200C6B2C85846AED8CA9451A /* ExportMnemonicInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportMnemonicInteractor.swift; sourceTree = ""; }; 20878E303E9332322655F008 /* ParaStkSelectCollatorsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkSelectCollatorsProtocols.swift; sourceTree = ""; }; @@ -3850,6 +4255,7 @@ 23A74BDB54D503FA2BFBEF35 /* StakingUnbondSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondSetupProtocols.swift; sourceTree = ""; }; 23BC71941B91D3E372CDB11C /* CrowdloanContributionSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupViewLayout.swift; sourceTree = ""; }; 23CC3812E4DFC26484324D57 /* ReferendumVotersInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVotersInteractor.swift; sourceTree = ""; }; + 23EF6FA61F2B7E3B2ADD3200 /* NPoolsRedeemViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsRedeemViewLayout.swift; sourceTree = ""; }; 245DED717B5B3FC380C24E3D /* ChainAddressDetailsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainAddressDetailsWireframe.swift; sourceTree = ""; }; 24B23BAD22C87DA5F324B44F /* DAppAuthSettingsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAuthSettingsWireframe.swift; sourceTree = ""; }; 256215C11DC0E091660034EA /* CrowdloanYourContributionsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanYourContributionsViewController.swift; sourceTree = ""; }; @@ -3860,6 +4266,9 @@ 27A5489E97F846FE3D5931E5 /* ParaStkYieldBoostStopViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStopViewFactory.swift; sourceTree = ""; }; 27D5AF2F7609ADE855308089 /* AccountExportPasswordViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordViewController.swift; sourceTree = ""; }; 285212895DBAB0098F302DF9 /* ParaStkYieldBoostStopWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStopWireframe.swift; sourceTree = ""; }; + 2864B5E1A5AFC6A8C1B4B9E5 /* NPoolsUnstakeSetupViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeSetupViewController.swift; sourceTree = ""; }; + 2876FA98B3E8F7EBCF5DEED0 /* StartStakingConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingConfirmWireframe.swift; sourceTree = ""; }; + 2899566C339BA9562BE61F4F /* NPoolsUnstakeSetupPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeSetupPresenter.swift; sourceTree = ""; }; 289E4923B76F126DD8E3902B /* NftListViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftListViewFactory.swift; sourceTree = ""; }; 28AD18C155A1278B9B53CFDB /* ParaStkStakeSetupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkStakeSetupInteractor.swift; sourceTree = ""; }; 28F5B57A24265C36A5F19B78 /* CrowdloanContributionConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmPresenter.swift; sourceTree = ""; }; @@ -3867,12 +4276,15 @@ 29BF0B2686329E392C5DFB19 /* DelegationListProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegationListProtocols.swift; sourceTree = ""; }; 2A028D27275688AF0061CB4C /* AddChainAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddChainAccount.swift; sourceTree = ""; }; 2A028D29275688E80061CB4C /* AddChainAccount+AccountCreatePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AddChainAccount+AccountCreatePresenter.swift"; sourceTree = ""; }; + 2A5A2FE372A88ECF23B620D2 /* StakingSelectPoolProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSelectPoolProtocols.swift; sourceTree = ""; }; + 2A651A934B32A7978850D639 /* NPoolsRedeemWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsRedeemWireframe.swift; sourceTree = ""; }; 2A66CF4E25D109770006E4C1 /* CDPhishingItem+CoreDataDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CDPhishingItem+CoreDataDecodable.swift"; sourceTree = ""; }; 2A66CFAE25D10EDF0006E4C1 /* PhishingItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhishingItem.swift; sourceTree = ""; }; 2A719A9FC28373296AB195CB /* MessageSheetViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MessageSheetViewLayout.swift; sourceTree = ""; }; 2A84E87725D425750006FE9C /* AlertControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertControllerFactory.swift; sourceTree = ""; }; 2A9F8D4F274E4EB6003720E0 /* AccountCreateViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCreateViewLayout.swift; sourceTree = ""; }; 2A9F8D51274E4EC4003720E0 /* AccountCreateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCreateViewController.swift; sourceTree = ""; }; + 2AA546FF04D7C9F741125567 /* StartStakingConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingConfirmInteractor.swift; sourceTree = ""; }; 2AB7A7FE25CD0E7F00767D87 /* GitHubPhishingAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubPhishingAPIService.swift; sourceTree = ""; }; 2AC7BC7D2731604B001D99B0 /* ChainAccountChanged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainAccountChanged.swift; sourceTree = ""; }; 2AC7BC7F27319FC7001D99B0 /* BalanceLockType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceLockType.swift; sourceTree = ""; }; @@ -3883,6 +4295,7 @@ 2ACA4A5B186EE6D40BFE9D66 /* ExportMnemonicWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportMnemonicWireframe.swift; sourceTree = ""; }; 2AD0A18F25D3D1E100312428 /* GitHubPhishingServiceFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubPhishingServiceFactory.swift; sourceTree = ""; }; 2AD0A19425D3D3EC00312428 /* GitHubOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubOperationFactory.swift; sourceTree = ""; }; + 2ADA652B86975A2044ABB065 /* NominationPoolBondMoreConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreConfirmViewController.swift; sourceTree = ""; }; 2AF8204E274FD2110092E3E7 /* BaseAccountCreatePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseAccountCreatePresenter.swift; sourceTree = ""; }; 2AFF4B9F274D1E4D00D790B4 /* UsernameSetupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameSetupViewController.swift; sourceTree = ""; }; 2AFF4BA1274D1E5C00D790B4 /* UsernameSetupViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameSetupViewLayout.swift; sourceTree = ""; }; @@ -3890,22 +4303,25 @@ 2C3F5511D2E7AC283FF021E8 /* GovernanceRevokeDelegationConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceRevokeDelegationConfirmProtocols.swift; sourceTree = ""; }; 2DC70DB97D2E9350022A899B /* TokensManagePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManagePresenter.swift; sourceTree = ""; }; 2E4B0600AFFB96A75CF98755 /* StakingRedeemProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRedeemProtocols.swift; sourceTree = ""; }; + 2E5C5EE99A4B73789BE23039 /* Pods-novawalletAll-novawalletIntegrationTests.staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawalletIntegrationTests.staging.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests.staging.xcconfig"; sourceTree = ""; }; 2E800814C025B38C87CC282D /* TokensAddSelectNetworkViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensAddSelectNetworkViewFactory.swift; sourceTree = ""; }; 2ECD8589BD30A8BE9492AD87 /* StakingRewardPayoutsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardPayoutsPresenter.swift; sourceTree = ""; }; 2F10F130391C4B3652FE8F59 /* ParitySignerTxScanProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxScanProtocols.swift; sourceTree = ""; }; + 2F5A8E5D1C5A7F0B5B1570B9 /* NominationPoolBondMoreConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreConfirmViewLayout.swift; sourceTree = ""; }; 2F8A45125DF93218FC6C5119 /* ParaStkYourCollatorsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYourCollatorsViewFactory.swift; sourceTree = ""; }; 2FC9F0E50317E1583EA8E345 /* GovernanceUnlockSetupViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnlockSetupViewController.swift; sourceTree = ""; }; 301287CBBF23EF58186A7BB5 /* DAppAuthConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAuthConfirmViewLayout.swift; sourceTree = ""; }; 3089A0A7C992300CE839A050 /* ParaStkYieldBoostStartPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStartPresenter.swift; sourceTree = ""; }; 30D089568ABA686D509DF917 /* StakingRebagConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRebagConfirmViewLayout.swift; sourceTree = ""; }; 30E25CF67173500E0AC19387 /* GovernanceUnavailableTracksViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnavailableTracksViewController.swift; sourceTree = ""; }; + 30E42BA40ACDEA2914DE6435 /* NPoolsClaimRewardsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsClaimRewardsViewFactory.swift; sourceTree = ""; }; 30F3EC1C2DAE60DD6BB99B42 /* StakingUnbondConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondConfirmViewFactory.swift; sourceTree = ""; }; 31226053044986BC828AA912 /* AccountExportPasswordPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordPresenter.swift; sourceTree = ""; }; - 312DE7ADA5ABC3214AD3D4AD /* StakingAmountInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingAmountInteractor.swift; sourceTree = ""; }; 313B89948DC631DE61E01007 /* LedgerTxConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerTxConfirmInteractor.swift; sourceTree = ""; }; 313CDB5EB70518DEC1BDB392 /* MoonbeamTermsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MoonbeamTermsViewLayout.swift; sourceTree = ""; }; 3157288B4033B2264628C866 /* StakingMoreOptionsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionsViewFactory.swift; sourceTree = ""; }; 31AC2847EB686A6B2A16D8DF /* TokensManageInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManageInteractor.swift; sourceTree = ""; }; + 3246115B53EFE9461CD2F68B /* NPoolsUnstakeSetupViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeSetupViewFactory.swift; sourceTree = ""; }; 3251EE6FFC95D656BA2146F4 /* CrowdloanYourContributionsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanYourContributionsInteractor.swift; sourceTree = ""; }; 3280014154DCC105757E317C /* ParitySignerAddressesViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerAddressesViewController.swift; sourceTree = ""; }; 329C58A0AE09361F5ECD6D4E /* DAppSearchPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppSearchPresenter.swift; sourceTree = ""; }; @@ -3914,6 +4330,7 @@ 335E8C17DCB794733476AAE3 /* ParaStkStakeConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkStakeConfirmViewController.swift; sourceTree = ""; }; 336395FFC4B2104A9651A2DE /* StakingRewardPayoutsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardPayoutsViewFactory.swift; sourceTree = ""; }; 337EC62037D657258BCBC02F /* DAppWalletAuthInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppWalletAuthInteractor.swift; sourceTree = ""; }; + 33DE8A571FE8D0431FD934A9 /* Pods-novawalletAll-novawallet.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawallet.dev.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawallet/Pods-novawalletAll-novawallet.dev.xcconfig"; sourceTree = ""; }; 33DFAA0EEEA7F99C6D1CF4B1 /* ReferendumFullDetailsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumFullDetailsPresenter.swift; sourceTree = ""; }; 340D78F99A4FCDC04C012ED3 /* StakingRebagConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRebagConfirmInteractor.swift; sourceTree = ""; }; 3531B386DEF40108C34E7232 /* ReferendumVoteConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVoteConfirmViewLayout.swift; sourceTree = ""; }; @@ -3937,8 +4354,10 @@ 37FF46AB59967BF656E9EF1C /* AssetsSettingsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetsSettingsViewFactory.swift; sourceTree = ""; }; 38BBECA4E3F54769D95DD21E /* TokensAddSelectNetworkViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensAddSelectNetworkViewController.swift; sourceTree = ""; }; 392B5AA43C68E640C9FDEE04 /* LedgerAccountConfirmationViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerAccountConfirmationViewFactory.swift; sourceTree = ""; }; + 3930902D540DB0B9A2CFD21C /* StakingTypeViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingTypeViewController.swift; sourceTree = ""; }; 3934C46625930FA8D171D3E7 /* AssetReceivePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetReceivePresenter.swift; sourceTree = ""; }; 39503B664F159E5D07FF6281 /* ParaStkCollatorFiltersPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkCollatorFiltersPresenter.swift; sourceTree = ""; }; + 397F057FD5B16A58E5F30F07 /* NPoolsUnstakeConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeConfirmWireframe.swift; sourceTree = ""; }; 39907750D40A8DD7FE1288C8 /* CreateWatchOnlyViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateWatchOnlyViewController.swift; sourceTree = ""; }; 399700B22225DD916DFACAF9 /* DelegateVotedReferendaViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegateVotedReferendaViewFactory.swift; sourceTree = ""; }; 3A46EE888D60C1538A0A3EFC /* NftDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsProtocols.swift; sourceTree = ""; }; @@ -3958,6 +4377,7 @@ 3CAF58F7D0659E89B66B75E4 /* GovernanceRevokeDelegationTracksViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceRevokeDelegationTracksViewController.swift; sourceTree = ""; }; 3D2A26EC9537BD4275A03272 /* OperationDetailsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OperationDetailsPresenter.swift; sourceTree = ""; }; 3D383344BEDAEDC76A6BE2CE /* DAppTxDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppTxDetailsProtocols.swift; sourceTree = ""; }; + 3D931049E1775C8D9C4EEC9D /* Pods_novawalletTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_novawalletTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3E09466B1FD637D4649D7CED /* DelegateVotedReferendaInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegateVotedReferendaInteractor.swift; sourceTree = ""; }; 3E5EB48ADB08405BF51F087D /* GovernanceRemoveVotesConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceRemoveVotesConfirmWireframe.swift; sourceTree = ""; }; 3E6E41F045986002E1E26C12 /* DAppListWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppListWireframe.swift; sourceTree = ""; }; @@ -3973,13 +4393,13 @@ 3FECFFBAB264397F9B2646CE /* ChangeWatchOnlyViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChangeWatchOnlyViewController.swift; sourceTree = ""; }; 408CF7752F4F638FA29DFE4A /* ParitySignerTxScanPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxScanPresenter.swift; sourceTree = ""; }; 40B1EE1E66B8325C47D8A404 /* GovernanceDelegateSearchProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateSearchProtocols.swift; sourceTree = ""; }; - 40B47961B2254E8A4D8EC588 /* StakingAmountPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingAmountPresenter.swift; sourceTree = ""; }; 4191E0055768541F6A3D8A61 /* StakingRewardPayoutsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardPayoutsInteractor.swift; sourceTree = ""; }; 41DFB2757D029FB5DF3CEBC2 /* WalletHistoryFilterProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilterProtocols.swift; sourceTree = ""; }; 43AC83139115B51457C9961F /* GovernanceDelegateInfoProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateInfoProtocols.swift; sourceTree = ""; }; 43AE3AE09F98E37C15BA87A1 /* LedgerInstructionsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerInstructionsWireframe.swift; sourceTree = ""; }; 43FBC2EA83A121CEBD25549D /* DAppOperationConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppOperationConfirmViewController.swift; sourceTree = ""; }; 44809BCF44D7329266A60A9D /* ParitySignerAddConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerAddConfirmInteractor.swift; sourceTree = ""; }; + 44AA632DE49B746BC38B959F /* NominationPoolSearchInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolSearchInteractor.swift; sourceTree = ""; }; 44DDD9500E4A1040D863BC1E /* ParaStkStakeSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkStakeSetupViewLayout.swift; sourceTree = ""; }; 44EAF50AAF6C23225E06C16C /* AssetsSearchInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetsSearchInteractor.swift; sourceTree = ""; }; 45C0B1C175A2470AAA50DAC5 /* DAppSettingsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppSettingsViewController.swift; sourceTree = ""; }; @@ -3995,6 +4415,7 @@ 48182DE3A1302757558031FD /* TokenManageSinglePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokenManageSinglePresenter.swift; sourceTree = ""; }; 48C158C8D1855BCE53636934 /* AccountCreateProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountCreateProtocols.swift; sourceTree = ""; }; 48CECA2C5A0EFEBFDBB3C90C /* DAppOperationConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppOperationConfirmWireframe.swift; sourceTree = ""; }; + 48E5BB1EB494B5DB92FC3053 /* Pods-novawalletAll-novawalletIntegrationTests.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawalletIntegrationTests.dev.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests.dev.xcconfig"; sourceTree = ""; }; 4A191B92AD171FDDDD8C30E2 /* DAppSearchWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppSearchWireframe.swift; sourceTree = ""; }; 4B243F6751E2277D9FC14481 /* AdvancedWalletViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AdvancedWalletViewFactory.swift; sourceTree = ""; }; 4B6060CE86A7EA49AD05329C /* ParaStkUnstakeConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkUnstakeConfirmViewController.swift; sourceTree = ""; }; @@ -4007,11 +4428,14 @@ 4E2CF76ABE7BC9A99724D393 /* ParitySignerAddConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerAddConfirmViewFactory.swift; sourceTree = ""; }; 4E3749525FED4CA4CD0DCDF5 /* CreateWatchOnlyPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateWatchOnlyPresenter.swift; sourceTree = ""; }; 4E4462DCD832DB73AA78D44C /* GovernanceYourDelegationsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceYourDelegationsViewController.swift; sourceTree = ""; }; + 4E553E3D45A7A759C917A4B2 /* StakingSelectPoolViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSelectPoolViewFactory.swift; sourceTree = ""; }; 4F21FBC9BE578A300A77E9C5 /* ParaStkRebondViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkRebondViewLayout.swift; sourceTree = ""; }; 4F9F9E43E296386A7F138326 /* AssetsSearchProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetsSearchProtocols.swift; sourceTree = ""; }; 5002B8FA2695F470587677D2 /* AccountConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountConfirmProtocols.swift; sourceTree = ""; }; 502D42F4A480889BA226CAD3 /* StakingMainPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingMainPresenter.swift; sourceTree = ""; }; 50829CD47D3F60E3067418B4 /* ParaStkYieldBoostStartProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStartProtocols.swift; sourceTree = ""; }; + 50AFCEE78CDFC4FE3238E158 /* NPoolsClaimRewardsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsClaimRewardsViewController.swift; sourceTree = ""; }; + 50C441FA177F6838F6902786 /* Pods-novawalletTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletTests.release.xcconfig"; path = "Target Support Files/Pods-novawalletTests/Pods-novawalletTests.release.xcconfig"; sourceTree = ""; }; 513A449CCF5A417B67B7067D /* GovernanceUnlockConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnlockConfirmPresenter.swift; sourceTree = ""; }; 5147BFCC44EB3938D50EE8D9 /* DAppPhishingPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppPhishingPresenter.swift; sourceTree = ""; }; 5159EA2661A6CBE123CCF891 /* ReferendumVoteSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVoteSetupProtocols.swift; sourceTree = ""; }; @@ -4053,6 +4477,7 @@ 59BED0E6DBC4C21D9625740C /* GovernanceUnavailableTracksProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnavailableTracksProtocols.swift; sourceTree = ""; }; 5A05EB4FAF2FDE7DECEA93E4 /* StakingMainViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingMainViewController.swift; sourceTree = ""; }; 5AA3BF0C9C1E0E2C67D962F5 /* PurchaseViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PurchaseViewFactory.swift; sourceTree = ""; }; + 5ACCF5C31EF0E346D0763897 /* Pods-novawalletAll-novawalletIntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawalletIntegrationTests.debug.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests.debug.xcconfig"; sourceTree = ""; }; 5AD16D5FA4115F2A525BDE4F /* ParaStkRedeemViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkRedeemViewController.swift; sourceTree = ""; }; 5AFA57BC935878DEBBE32EEA /* GovernanceRevokeDelegationConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceRevokeDelegationConfirmInteractor.swift; sourceTree = ""; }; 5B675B6727754E3727CBC7BE /* DAppWalletAuthViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppWalletAuthViewController.swift; sourceTree = ""; }; @@ -4071,27 +4496,31 @@ 5DDDB8BE848C65B65FE4086D /* WalletConnectInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletConnectInteractor.swift; sourceTree = ""; }; 5E2EB9EE4A87BD4A74040784 /* ReferendumDetailsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumDetailsWireframe.swift; sourceTree = ""; }; 5E86B1DF9D9E8E5DD8DC49DE /* DelegateVotedReferendaViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegateVotedReferendaViewLayout.swift; sourceTree = ""; }; - 5EAF3AEE27F7901458B39A7A /* Pods-novawalletAll-novawalletIntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawalletIntegrationTests.debug.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests.debug.xcconfig"; sourceTree = ""; }; 5F4F3C080F3D5C1E64475903 /* ParitySignerAddressesProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerAddressesProtocols.swift; sourceTree = ""; }; 5F791FE1B479CE1DF936F79F /* CrowdloanContributionConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmViewFactory.swift; sourceTree = ""; }; 5FCECA20E6DCA5D228F44477 /* StakingUnbondSetupPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondSetupPresenter.swift; sourceTree = ""; }; 5FD612D8F897463726CDD033 /* LedgerPerformOperationViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerPerformOperationViewController.swift; sourceTree = ""; }; + 60EAEB7059DC148A36865DA8 /* Pods-novawalletTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletTests.debug.xcconfig"; path = "Target Support Files/Pods-novawalletTests/Pods-novawalletTests.debug.xcconfig"; sourceTree = ""; }; 60F6D1F16B35354844E435AC /* WalletConnectProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletConnectProtocols.swift; sourceTree = ""; }; 611AD2A7BEEEBA634F56163D /* ParaStkUnstakeViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkUnstakeViewLayout.swift; sourceTree = ""; }; 611FBB25D55CF56F36026074 /* AssetsSearchViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetsSearchViewController.swift; sourceTree = ""; }; 61EBE466BDCF77E65FDCDF81 /* ExportMnemonicPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportMnemonicPresenter.swift; sourceTree = ""; }; - 6216F6F1B91F798F07695FB6 /* StakingAmountWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingAmountWireframe.swift; sourceTree = ""; }; 627CE71CD8C7CE59B8AFBAD6 /* CommonDelegationTracksProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommonDelegationTracksProtocols.swift; sourceTree = ""; }; 62C5AF7E89A8C6CFF5AE03B1 /* YourWalletsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourWalletsProtocols.swift; sourceTree = ""; }; 62FA66143B25AA70B02CE461 /* ExportSeedViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportSeedViewFactory.swift; sourceTree = ""; }; 6325D31600DC2E1464ABE948 /* StakingDashboardProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingDashboardProtocols.swift; sourceTree = ""; }; 638A65DAC86BAF9EB4D2F2F8 /* StakingRewardDetailsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardDetailsWireframe.swift; sourceTree = ""; }; 6475F9C6C6B095B9C5026CE9 /* ParaStkYieldBoostStartViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStartViewController.swift; sourceTree = ""; }; + 64E66B662DD15424D2B383A7 /* StakingTypeViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingTypeViewLayout.swift; sourceTree = ""; }; + 6589230FD54838BFCF6E3FD8 /* StartStakingConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingConfirmViewFactory.swift; sourceTree = ""; }; 6594A70C41C42049D7FAC692 /* DAppSettingsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppSettingsViewLayout.swift; sourceTree = ""; }; 65AD15693E21C869DE1FDD17 /* UsernameSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UsernameSetupWireframe.swift; sourceTree = ""; }; 6606F6FCB40D5EDA2CDFA84F /* GovernanceRemoveVotesConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceRemoveVotesConfirmViewController.swift; sourceTree = ""; }; 661356CFE77B978610397907 /* OperationDetailsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OperationDetailsInteractor.swift; sourceTree = ""; }; + 663DD0E9F1F758C06E7EC829 /* StakingTypeInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingTypeInteractor.swift; sourceTree = ""; }; + 6654DEA68D7ED47AE8E52206 /* StakingSelectPoolWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSelectPoolWireframe.swift; sourceTree = ""; }; 667D74094E813EA3C53EE897 /* AssetDetailsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetDetailsPresenter.swift; sourceTree = ""; }; + 6708EF7D7F70868969EFADA9 /* StakingSetupAmountViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSetupAmountViewController.swift; sourceTree = ""; }; 674601F0C47EF05F7D35A592 /* TokenManageSingleProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokenManageSingleProtocols.swift; sourceTree = ""; }; 6746DDB8F277A968E6B25332 /* AssetsSearchWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetsSearchWireframe.swift; sourceTree = ""; }; 6747B9F68F9E92845122D8D2 /* LedgerInstructionsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerInstructionsViewLayout.swift; sourceTree = ""; }; @@ -4099,9 +4528,12 @@ 677CE34BFAB45122C57095F6 /* DAppBrowserViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppBrowserViewFactory.swift; sourceTree = ""; }; 67CAEB35921A61A8EC131AF8 /* LedgerDiscoverViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerDiscoverViewFactory.swift; sourceTree = ""; }; 68D44AF5C59681B54ECD7658 /* AddDelegationPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddDelegationPresenter.swift; sourceTree = ""; }; + 694E15BA7E0C59E46D0A6612 /* StartStakingInfoViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingInfoViewFactory.swift; sourceTree = ""; }; 6962C8E51EB317DE3AAE4BDF /* GovernanceSelectTracksViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceSelectTracksViewController.swift; sourceTree = ""; }; 6A3105383F2825940D0105D5 /* ReferendumVoteSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVoteSetupViewLayout.swift; sourceTree = ""; }; + 6A4009AE7EF95E9CE1EB88B8 /* NominationPoolBondMoreBaseWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreBaseWireframe.swift; sourceTree = ""; }; 6A695CA303926DFB5D54E309 /* LedgerAccountConfirmationViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerAccountConfirmationViewLayout.swift; sourceTree = ""; }; + 6A7302440137F083F7AEC64E /* NPoolsClaimRewardsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsClaimRewardsViewLayout.swift; sourceTree = ""; }; 6A825B6368073B06F32D7C8F /* StakingMainViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingMainViewFactory.swift; sourceTree = ""; }; 6AD8B98AB03AAF06AA891695 /* TransferConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferConfirmViewLayout.swift; sourceTree = ""; }; 6B60728FCFBC8A9BE4C7B50B /* YourValidatorListInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourValidatorListInteractor.swift; sourceTree = ""; }; @@ -4114,18 +4546,24 @@ 6E2509FEA85677165C4CCCFF /* YourWalletsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourWalletsViewLayout.swift; sourceTree = ""; }; 6F0AFEA616FDDF846F3F3650 /* NftListWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftListWireframe.swift; sourceTree = ""; }; 6F404EE82BC45BFE0F42E0A4 /* WalletsListWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsListWireframe.swift; sourceTree = ""; }; + 6F5788D702D2B2203949329E /* StartStakingInfoViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingInfoViewController.swift; sourceTree = ""; }; 6F65E56B70C5AF51A365C2BB /* InAppUpdatesViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = InAppUpdatesViewFactory.swift; sourceTree = ""; }; + 6F80E6C0C3AC88D7CDB8204F /* StakingSetupAmountPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSetupAmountPresenter.swift; sourceTree = ""; }; 7059B3F1E8DC94D36733B4C7 /* DelegationReferendumVotersViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegationReferendumVotersViewLayout.swift; sourceTree = ""; }; 7073BBC153295FF46FD06FB3 /* WalletsListViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsListViewLayout.swift; sourceTree = ""; }; 70A399F229B59A854FEA6D91 /* LedgerPerformOperationViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerPerformOperationViewLayout.swift; sourceTree = ""; }; + 71040ECB206855280257D4F2 /* StartStakingConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingConfirmPresenter.swift; sourceTree = ""; }; 71285CF636B32ACD8EB5519E /* ReferralCrowdloanViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferralCrowdloanViewFactory.swift; sourceTree = ""; }; 715F4A252715B543F11087AB /* DAppOperationConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppOperationConfirmInteractor.swift; sourceTree = ""; }; 7186BB5CB39B28A78D35D635 /* GovernanceYourDelegationsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceYourDelegationsWireframe.swift; sourceTree = ""; }; + 71D3243E13077BFD9DAC8FFC /* NominationPoolBondMoreConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreConfirmPresenter.swift; sourceTree = ""; }; 722DC609FE13ACBEE4328873 /* MarkdownDescriptionProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MarkdownDescriptionProtocols.swift; sourceTree = ""; }; + 725529DC6B70BF5B091B3748 /* NominationPoolBondMoreSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreSetupProtocols.swift; sourceTree = ""; }; 72EF3BEC3EA07548C10A87FD /* ReferendumSearchWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumSearchWireframe.swift; sourceTree = ""; }; 72FEB9C65F32B7A4FD27C9EB /* ParitySignerTxScanInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxScanInteractor.swift; sourceTree = ""; }; 7306D50F278F6CC90DC88F27 /* AccountConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountConfirmPresenter.swift; sourceTree = ""; }; 73D569738955713647612599 /* OperationDetailsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OperationDetailsViewLayout.swift; sourceTree = ""; }; + 73D6B3F74819B5D45324F714 /* NominationPoolBondMoreConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreConfirmInteractor.swift; sourceTree = ""; }; 73DA8D51B588486D304F1B73 /* DAppAddFavoriteViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAddFavoriteViewFactory.swift; sourceTree = ""; }; 73DE88ED69EF6E4F4F6612D8 /* AssetReceiveViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetReceiveViewLayout.swift; sourceTree = ""; }; 7484BA696561262926D87FE5 /* CrowdloanContributionSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupProtocols.swift; sourceTree = ""; }; @@ -4136,17 +4574,36 @@ 75ADB95DAB1F29E6A3FDD166 /* WalletConnectSessionDetailsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletConnectSessionDetailsPresenter.swift; sourceTree = ""; }; 75CFAA1D2D04553B10421C69 /* DAppAuthConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAuthConfirmProtocols.swift; sourceTree = ""; }; 76AA6A6232B1CF2D5AF74D0D /* ParaStkUnstakeInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkUnstakeInteractor.swift; sourceTree = ""; }; + 770F57872A8A2CE0005FD7C1 /* StakingSelectPoolViewStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingSelectPoolViewStyles.swift; sourceTree = ""; }; + 770F578A2A8A48FF005FD7C1 /* ButtonViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonViewModel.swift; sourceTree = ""; }; + 77171CA72A98BBA10032B387 /* NominationPoolErrorPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolErrorPresentable.swift; sourceTree = ""; }; + 77171CA92A98BC420032B387 /* NominationPoolDataValidatorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolDataValidatorFactory.swift; sourceTree = ""; }; 77204EA32A1CD59100BBDE4A /* GenericBorderedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericBorderedView.swift; sourceTree = ""; }; 77204EA52A1E0EAA00BBDE4A /* WalletConnectionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectionsView.swift; sourceTree = ""; }; 7725062B2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReferendumSearchViewLayout.swift; sourceTree = ""; }; + 7726CD542A9728D700CE9064 /* StakingTypeSelectedStakingViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingTypeSelectedStakingViewModelFactory.swift; sourceTree = ""; }; 7728E58A2A123AEE007901E0 /* ReferendumsSearchOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferendumsSearchOperationFactory.swift; sourceTree = ""; }; 7728E58C2A123B42007901E0 /* SearchOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchOperationFactory.swift; sourceTree = ""; }; 7728E58E2A123B70007901E0 /* ReferendumsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferendumsState.swift; sourceTree = ""; }; 7728E5902A1324A2007901E0 /* ReferendumsSearchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferendumsSearchManager.swift; sourceTree = ""; }; 7728E5922A13290D007901E0 /* GenericLens.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericLens.swift; sourceTree = ""; }; + 772B1C7C2A93837800A19061 /* StartStakingCustomValidatorListWireframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartStakingCustomValidatorListWireframe.swift; sourceTree = ""; }; 7731E9C32A14DA3F0085B5FF /* BorderedActionControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedActionControlView.swift; sourceTree = ""; }; + 7738FB692A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingRelaychainInteractor.swift; sourceTree = ""; }; 774A481029F8BFB70094635B /* OperationAuthPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationAuthPresentable.swift; sourceTree = ""; }; 7756927C2A20B88200220756 /* TokenOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenOperation.swift; sourceTree = ""; }; + 775F194C2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoRelaychainPresenter.swift; sourceTree = ""; }; + 775F19502A5811FA009915B6 /* StartStakingParachainInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingParachainInteractor.swift; sourceTree = ""; }; + 775F19522A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoParachainPresenter.swift; sourceTree = ""; }; + 77799ADC2A74219A00B7E564 /* ButtonState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonState.swift; sourceTree = ""; }; + 77799ADE2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingInfoRelaychainWireframe.swift; sourceTree = ""; }; + 77799AE42A792AE700B7E564 /* StakingTypeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingTypeViewModel.swift; sourceTree = ""; }; + 77799AE62A792B6F00B7E564 /* StakingTypeAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingTypeAccountViewModel.swift; sourceTree = ""; }; + 77799AE82A7C99D200B7E564 /* StakingTypeViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingTypeViewModelFactory.swift; sourceTree = ""; }; + 77799AEB2A7CFB5700B7E564 /* PoolStakingTypeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoolStakingTypeViewModel.swift; sourceTree = ""; }; + 77799AED2A7CFB6A00B7E564 /* DirectStakingTypeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectStakingTypeViewModel.swift; sourceTree = ""; }; + 77799AEF2A7CFB7C00B7E564 /* ValidatorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidatorViewModel.swift; sourceTree = ""; }; + 77799AF12A7CFB8D00B7E564 /* PoolAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoolAccountViewModel.swift; sourceTree = ""; }; 777BD85F29F9730F004969A2 /* ReferendumsFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferendumsFilterViewModel.swift; sourceTree = ""; }; 777BD86129F97322004969A2 /* ReferendumsFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferendumsFilter.swift; sourceTree = ""; }; 777BD86329F979DA004969A2 /* SelectableFilterCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableFilterCell.swift; sourceTree = ""; }; @@ -4154,6 +4611,10 @@ 777BD86729FA3376004969A2 /* ReferendumsSettingsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferendumsSettingsCell.swift; sourceTree = ""; }; 777BD86929FA3D5E004969A2 /* ReferendumsFilter+match.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReferendumsFilter+match.swift"; sourceTree = ""; }; 778210852A6588D100256E78 /* DiffableDataStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiffableDataStore.swift; sourceTree = ""; }; + 77864F4B2A6AC5A100FA7BA7 /* TitleHorizontalMultiValueView+Bind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TitleHorizontalMultiValueView+Bind.swift"; sourceTree = ""; }; + 77895C9E2A8F5D40006870FB /* NominationPoolSearchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolSearchManager.swift; sourceTree = ""; }; + 77895CA02A8F7360006870FB /* NominationPoolSearchOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolSearchOperationFactory.swift; sourceTree = ""; }; + 77895CA22A8F8CFD006870FB /* NominationPoolsFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsFilters.swift; sourceTree = ""; }; 778D979A2A24D1D8002BA681 /* BaseAssetsSearchViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseAssetsSearchViewLayout.swift; sourceTree = ""; }; 778D979C2A24D21B002BA681 /* AssetsOperationViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsOperationViewLayout.swift; sourceTree = ""; }; 778D979E2A24D248002BA681 /* SearchViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewProtocol.swift; sourceTree = ""; }; @@ -4167,6 +4628,7 @@ 7796C7082A17D24A00D56094 /* UILayoutPriority+Custom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILayoutPriority+Custom.swift"; sourceTree = ""; }; 779A8F982A04BAC000BE31B3 /* StakingRewardActionControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardActionControl.swift; sourceTree = ""; }; 779A8F9A2A050C4400BE31B3 /* StakingRewardDateCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StakingRewardDateCell.swift; sourceTree = ""; }; + 779C8BE72AA1DD1B001A4A3C /* NominationPoolsBondMoreHintsFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NominationPoolsBondMoreHintsFactory.swift; sourceTree = ""; }; 77A0B2EC2A3B7F3300CBF653 /* DAppCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAppCollectionViewCell.swift; sourceTree = ""; }; 77A0B2EE2A3B85B700CBF653 /* BlurBackgroundCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurBackgroundCollectionReusableView.swift; sourceTree = ""; }; 77A0B2F22A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionsViewModelFactory.swift; sourceTree = ""; }; @@ -4185,9 +4647,15 @@ 77A6F5CE2A31C4D4004AFD1A /* Web3TransferRecipientRepositoryFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Web3TransferRecipientRepositoryFactory.swift; sourceTree = ""; }; 77A6F5D12A31DB8C004AFD1A /* JsonCanonicalizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonCanonicalizer.swift; sourceTree = ""; }; 77A6F5D42A31E046004AFD1A /* JsonCanonicalizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonCanonicalizerTests.swift; sourceTree = ""; }; + 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashModel.swift; sourceTree = ""; }; + 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationPoolRewardOrSlashViewModel.swift; sourceTree = ""; }; + 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDetailsPoolRewardView.swift; sourceTree = ""; }; 77CB33CD2A38780700B6709A /* structures_output.json */ = {isa = PBXFileReference; explicitFileType = text.json; fileEncoding = 4; path = structures_output.json; sourceTree = ""; usesTabs = 0; wrapsLines = 0; }; 77CB33D12A38893900B6709A /* Web3NameIntegrityVerifierWithCanonicalizationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Web3NameIntegrityVerifierWithCanonicalizationData.swift; sourceTree = ""; }; 77CB33D62A3998FC00B6709A /* Array+Sort.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Sort.swift"; sourceTree = ""; }; + 77CC82A22A984BC3002D022F /* StartStakingSelectedValidatorsListWireframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartStakingSelectedValidatorsListWireframe.swift; sourceTree = ""; }; + 77CC82A42A984EDA002D022F /* UINavigaionController+Pop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigaionController+Pop.swift"; sourceTree = ""; }; + 77CC82A62A986CF1002D022F /* StakingSelectValidatorsDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingSelectValidatorsDelegate.swift; sourceTree = ""; }; 77E0DC9D2A6940C400D03724 /* Calendar+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Calendar+Helpers.swift"; sourceTree = ""; }; 77E255652A16059A00B644C3 /* MultiassetUserDataModel9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel9.xcdatamodel; sourceTree = ""; }; 77E255662A16145500B644C3 /* StakingRewardsFilterMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardsFilterMapper.swift; sourceTree = ""; }; @@ -4203,19 +4671,43 @@ 77ED167B2A0CF42E00E1FC8C /* StakingRewardFiltersPeriod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRewardFiltersPeriod.swift; sourceTree = ""; }; 77ED167D2A0D0AE900E1FC8C /* Lenses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lenses.swift; sourceTree = ""; }; 77ED167F2A0D6E9A00E1FC8C /* TableViewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewModels.swift; sourceTree = ""; }; + 77EFFC882A6E7A24009E28F8 /* AccountExistense.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AccountExistense.swift; path = novawallet/Modules/Staking/StartStakingInfo/Model/AccountExistense.swift; sourceTree = SOURCE_ROOT; }; + 77EFFC892A6E7A24009E28F8 /* DefaultStakingRewardDestination.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DefaultStakingRewardDestination.swift; path = novawallet/Modules/Staking/StartStakingInfo/Model/DefaultStakingRewardDestination.swift; sourceTree = SOURCE_ROOT; }; + 77EFFC8C2A6EECFD009E28F8 /* StakingAmountViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingAmountViewModelFactory.swift; sourceTree = ""; }; + 77EFFC8E2A714C21009E28F8 /* StakingTypeBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingTypeBannerView.swift; sourceTree = ""; }; + 77EFFC902A7276F1009E28F8 /* StakingTypeAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingTypeAccountView.swift; sourceTree = ""; }; + 77EFFC922A72A288009E28F8 /* StakingTypeBaseBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingTypeBaseBannerView.swift; sourceTree = ""; }; + 77F033922A814296006BC67E /* GenericStakingTypeAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericStakingTypeAccountView.swift; sourceTree = ""; }; + 77F033942A8142B0006BC67E /* StakingTypeValidatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingTypeValidatorView.swift; sourceTree = ""; }; + 77F033962A8142D1006BC67E /* StakingSetupAmountStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingSetupAmountStyles.swift; sourceTree = ""; }; + 77F033982A8142E5006BC67E /* DirectStakingTypeAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectStakingTypeAccountViewModel.swift; sourceTree = ""; }; + 77F0339A2A814505006BC67E /* StakingSelectionMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StakingSelectionMethod.swift; path = ../../StakingType/Model/StakingSelectionMethod.swift; sourceTree = ""; }; + 77F0339C2A837AB3006BC67E /* StakingTypeSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingTypeSelection.swift; sourceTree = ""; }; + 77F033A12A84E00F006BC67E /* StakingPoolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingPoolView.swift; sourceTree = ""; }; + 77F033A32A84E028006BC67E /* StakingSelectPoolListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingSelectPoolListHeaderView.swift; sourceTree = ""; }; + 77F033A52A84EAC3006BC67E /* StakingPoolTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingPoolTableViewCell.swift; sourceTree = ""; }; + 77F033A72A86136D006BC67E /* StakingSelectPoolViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingSelectPoolViewModelFactory.swift; sourceTree = ""; }; + 77F1893D2A4996FC00E8B933 /* ParagraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParagraphView.swift; sourceTree = ""; }; + 77F1893F2A49972300E8B933 /* UILabel+bind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+bind.swift"; sourceTree = ""; }; + 77F189432A49974A00E8B933 /* UITextView+bind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITextView+bind.swift"; sourceTree = ""; }; + 77F189462A49BD6700E8B933 /* StartStakingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingViewModel.swift; sourceTree = ""; }; + 77F189482A4A299800E8B933 /* StartStakingViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartStakingViewModelFactory.swift; sourceTree = ""; }; 77F3F175AFC554763696CB2A /* StakingMoreOptionsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionsPresenter.swift; sourceTree = ""; }; + 77F9FB062A9D96E900820625 /* NominationPoolBondMoreSetupPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreSetupPresenter.swift; sourceTree = ""; }; + 77F9FB082A9D971000820625 /* NominationPoolBondMoreSetupInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreSetupInteractor.swift; sourceTree = ""; }; + 77F9FB0A2A9D97A100820625 /* NominationPoolBondMoreSetupWireframe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreSetupWireframe.swift; sourceTree = ""; }; + 77F9FB0C2A9D9C5600820625 /* NominationPoolBondMoreBaseProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreBaseProtocols.swift; sourceTree = ""; }; 781FA4C896AF31B4035AFB38 /* ChainAddressDetailsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainAddressDetailsViewFactory.swift; sourceTree = ""; }; 782CC21A2F9EEF5DBA3AB1AA /* PurchaseProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PurchaseProtocols.swift; sourceTree = ""; }; 78536852751EF56F58C5691E /* ReferendumDetailsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumDetailsPresenter.swift; sourceTree = ""; }; 7859654B7C1FAC269CA61E71 /* ParitySignerTxQrInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxQrInteractor.swift; sourceTree = ""; }; 78670B0926E92B75088D2D7B /* WalletHistoryFilterWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilterWireframe.swift; sourceTree = ""; }; 78A5A3C9077FCE262224B832 /* ParaStkYieldBoostSetupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostSetupInteractor.swift; sourceTree = ""; }; - 78B08033E84541B42A6EAFEE /* Pods-novawalletTests.staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletTests.staging.xcconfig"; path = "Target Support Files/Pods-novawalletTests/Pods-novawalletTests.staging.xcconfig"; sourceTree = ""; }; 78CF6E3E64F7608EF6C28399 /* ParitySignerWelcomeViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerWelcomeViewController.swift; sourceTree = ""; }; 78F5CAA29A0D442151679EA8 /* StakingRewardFiltersViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardFiltersViewFactory.swift; sourceTree = ""; }; 7911693957DFAF141EBDAFEC /* StakingRewardPayoutsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardPayoutsProtocols.swift; sourceTree = ""; }; 793046EA14E4CAB096803BCD /* NftDetailsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsWireframe.swift; sourceTree = ""; }; - 79BC90C31B17F8935FE8456F /* Pods-novawalletAll-novawallet.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawallet.dev.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawallet/Pods-novawalletAll-novawallet.dev.xcconfig"; sourceTree = ""; }; + 793356FB65FB73CE7097C6F1 /* NPoolsUnstakeConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeConfirmProtocols.swift; sourceTree = ""; }; 7A092ADC09DA0429548EBC08 /* NftListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftListPresenter.swift; sourceTree = ""; }; 7ACF32611D345B87BCE29FE0 /* DAppAddFavoriteWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAddFavoriteWireframe.swift; sourceTree = ""; }; 7B13D65B93E65B5112272962 /* DelegationReferendumVotersWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegationReferendumVotersWireframe.swift; sourceTree = ""; }; @@ -4237,6 +4729,7 @@ 7F8B46E3BAB48A2D2E1D2EF4 /* ParitySignerTxScanWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxScanWireframe.swift; sourceTree = ""; }; 7FF467EBB295AC2A3DDA4492 /* GovernanceEditDelegationTracksPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceEditDelegationTracksPresenter.swift; sourceTree = ""; }; 812BCD9B7B25BCA02E32452E /* DAppAuthSettingsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAuthSettingsInteractor.swift; sourceTree = ""; }; + 8145FD81205F06EB6B8B51DA /* StakingSetupAmountInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSetupAmountInteractor.swift; sourceTree = ""; }; 8167536168325942DA6892E1 /* LedgerWalletConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerWalletConfirmWireframe.swift; sourceTree = ""; }; 817E84903F4A2CD5333518DE /* ParitySignerTxScanViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxScanViewController.swift; sourceTree = ""; }; 818D1B632559444D08303469 /* ParaStkSelectCollatorsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkSelectCollatorsViewController.swift; sourceTree = ""; }; @@ -4244,6 +4737,7 @@ 81F4883B898928C77D17C824 /* AssetListViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetListViewLayout.swift; sourceTree = ""; }; 8202B83B2DF36439CB6449C6 /* TransferSetupViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferSetupViewFactory.swift; sourceTree = ""; }; 82445792D5EF0120B5233767 /* WalletConnectSessionsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletConnectSessionsPresenter.swift; sourceTree = ""; }; + 834314A1286CB91F8BB43F06 /* NominationPoolSearchViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolSearchViewController.swift; sourceTree = ""; }; 837E9D1F8096A0E9CA0E0CEB /* MoonbeamTermsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MoonbeamTermsPresenter.swift; sourceTree = ""; }; 83AB0AD3A7CECD061611F60C /* AssetSelectionInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetSelectionInteractor.swift; sourceTree = ""; }; 83D189355A12F42CBAAFF238 /* ReferendumsFiltersViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumsFiltersViewLayout.swift; sourceTree = ""; }; @@ -4276,6 +4770,7 @@ 84031C16263EC95C008FD9D4 /* SetPayeeCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetPayeeCall.swift; sourceTree = ""; }; 84033054290FD745009C18E6 /* ReferendumsUnlocksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferendumsUnlocksViewModel.swift; sourceTree = ""; }; 84033056290FD8AB009C18E6 /* ReferendumsPersonalActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferendumsPersonalActivityView.swift; sourceTree = ""; }; + 84033496FC259BCA5420D52B /* NominationPoolBondMoreBaseInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreBaseInteractor.swift; sourceTree = ""; }; 84038FEB26FFBA4D00C73F3F /* PriceLocalStorageSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceLocalStorageSubscriber.swift; sourceTree = ""; }; 84038FED26FFBA6200C73F3F /* PriceLocalSubscriptionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceLocalSubscriptionHandler.swift; sourceTree = ""; }; 84038FEF26FFBE0600C73F3F /* JsonLocalStorageSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonLocalStorageSubscriber.swift; sourceTree = ""; }; @@ -4436,7 +4931,6 @@ 841E5562282E9EF400C8438F /* ParachainStakingCommonData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParachainStakingCommonData.swift; sourceTree = ""; }; 841E5564282EA76C00C8438F /* ParachainStakingBaseState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParachainStakingBaseState.swift; sourceTree = ""; }; 841E5566282EAC1000C8438F /* ParachainStakingInitState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParachainStakingInitState.swift; sourceTree = ""; }; - 841E5568282EAC2600C8438F /* ParachainStakingNoStakingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParachainStakingNoStakingState.swift; sourceTree = ""; }; 841E556A282EAC3600C8438F /* ParachainStakingDelegatorState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParachainStakingDelegatorState.swift; sourceTree = ""; }; 841E556C282EC50700C8438F /* ParaStkStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParaStkStateMachine.swift; sourceTree = ""; }; 841E6AF525EC12100007DDFE /* PreparedNomination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreparedNomination.swift; sourceTree = ""; }; @@ -4544,14 +5038,12 @@ 842898D0265A955A002D5D65 /* ImageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewModel.swift; sourceTree = ""; }; 842A735D27DB2EC4006EE1EA /* OperationDetailsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationDetailsModel.swift; sourceTree = ""; }; 842A736127DB3032006EE1EA /* OperationExtrinsicModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationExtrinsicModel.swift; sourceTree = ""; }; - 842A736327DB31A3006EE1EA /* OperationRewardModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationRewardModel.swift; sourceTree = ""; }; - 842A736527DB485E006EE1EA /* OperationSlashModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationSlashModel.swift; sourceTree = ""; }; + 842A736327DB31A3006EE1EA /* OperationRewardOrSlashModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationRewardOrSlashModel.swift; sourceTree = ""; }; 842A736727DB4883006EE1EA /* OperationTransferModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationTransferModel.swift; sourceTree = ""; }; 842A736A27DB7A2E006EE1EA /* OperationDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationDetailsViewModel.swift; sourceTree = ""; }; 842A736C27DB7B5E006EE1EA /* OperationTransferViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationTransferViewModel.swift; sourceTree = ""; }; 842A736E27DB7E57006EE1EA /* OperationExtrinsicViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationExtrinsicViewModel.swift; sourceTree = ""; }; - 842A737027DB7EF1006EE1EA /* OperationSlashViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationSlashViewModel.swift; sourceTree = ""; }; - 842A737227DB7F75006EE1EA /* OperationRewardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationRewardViewModel.swift; sourceTree = ""; }; + 842A737227DB7F75006EE1EA /* OperationRewardOrSlashViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationRewardOrSlashViewModel.swift; sourceTree = ""; }; 842A737427DB8338006EE1EA /* OperationDetailsViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationDetailsViewModelFactory.swift; sourceTree = ""; }; 842A737627DC7AEB006EE1EA /* NetworkViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkViewModelFactory.swift; sourceTree = ""; }; 842A737827DC7CEF006EE1EA /* DisplayAddressViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayAddressViewModelFactory.swift; sourceTree = ""; }; @@ -4623,7 +5115,6 @@ 843074F828BF6201009D463B /* NoAccountSupportPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoAccountSupportPresentable.swift; sourceTree = ""; }; 8430AACB2602249B005B1066 /* InitialStakingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialStakingState.swift; sourceTree = ""; }; 8430AAD32602285B005B1066 /* StakingStateCommonData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingStateCommonData.swift; sourceTree = ""; }; - 8430AADB26022C58005B1066 /* NoStashState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoStashState.swift; sourceTree = ""; }; 8430AAE026022CA1005B1066 /* BaseStakingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStakingState.swift; sourceTree = ""; }; 8430AAE826022F69005B1066 /* StashState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StashState.swift; sourceTree = ""; }; 8430AAF02602306A005B1066 /* BondedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BondedState.swift; sourceTree = ""; }; @@ -4839,7 +5330,6 @@ 8448F7A12882ABF50080CEA9 /* CustomSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSearchView.swift; sourceTree = ""; }; 8448F7A32882E21E0080CEA9 /* SearchMatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchMatch.swift; sourceTree = ""; }; 8448F7A5288314250080CEA9 /* AssetListAssetsViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListAssetsViewModelFactory.swift; sourceTree = ""; }; - 8449660925E15ECA00F2E9F5 /* RewardDestinationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RewardDestinationViewModel.swift; sourceTree = ""; }; 844A539429BF54BA00C77111 /* XcmPalletMetadataQueryFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmPalletMetadataQueryFactory.swift; sourceTree = ""; }; 844A539629BF606D00C77111 /* BlockchainWeightFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockchainWeightFactory.swift; sourceTree = ""; }; 844ADE7428CA31BA00EE29F7 /* ParaStkYieldBoostPeriodViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostPeriodViewModel.swift; sourceTree = ""; }; @@ -5057,8 +5547,6 @@ 8463781E2979DACB0034162B /* ReferendumsActivityViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferendumsActivityViewModelFactory.swift; sourceTree = ""; }; 8463A6F825E2F82E003B8160 /* CDSingleValue+CoreDataCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CDSingleValue+CoreDataCodable.swift"; sourceTree = ""; }; 8463A70225E2FCD0003B8160 /* WeakWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakWrapper.swift; sourceTree = ""; }; - 8463A71125E30C95003B8160 /* BalanceViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceViewModelFactory.swift; sourceTree = ""; }; - 8463A71925E3116A003B8160 /* BalanceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceViewModel.swift; sourceTree = ""; }; 8463A71E25E39E07003B8160 /* StorageProviderSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageProviderSource.swift; sourceTree = ""; }; 8463A72425E3A82A003B8160 /* DataProviderProxyTrigger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProviderProxyTrigger.swift; sourceTree = ""; }; 8463A72C25E3A8E1003B8160 /* ChainStorageDecodedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainStorageDecodedItem.swift; sourceTree = ""; }; @@ -5267,10 +5755,8 @@ 8477DAA22888329800129B45 /* watchOnlyPreset.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = watchOnlyPreset.json; sourceTree = ""; }; 8477DAA5288832CB00129B45 /* WatchOnlyPresetRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchOnlyPresetRepository.swift; sourceTree = ""; }; 84786DA725F9F58E0089DFF7 /* EraValidatorService+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EraValidatorService+Fetch.swift"; sourceTree = ""; }; - 84786E0F25FA20D30089DFF7 /* StakingAccountResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingAccountResolver.swift; sourceTree = ""; }; 84786E1425FA57B90089DFF7 /* StakingLedger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingLedger.swift; sourceTree = ""; }; 84786E1925FA6A470089DFF7 /* StashItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StashItem.swift; sourceTree = ""; }; - 84786E1E25FA6C390089DFF7 /* CDStashItem+CoreDataCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CDStashItem+CoreDataCodable.swift"; sourceTree = ""; }; 84786E2325FBA2A50089DFF7 /* StakingAccountSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingAccountSubscription.swift; sourceTree = ""; }; 8479607D283B60AA0084E779 /* ActionLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionLoadingView.swift; sourceTree = ""; }; 8479607F283B63240084E779 /* LoadableActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableActionView.swift; sourceTree = ""; }; @@ -5294,7 +5780,6 @@ 847A25C928D85204006AC9F5 /* ReferendumInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferendumInfo.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 = ""; }; - 847ABE322853333A00851218 /* StakingSharedState+Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StakingSharedState+Duration.swift"; sourceTree = ""; }; 847C15BE2A0CFE0D003F3FF8 /* WalletConnectErrorPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectErrorPresentable.swift; sourceTree = ""; }; 847C961F255340F2002D288F /* ExportGenericViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportGenericViewController.swift; sourceTree = ""; }; 847C962725534134002D288F /* ExportGenericProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportGenericProtocols.swift; sourceTree = ""; }; @@ -5504,7 +5989,7 @@ 8490145724A9406D008F705E /* LegalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalData.swift; sourceTree = ""; }; 8490145B24A94328008F705E /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 8490145F24A94373008F705E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - 8490146224A9438E008F705E /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + 8490146224A9438E008F705E /* ru */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 8490146924A9463B008F705E /* Locale+Localization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Locale+Localization.swift"; sourceTree = ""; }; 8490146C24A9487A008F705E /* ErrorPresentable+AlertText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ErrorPresentable+AlertText.swift"; sourceTree = ""; }; 8490146F24A94A37008F705E /* RootProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootProtocol.swift; sourceTree = ""; }; @@ -5552,7 +6037,6 @@ 8493D0DE26FE7D7400A28008 /* ChainRepositoryFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainRepositoryFactory.swift; sourceTree = ""; }; 8493D0E026FF4F5000A28008 /* ScaleCoder+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScaleCoder+Extension.swift"; sourceTree = ""; }; 8493D0E226FF571D00A28008 /* PriceProviderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceProviderFactory.swift; sourceTree = ""; }; - 8493D3E52705994200157009 /* StakingSharedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingSharedState.swift; sourceTree = ""; }; 8493D3E827059B6700157009 /* StakingServiceFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingServiceFactory.swift; sourceTree = ""; }; 8493FF36291A3D4C00F09F1B /* SubstrateDataModel5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel5.xcdatamodel; sourceTree = ""; }; 8493FF37291A59D800F09F1B /* ReferendumMetadataMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferendumMetadataMapper.swift; sourceTree = ""; }; @@ -5982,8 +6466,6 @@ 84C2063D28D1E8CE006D0D52 /* AssetBalanceChanged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetBalanceChanged.swift; sourceTree = ""; }; 84C2063F28D1EAD2006D0D52 /* AccountAssetBalanceTrigger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountAssetBalanceTrigger.swift; sourceTree = ""; }; 84C2802026F541DE006E8014 /* WebSocketEngine+Connection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebSocketEngine+Connection.swift"; sourceTree = ""; }; - 84C2F27625E296CD0050A4AD /* RewardDestinationViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RewardDestinationViewModelFactory.swift; sourceTree = ""; }; - 84C2F27C25E297350050A4AD /* CalculatedReward.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalculatedReward.swift; sourceTree = ""; }; 84C3316E294C695600F8CE4C /* SubstrateDataModel9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel9.xcdatamodel; sourceTree = ""; }; 84C34203283124C600156569 /* StakingParachainWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingParachainWireframe.swift; sourceTree = ""; }; 84C3420628314D9600156569 /* ParaStkScheduledRequestsQueryFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParaStkScheduledRequestsQueryFactory.swift; sourceTree = ""; }; @@ -6099,7 +6581,6 @@ 84D1110D26B931C20016D962 /* ChainModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainModel.swift; sourceTree = ""; }; 84D1111026B932480016D962 /* AssetModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetModel.swift; sourceTree = ""; }; 84D1111226B932C40016D962 /* ChainNodeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainNodeModel.swift; sourceTree = ""; }; - 84D17ECB2803F7EF00F7BAFF /* StakingAmountLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingAmountLayout.swift; sourceTree = ""; }; 84D17ECD2804290700F7BAFF /* RadioSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioSelectorView.swift; sourceTree = ""; }; 84D17ED42805307000F7BAFF /* MultiassetUserDataModel3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MultiassetUserDataModel3.xcdatamodel; sourceTree = ""; }; 84D17ED528053D6D00F7BAFF /* DAppFavorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAppFavorite.swift; sourceTree = ""; }; @@ -6287,7 +6768,6 @@ 84E8BA292A00EF4C00FD9F40 /* XcmBaseMetadataQueryFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcmBaseMetadataQueryFactory.swift; sourceTree = ""; }; 84E90BA028D0B51000529633 /* CheckboxControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxControlView.swift; sourceTree = ""; }; 84E9A04F28F000AB00551DC4 /* ReferendumMetadataLocal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferendumMetadataLocal.swift; sourceTree = ""; }; - 84EA0B2925E579DF00AFB0DC /* AssetBalanceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetBalanceViewModel.swift; sourceTree = ""; }; 84EB6C4D281999E100CFD8B2 /* PayoutTimeViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayoutTimeViewModelFactory.swift; sourceTree = ""; }; 84EBA4EF27AD26A5000AEEAD /* AssetBalanceId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetBalanceId.swift; sourceTree = ""; }; 84EBAB05265DC24C0015E446 /* CrowdloanContributionViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionViewModelFactory.swift; sourceTree = ""; }; @@ -6494,9 +6974,10 @@ 85D8B7BE70A9F907F8B43BFC /* TransferSetupViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferSetupViewController.swift; sourceTree = ""; }; 85F45A5C6145F863760F4409 /* AccountImportWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountImportWireframe.swift; sourceTree = ""; }; 8602E65CC4E81A7BE1727CE3 /* VotesViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = VotesViewLayout.swift; sourceTree = ""; }; + 862545D6BD501914AE04D776 /* NPoolsRedeemViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsRedeemViewController.swift; sourceTree = ""; }; 86DCB6F3977BDE1BDC7BC3F9 /* ParaStkUnstakeConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkUnstakeConfirmPresenter.swift; sourceTree = ""; }; 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; }; + 86F7ACFB151C31B3A5044DCD /* StakingSelectPoolInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSelectPoolInteractor.swift; sourceTree = ""; }; 879470FAF62A152E52DF4845 /* StakingDashboardInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingDashboardInteractor.swift; sourceTree = ""; }; 87CD8C618D61C78EA8C58532 /* ParitySignerTxScanViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxScanViewLayout.swift; sourceTree = ""; }; 880059D728EEBC0200E87B9B /* SliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderView.swift; sourceTree = ""; }; @@ -6516,10 +6997,7 @@ 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 = ""; }; 880CC0A729E7F13B008C7F65 /* EquilibriumAccountBalancesSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EquilibriumAccountBalancesSubscription.swift; sourceTree = ""; }; 880CC0A929E7F151008C7F65 /* EquilibriumLocksSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EquilibriumLocksSubscription.swift; sourceTree = ""; }; 880CC0AB29E7F168008C7F65 /* EquilibriumReservedSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EquilibriumReservedSubscription.swift; sourceTree = ""; }; @@ -6704,6 +7182,7 @@ 88AC5ADB2948A9090056DD40 /* TransactionHistoryHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionHistoryHeaderView.swift; sourceTree = ""; }; 88AC5ADD2948CB540056DD40 /* TransactionHistoryViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionHistoryViewModelFactory.swift; sourceTree = ""; }; 88AF35DD28C21D28003730DA /* LocksSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocksSubscription.swift; sourceTree = ""; }; + 88B13FCAD745A663807219B1 /* NPoolsRedeemViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsRedeemViewFactory.swift; sourceTree = ""; }; 88B1862928EF30A600D49854 /* YourVoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YourVoteView.swift; sourceTree = ""; }; 88B1C33E29B9C86D00DCA101 /* BagListCalls.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BagListCalls.swift; sourceTree = ""; }; 88B1C34129BA046000DCA101 /* StakingRebagConfirmError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingRebagConfirmError.swift; sourceTree = ""; }; @@ -6728,7 +7207,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 = ""; }; + 88CD320F28E2137200542F0D /* ExternalBalanceContribution.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExternalBalanceContribution.swift; sourceTree = ""; }; 88D02FE22942EA2200E26390 /* PayButtonsRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayButtonsRow.swift; sourceTree = ""; }; 88D02FE42942EA7800E26390 /* AssetDetailsStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetDetailsStyles.swift; sourceTree = ""; }; 88D02FE72942EB1A00E26390 /* AssetDetailsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetDetailsModel.swift; sourceTree = ""; }; @@ -6784,12 +7263,12 @@ 88FB7DCE295071B100784E08 /* ContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerViewController.swift; sourceTree = ""; }; 88FB7DD02950720800784E08 /* ContainerProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainerProtocols.swift; sourceTree = ""; }; 88FB7DD22950723100784E08 /* NovaWalletViewModelObserverContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NovaWalletViewModelObserverContainer.swift; sourceTree = ""; }; - 88FB7DD62951B1AF00784E08 /* WalletHistoryFilter+CallCodingPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletHistoryFilter+CallCodingPath.swift"; sourceTree = ""; }; 88FF5C7B29C8360400D1CB5D /* Caip19+ParseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Caip19+ParseError.swift"; sourceTree = ""; }; 88FF5C7E29C8364500D1CB5D /* Caip2+ParseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Caip2+ParseError.swift"; sourceTree = ""; }; + 894A6BB613EA991A09976B30 /* NominationPoolBondMoreConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreConfirmProtocols.swift; sourceTree = ""; }; 899686C7351A2600FFA08371 /* TransferConfirmOnChainViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferConfirmOnChainViewFactory.swift; sourceTree = ""; }; + 89CA10C284A0C9853E043A9D /* Pods_novawalletAll_novawalletIntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_novawalletAll_novawalletIntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 89CFED2E01AB638656E251AF /* NftListProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftListProtocols.swift; sourceTree = ""; }; - 8ADA5C374888879D27DBAA29 /* Pods-novawalletTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletTests.release.xcconfig"; path = "Target Support Files/Pods-novawalletTests/Pods-novawalletTests.release.xcconfig"; sourceTree = ""; }; 8B0BF8DFAA80B405D4A5D891 /* GovernanceUnavailableTracksViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnavailableTracksViewFactory.swift; sourceTree = ""; }; 8B4C1B5D56DB69BA0AECF731 /* ExportSeedWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportSeedWireframe.swift; sourceTree = ""; }; 8B56BDC7E6221DE292498D3A /* ParitySignerTxQrViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxQrViewFactory.swift; sourceTree = ""; }; @@ -6799,6 +7278,7 @@ 8D3C7EFCD6663C736563688A /* WalletsChooseViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsChooseViewController.swift; sourceTree = ""; }; 8D51D60F19284936A6E9F47D /* ReferralCrowdloanWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferralCrowdloanWireframe.swift; sourceTree = ""; }; 8D913590BB0E9435259719CD /* WalletsListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsListViewController.swift; sourceTree = ""; }; + 8D99FECD7EDCFE3A23E1AEAF /* StakingTypeWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingTypeWireframe.swift; sourceTree = ""; }; 8E1179D4A18F46C75B19CAC2 /* ParaStkRedeemProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkRedeemProtocols.swift; sourceTree = ""; }; 8E1A4406DA1DCAA35F11C8F1 /* ChainAddressDetailsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainAddressDetailsViewLayout.swift; sourceTree = ""; }; 8E1B9909758C171475DCC766 /* GovernanceDelegateInfoViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateInfoViewController.swift; sourceTree = ""; }; @@ -6808,18 +7288,18 @@ 8EE72F2B6612508D4783A507 /* DAppAuthSettingsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAuthSettingsProtocols.swift; sourceTree = ""; }; 8EE81AE154A736A93FF3812B /* GovernanceUnlockConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnlockConfirmViewLayout.swift; sourceTree = ""; }; 8F96182151D003DF6789CB4B /* DAppSearchProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppSearchProtocols.swift; sourceTree = ""; }; - 8F9C67FCF466D9EF48ED35D2 /* Pods-novawalletTests.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletTests.dev.xcconfig"; path = "Target Support Files/Pods-novawalletTests/Pods-novawalletTests.dev.xcconfig"; sourceTree = ""; }; 8FDF5963FA924F8C815F3BCF /* ParaStkRebondViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkRebondViewController.swift; sourceTree = ""; }; 902360B8577FAC9504F5854F /* LedgerAccountConfirmationPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerAccountConfirmationPresenter.swift; sourceTree = ""; }; 90681A889A40F7163CBE9B6B /* AssetsSettingsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetsSettingsProtocols.swift; sourceTree = ""; }; 906C55FC079AF6112AF0745B /* YourValidatorListWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourValidatorListWireframe.swift; sourceTree = ""; }; 90BA9C7259E59AC6CB422F82 /* TokensAddSelectNetworkProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensAddSelectNetworkProtocols.swift; sourceTree = ""; }; 90C2A8B3F0006DBCB4B7337A /* StakingRebagConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRebagConfirmProtocols.swift; sourceTree = ""; }; + 91C760EDD8CDD8073063D76D /* NominationPoolBondMoreSetupViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreSetupViewFactory.swift; sourceTree = ""; }; 91D44421CCD7AD220A05CD0E /* PurchaseInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PurchaseInteractor.swift; sourceTree = ""; }; 92381CBE193B3317F477D377 /* GovernanceRevokeDelegationConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceRevokeDelegationConfirmViewController.swift; sourceTree = ""; }; 935B5118367A118FC86B66C8 /* LedgerNetworkSelectionWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerNetworkSelectionWireframe.swift; sourceTree = ""; }; + 939D6CB66B9D5B86FEE5256E /* StakingSelectPoolViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSelectPoolViewLayout.swift; sourceTree = ""; }; 93B26AA9CB558F02F69FF59B /* ExportMnemonicConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportMnemonicConfirmWireframe.swift; sourceTree = ""; }; - 93C4B831433B7A96FF654763 /* Pods-novawalletTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletTests.debug.xcconfig"; path = "Target Support Files/Pods-novawalletTests/Pods-novawalletTests.debug.xcconfig"; sourceTree = ""; }; 94A44A00DAD3ADF020E2CB3D /* DelegateVotedReferendaProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegateVotedReferendaProtocols.swift; sourceTree = ""; }; 953E21C32079A8051A0EE964 /* ReferralCrowdloanProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferralCrowdloanProtocols.swift; sourceTree = ""; }; 95A04CDB05A013ED57D3DEA3 /* LedgerAccountConfirmationViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerAccountConfirmationViewController.swift; sourceTree = ""; }; @@ -6841,7 +7321,6 @@ 9925D7FC8A58695700B4A308 /* ParaStkStakeConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkStakeConfirmPresenter.swift; sourceTree = ""; }; 994E31F4AE945AF2DE98539C /* ChangeWatchOnlyWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChangeWatchOnlyWireframe.swift; sourceTree = ""; }; 998CDEAB9F149770B27F5317 /* AssetDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetDetailsProtocols.swift; sourceTree = ""; }; - 999C15317E0B4FC67B9C17C5 /* StakingAmountProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingAmountProtocols.swift; sourceTree = ""; }; 99C973B203F72D6233718CD4 /* ParaStkUnstakeConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkUnstakeConfirmProtocols.swift; sourceTree = ""; }; 99F4AC351A494FC4A89A6CF6 /* DelegationReferendumVotersProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegationReferendumVotersProtocols.swift; sourceTree = ""; }; 9ADD21F058AE84F533353158 /* WalletConnectSessionsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletConnectSessionsViewController.swift; sourceTree = ""; }; @@ -6851,6 +7330,7 @@ 9BCCD837A377C237C18B117E /* OperationDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OperationDetailsViewController.swift; sourceTree = ""; }; 9CDC7A44F6B01FE389F34C3A /* ParaStkStakeConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkStakeConfirmInteractor.swift; sourceTree = ""; }; 9D16A60434C1D9929E65998B /* ParaStkCollatorInfoViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkCollatorInfoViewFactory.swift; sourceTree = ""; }; + 9D9131BF410C62A93646CA0A /* NPoolsUnstakeConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeConfirmPresenter.swift; sourceTree = ""; }; 9D93D6B6DB7BACFEA6F2738C /* ParaStkCollatorInfoInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkCollatorInfoInteractor.swift; sourceTree = ""; }; 9DBACA1AB17E90565F133C19 /* WalletsListInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsListInteractor.swift; sourceTree = ""; }; 9F1BCCE09BB15106FDC02495 /* ParaStkRebondInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkRebondInteractor.swift; sourceTree = ""; }; @@ -6859,6 +7339,7 @@ 9FDCDDBF77A13891860416BB /* TokensAddSelectNetworkViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensAddSelectNetworkViewLayout.swift; sourceTree = ""; }; 9FDF20DCECDEA61E1BDE780B /* CrowdloanYourContributionsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanYourContributionsViewLayout.swift; sourceTree = ""; }; A028BD1A81CA95BE0DB66031 /* DAppTxDetailsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppTxDetailsViewFactory.swift; sourceTree = ""; }; + A084C41472C2E504575C709B /* NPoolsRedeemInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsRedeemInteractor.swift; sourceTree = ""; }; A0F285548A6F98B3EB3F170C /* NftDetailsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsViewLayout.swift; sourceTree = ""; }; A12FACE9CF804AF777024A31 /* ParitySignerAddressesViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerAddressesViewFactory.swift; sourceTree = ""; }; A14CA4551FCC2EBD078E2242 /* AccountConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountConfirmViewFactory.swift; sourceTree = ""; }; @@ -6866,36 +7347,39 @@ A18EBB9EF66ACF134B5BAEB4 /* GovernanceYourDelegationsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceYourDelegationsViewLayout.swift; sourceTree = ""; }; A1E3BDBC4DFACF9422ED6B5A /* TokensAddSelectNetworkPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensAddSelectNetworkPresenter.swift; sourceTree = ""; }; A27FB20961F2F221A96624A6 /* GovernanceDelegateConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateConfirmInteractor.swift; sourceTree = ""; }; - A2AEC47F0599E0AC45237639 /* Pods-novawalletAll-novawallet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawallet.release.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawallet/Pods-novawalletAll-novawallet.release.xcconfig"; sourceTree = ""; }; + A28FAB72B50D8C736AF67A39 /* StartStakingConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingConfirmProtocols.swift; sourceTree = ""; }; + A2BC4EFADD593325FF122765 /* StartStakingInfoBaseInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingInfoBaseInteractor.swift; sourceTree = ""; }; + A2DB310746B3C2B7DC09389F /* Pods-novawalletAll-novawalletIntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawalletIntegrationTests.release.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests.release.xcconfig"; sourceTree = ""; }; A2E14458DEC3317602A17527 /* GovernanceUnlockSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnlockSetupViewLayout.swift; sourceTree = ""; }; A2FCBFC59ED9D7E6F046D2A1 /* GovernanceYourDelegationsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceYourDelegationsProtocols.swift; sourceTree = ""; }; A3104ABC4BECF08B0BA836AA /* AccountConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountConfirmViewController.swift; sourceTree = ""; }; A31780E84948D7FE632ECB02 /* YourValidatorListProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourValidatorListProtocols.swift; sourceTree = ""; }; + A31B8F292DA050D1D19B9F5F /* NominationPoolSearchProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolSearchProtocols.swift; sourceTree = ""; }; A3BACB7E24BC87F9218DBBC4 /* StakingPayoutConfirmationViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPayoutConfirmationViewController.swift; sourceTree = ""; }; A3D49E5A3BB8ABFA1448BF43 /* GovernanceDelegateSearchPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateSearchPresenter.swift; sourceTree = ""; }; A43F25AF006B7B19281AC7B1 /* GovernanceDelegateConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateConfirmWireframe.swift; sourceTree = ""; }; A473EB36D61D5DF3BC69F7B9 /* AdvancedWalletViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AdvancedWalletViewLayout.swift; sourceTree = ""; }; A479F3338BE7DA2C846023FA /* AssetDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetDetailsViewController.swift; sourceTree = ""; }; - A4865344B432B891D5B48825 /* Pods-novawalletAll-novawalletIntegrationTests.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawalletIntegrationTests.dev.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests.dev.xcconfig"; sourceTree = ""; }; A4C8E60A4D1C8B4BBDA9408E /* DelegateVotedReferendaWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegateVotedReferendaWireframe.swift; sourceTree = ""; }; A4C96F37D7166820D6C9CC62 /* ParaStkCollatorInfoPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkCollatorInfoPresenter.swift; sourceTree = ""; }; A4F2B1A2A919663ABEE3367A /* MoonbeamTermsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MoonbeamTermsWireframe.swift; sourceTree = ""; }; A5F05632A6635A54A9CDA7FC /* OperationDetailsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OperationDetailsViewFactory.swift; sourceTree = ""; }; A6A61CD43EDD8791F6DA9581 /* GovernanceSelectTracksViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceSelectTracksViewFactory.swift; sourceTree = ""; }; + A71030ADF84393172807E434 /* StakingSetupAmountWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSetupAmountWireframe.swift; sourceTree = ""; }; A73D7BA4201018DF9163536D /* LedgerWalletConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerWalletConfirmProtocols.swift; sourceTree = ""; }; + A76E1F9D7AD364D8AD4CC721 /* StakingTypePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingTypePresenter.swift; sourceTree = ""; }; A7ACD8A8495274E41F711B77 /* ParitySignerAddressesPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerAddressesPresenter.swift; sourceTree = ""; }; A7AD1285797131E836CD994B /* AssetSelectionWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetSelectionWireframe.swift; sourceTree = ""; }; A813E09FA3047357C7A75564 /* DelegationReferendumVotersInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegationReferendumVotersInteractor.swift; sourceTree = ""; }; A82E373FFFBF708D7CF0973E /* StakingUnbondSetupViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondSetupViewFactory.swift; sourceTree = ""; }; A84638893DC99974E098719E /* StakingUnbondConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondConfirmWireframe.swift; sourceTree = ""; }; A865455F8FC60413A6CB8A44 /* ExportSeedInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportSeedInteractor.swift; sourceTree = ""; }; - A8683EB10308DBF8D445266F /* Pods-novawalletAll-novawalletIntegrationTests.staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawalletIntegrationTests.staging.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests.staging.xcconfig"; sourceTree = ""; }; A8FC21B4670E7B22B787357D /* WalletsListProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsListProtocols.swift; sourceTree = ""; }; A937F85FE35340EDA131C2EC /* TransactionHistoryViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransactionHistoryViewFactory.swift; sourceTree = ""; }; A9B778B6EC447C0B55110638 /* TokensManageViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManageViewLayout.swift; sourceTree = ""; }; AA2580363AC3E4A9CD40256E /* RecommendedValidatorListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RecommendedValidatorListPresenter.swift; sourceTree = ""; }; AA2BB29AE8E556E6756A4F02 /* DAppListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppListViewController.swift; sourceTree = ""; }; - AC14FB018B14852F670133DE /* Pods-novawalletAll-novawallet.staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawallet.staging.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawallet/Pods-novawalletAll-novawallet.staging.xcconfig"; sourceTree = ""; }; + AAC5294102607A80F62F9848 /* NPoolsUnstakeConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeConfirmViewFactory.swift; sourceTree = ""; }; AC155A29FA8777D90A46913D /* CrowdloanYourContributionsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanYourContributionsViewFactory.swift; sourceTree = ""; }; AC1626234F0BF7E20D351CB2 /* ReferendumSearchPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumSearchPresenter.swift; sourceTree = ""; }; AC3903A0A93FB8068071F9BD /* GovernanceDelegateSetupViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateSetupViewController.swift; sourceTree = ""; }; @@ -7036,6 +7520,7 @@ B193A261FDF933FE6C874B4E /* WalletConnectServiceFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletConnectServiceFactory.swift; sourceTree = ""; }; B1AC788C6E0A621B6D88D1BC /* ParaStkStakeSetupPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkStakeSetupPresenter.swift; sourceTree = ""; }; B1F9B478321689D963F51C4E /* LedgerInstructionsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerInstructionsViewFactory.swift; sourceTree = ""; }; + B213965350582497E2F86E26 /* NPoolsClaimRewardsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsClaimRewardsPresenter.swift; sourceTree = ""; }; B213C270130EDCF51303BFBE /* DAppAuthSettingsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAuthSettingsPresenter.swift; sourceTree = ""; }; B243F7A096241F329224A18E /* AssetReceiveInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetReceiveInteractor.swift; sourceTree = ""; }; B29514E516CEAAB159851D95 /* AccountImportProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountImportProtocols.swift; sourceTree = ""; }; @@ -7044,11 +7529,13 @@ B3CC9CAB00B604CD1AC5B4D8 /* NftDetailsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsViewFactory.swift; sourceTree = ""; }; B48994B2FA6FEE45A718252C /* DAppAuthConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAuthConfirmViewFactory.swift; sourceTree = ""; }; B4B15579DAD0DD84AEDA7D01 /* ParaStkYieldBoostStopPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStopPresenter.swift; sourceTree = ""; }; + B50ACC876575577A9D477A77 /* StakingTypeProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingTypeProtocols.swift; sourceTree = ""; }; B56202207DF8BB6684C6EF6C /* AdvancedWalletPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AdvancedWalletPresenter.swift; sourceTree = ""; }; B5BC1402B34E341312ABB378 /* LedgerWalletConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerWalletConfirmPresenter.swift; sourceTree = ""; }; B6884DFC1AA1B995C21C274C /* WalletHistoryFilterViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletHistoryFilterViewController.swift; sourceTree = ""; }; B718CE9C51158F87D37894BB /* GovernanceDelegateConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateConfirmViewFactory.swift; sourceTree = ""; }; B727587201B9D6F91A28428A /* ReferendumDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumDetailsViewController.swift; sourceTree = ""; }; + B73F89021BEE1F4576128305 /* StakingSetupAmountProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSetupAmountProtocols.swift; sourceTree = ""; }; B765BDAA27726E2586953368 /* OnChainTransferSetupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OnChainTransferSetupInteractor.swift; sourceTree = ""; }; B7CB6BF970620958C9DDD037 /* ParaStkStakeConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkStakeConfirmWireframe.swift; sourceTree = ""; }; B8A6C6207095F63972E14618 /* DAppPhishingProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppPhishingProtocols.swift; sourceTree = ""; }; @@ -7056,6 +7543,7 @@ B8B263D5668F1C91E2CF61D9 /* GovernanceRevokeDelegationConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceRevokeDelegationConfirmWireframe.swift; sourceTree = ""; }; B9035D0F739C88CCBE11E886 /* GovernanceRemoveVotesConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceRemoveVotesConfirmInteractor.swift; sourceTree = ""; }; B90CEC70F101AA25A4C00021 /* YourValidatorListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourValidatorListViewController.swift; sourceTree = ""; }; + B9D4A8C06D94C748124E6AA5 /* StartStakingInfoViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingInfoViewLayout.swift; sourceTree = ""; }; BA4D4C049E3B32A529DDFEB5 /* WalletConnectSessionDetailsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletConnectSessionDetailsViewLayout.swift; sourceTree = ""; }; BA518E1D79D86360F145B428 /* TokensAddSelectNetworkInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensAddSelectNetworkInteractor.swift; sourceTree = ""; }; BA7DAF20C447065DA5467696 /* TransactionHistoryViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransactionHistoryViewLayout.swift; sourceTree = ""; }; @@ -7074,6 +7562,7 @@ BDB66DA5010441586327E139 /* MoonbeamTermsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MoonbeamTermsViewController.swift; sourceTree = ""; }; BE0F7D4389B932A1F2E17361 /* DAppSearchViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppSearchViewLayout.swift; sourceTree = ""; }; BE103341935B2A4B8C32B966 /* WalletConnectSessionDetailsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletConnectSessionDetailsViewFactory.swift; sourceTree = ""; }; + BE86742207CDAD889A01B40A /* NPoolsUnstakeSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeSetupViewLayout.swift; sourceTree = ""; }; BF16BF87D3DEA31E6003C969 /* DelegationListWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegationListWireframe.swift; sourceTree = ""; }; BF2A45FEEA61BDB78BFAB0D7 /* DelegationReferendumVotersViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegationReferendumVotersViewController.swift; sourceTree = ""; }; BFCA1A3205F91128556F0F11 /* StakingDashboardViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingDashboardViewLayout.swift; sourceTree = ""; }; @@ -7086,12 +7575,14 @@ C2956D0C69019DDCDAB2EB34 /* CustomValidatorListViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CustomValidatorListViewLayout.swift; sourceTree = ""; }; C2CCB36A1265AA89D345B3CE /* TransactionHistoryProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransactionHistoryProtocols.swift; sourceTree = ""; }; C3ABAD23C0039AFA8351C650 /* ReferendumDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumDetailsProtocols.swift; sourceTree = ""; }; + C3E8ACEB2D157E376D53C6DE /* NominationPoolBondMoreConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreConfirmViewFactory.swift; sourceTree = ""; }; C4E807E9E12A130C50E8FFDF /* StakingDashboardViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingDashboardViewFactory.swift; sourceTree = ""; }; C503100478AB56E903598A78 /* ReferralCrowdloanPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferralCrowdloanPresenter.swift; sourceTree = ""; }; C52D6675524DB913210F0459 /* DAppSettingsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppSettingsPresenter.swift; sourceTree = ""; }; C5E9D289393AA2CC1E34C2F4 /* AssetDetailsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetDetailsWireframe.swift; sourceTree = ""; }; C74A2166B054240BD5D925B6 /* UsernameSetupViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UsernameSetupViewFactory.swift; sourceTree = ""; }; C80D934D47929D2331111AD7 /* ReferendumFullDetailsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumFullDetailsWireframe.swift; sourceTree = ""; }; + C92B3D5B314FB3EAE65FA471 /* StartStakingInfoProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingInfoProtocols.swift; sourceTree = ""; }; C96C3B5ABF4A8124848EFD17 /* ControllerAccountConfirmationWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountConfirmationWireframe.swift; sourceTree = ""; }; C995D640129977CAB05982EC /* TokensManageAddProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManageAddProtocols.swift; sourceTree = ""; }; C9978451AB2F4958E6FF117D /* YourWalletsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourWalletsViewFactory.swift; sourceTree = ""; }; @@ -7106,10 +7597,12 @@ CC5083A5751A1A3CC95F4F6F /* StakingUnbondSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondSetupWireframe.swift; sourceTree = ""; }; CCBCB7C3ABB6C06CD4681D44 /* LedgerTxConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerTxConfirmViewFactory.swift; sourceTree = ""; }; CD098B40697FD6CC08F9A6AC /* UsernameSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UsernameSetupProtocols.swift; sourceTree = ""; }; + CD38E012F7837AC692CA2C41 /* StakingSetupAmountViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSetupAmountViewLayout.swift; sourceTree = ""; }; CD6B5B187E83839481846C7E /* NftDetailsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsInteractor.swift; sourceTree = ""; }; CD7A6C62EC06FC3B2693FB43 /* GovernanceDelegateSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateSetupViewLayout.swift; sourceTree = ""; }; CDB47990BC7A594E663DAC00 /* ReferendumVoteConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVoteConfirmPresenter.swift; sourceTree = ""; }; CE98454DC77EAA01301B9BBF /* ParaStkCollatorFiltersProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkCollatorFiltersProtocols.swift; sourceTree = ""; }; + CF389223A781CA2088C7A4DD /* NPoolsClaimRewardsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsClaimRewardsWireframe.swift; sourceTree = ""; }; CF7A019F89C6CD418AEEE79C /* YourWalletsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YourWalletsPresenter.swift; sourceTree = ""; }; CF891BE39D442C2D06DDF3BB /* StakingRewardDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardDetailsProtocols.swift; sourceTree = ""; }; CFA54AB88E24A2053F289D74 /* GovernanceUnlockSetupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnlockSetupInteractor.swift; sourceTree = ""; }; @@ -7119,11 +7612,11 @@ D101339CC1292531CC4DB0AC /* StakingUnbondSetupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondSetupInteractor.swift; sourceTree = ""; }; D1852EB0DD9E0C39E0AAEE68 /* AssetListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetListPresenter.swift; sourceTree = ""; }; D19485BAFCB2056BDC135441 /* TokenManageSingleViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokenManageSingleViewFactory.swift; sourceTree = ""; }; + D256E075B9F6E26225C20B8C /* NPoolsRedeemPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsRedeemPresenter.swift; sourceTree = ""; }; D26BFF6789F0A11C5D870348 /* StackingRewardFiltersProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StackingRewardFiltersProtocols.swift; sourceTree = ""; }; D29FE1BF422468BECDCDEE63 /* GovernanceRemoveVotesConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceRemoveVotesConfirmPresenter.swift; sourceTree = ""; }; D2FCA2DD3A8898D64CBC9F97 /* AccountManagementViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountManagementViewController.swift; sourceTree = ""; }; D385A8FCB0E6F3F6B6872F01 /* TransferOnChainConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferOnChainConfirmPresenter.swift; sourceTree = ""; }; - D39D54DC9992CF9CB6699AA3 /* StakingAmountViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingAmountViewFactory.swift; sourceTree = ""; }; D45B7031E0809CED062C83F8 /* StakingUnbondConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingUnbondConfirmPresenter.swift; sourceTree = ""; }; D46DF4E6D8DAF6913474DED5 /* GovernanceYourDelegationsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceYourDelegationsInteractor.swift; sourceTree = ""; }; D4C391292F22D16427C77CD9 /* SelectValidatorsStartViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartViewFactory.swift; sourceTree = ""; }; @@ -7133,12 +7626,14 @@ D5AC65A04352405327BFE946 /* ReferendumDetailsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumDetailsInteractor.swift; sourceTree = ""; }; D613E20E96E7BA5B8F4B9799 /* StakingRewardDetailsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardDetailsInteractor.swift; sourceTree = ""; }; D6470B066E67834BF97E0A68 /* StakingRewardDetailsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRewardDetailsPresenter.swift; sourceTree = ""; }; + D686A91FF92C89FE8937EF5A /* NominationPoolSearchViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolSearchViewLayout.swift; sourceTree = ""; }; D6C6573C52692E4A56E35FF9 /* RecommendedValidatorListProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RecommendedValidatorListProtocols.swift; sourceTree = ""; }; + D6C738EAB236FB5D854A8D77 /* Pods-novawalletTests.staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletTests.staging.xcconfig"; path = "Target Support Files/Pods-novawalletTests/Pods-novawalletTests.staging.xcconfig"; sourceTree = ""; }; D6E8ABCF1EB5064F40B697CC /* ParitySignerAddConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerAddConfirmPresenter.swift; sourceTree = ""; }; D7280DD0C282CB1631C93DFB /* AssetsSettingsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetsSettingsPresenter.swift; sourceTree = ""; }; D74FF4FE525ADD8E7805481E /* AddDelegationViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddDelegationViewLayout.swift; sourceTree = ""; }; D779CD7E426E469760290DBC /* AddDelegationWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddDelegationWireframe.swift; sourceTree = ""; }; - D79EAD799CBB1ABB9541A232 /* Pods_novawalletTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_novawalletTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D790172C70C98CAE984F7183 /* NPoolsUnstakeConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeConfirmViewController.swift; sourceTree = ""; }; D7A0A5EE9BE2862B085712A0 /* AssetSelectionPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetSelectionPresenter.swift; sourceTree = ""; }; D7E4D8E59F0976D412FF0B10 /* LedgerAccountConfirmationProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerAccountConfirmationProtocols.swift; sourceTree = ""; }; D7FE5F01FC9364788A91EFA5 /* SelectValidatorsConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsConfirmProtocols.swift; sourceTree = ""; }; @@ -7153,12 +7648,14 @@ D9D163652744F9BF00681C1F /* ExternalContributionSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalContributionSource.swift; sourceTree = ""; }; D9D1636727451C2400681C1F /* AcalaLiquidContributionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcalaLiquidContributionResponse.swift; sourceTree = ""; }; D9DFFBEFB45E0A03FAA142C3 /* ReferendumsFiltersViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumsFiltersViewController.swift; sourceTree = ""; }; + D9E599C8CB87A955659DAEBF /* NPoolsUnstakeSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeSetupProtocols.swift; sourceTree = ""; }; DA086DFCCA0976489FD38B95 /* MoonbeamTermsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MoonbeamTermsInteractor.swift; sourceTree = ""; }; DA134C6DB56DE1DFBA1B88B4 /* AssetsSettingsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetsSettingsInteractor.swift; sourceTree = ""; }; + DA576BBCED647C22E93F0202 /* NominationPoolBondMoreConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreConfirmWireframe.swift; sourceTree = ""; }; DA9DD724F02DA0A174D875A8 /* CreateWatchOnlyViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateWatchOnlyViewLayout.swift; sourceTree = ""; }; - DB76FEC075A6FE1D246BA5DD /* StakingAmountViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingAmountViewController.swift; sourceTree = ""; }; DB7F5F9B54BE4234C5682BDE /* StakingRedeemViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRedeemViewController.swift; sourceTree = ""; }; DB8939D2E509D6FF66B7E117 /* InAppUpdatesProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = InAppUpdatesProtocols.swift; sourceTree = ""; }; + DB9AB4A7C57057ECFA1DA03E /* StartStakingConfirmViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingConfirmViewLayout.swift; sourceTree = ""; }; DBA49A762B2FBB66FD6A55FC /* ControllerAccountConfirmationProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountConfirmationProtocols.swift; sourceTree = ""; }; DBAD69608028E9FAE0F8E58D /* StakingDashboardViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingDashboardViewController.swift; sourceTree = ""; }; DBD0DBD280596DBBC5CE5A8F /* ParaStkYieldBoostStopProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStopProtocols.swift; sourceTree = ""; }; @@ -7168,14 +7665,17 @@ DD1A35F4D82F97C9663F1CD4 /* ReferendumVoteConfirmInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVoteConfirmInteractor.swift; sourceTree = ""; }; DD2F1EEBF48485F02BF690A4 /* ParaStkStakeSetupViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkStakeSetupViewController.swift; sourceTree = ""; }; DD4A5B4E6779AA27F10713C6 /* DAppWalletAuthViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppWalletAuthViewFactory.swift; sourceTree = ""; }; + DD71DD9CFF2B4BECCDEAF8C0 /* StakingSetupAmountViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSetupAmountViewFactory.swift; sourceTree = ""; }; DDB1A69B90DCA16C219BF087 /* GovernanceDelegateInfoPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateInfoPresenter.swift; sourceTree = ""; }; DDF3C1CFECE4340E82837FC4 /* ReferendumOnChainVotersViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumOnChainVotersViewFactory.swift; sourceTree = ""; }; DE767858B6CF5F6F7C7B418E /* ReferendumVoteConfirmProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVoteConfirmProtocols.swift; sourceTree = ""; }; DEE05ECCFE3DD11A2EAAF495 /* AssetDetailsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetDetailsViewLayout.swift; sourceTree = ""; }; DF715CEF29477B59119520F1 /* ParitySignerAddressesInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerAddressesInteractor.swift; sourceTree = ""; }; + DFDF265271A0E8EB0C777522 /* Pods-novawalletAll-novawallet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawallet.release.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawallet/Pods-novawalletAll-novawallet.release.xcconfig"; sourceTree = ""; }; DFF58EC3A44E4DDDFB4B5C84 /* ParaStkYieldBoostStopViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostStopViewLayout.swift; sourceTree = ""; }; E0DB5EA5195D9433A4B90793 /* AdvancedWalletWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AdvancedWalletWireframe.swift; sourceTree = ""; }; E11575D8B4F64C2E805372A5 /* AccountExportPasswordViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountExportPasswordViewFactory.swift; sourceTree = ""; }; + E12E4AA5C56575FD3ABA7693 /* NPoolsClaimRewardsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsClaimRewardsProtocols.swift; sourceTree = ""; }; E13BDB2E7FF04670131408DB /* StakingMoreOptionsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingMoreOptionsWireframe.swift; sourceTree = ""; }; E18B94164B49123E62FA60B7 /* AccountManagementViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountManagementViewFactory.swift; sourceTree = ""; }; E1E60EF37AC0A7646ED8FE64 /* AccountImportViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountImportViewFactory.swift; sourceTree = ""; }; @@ -7184,6 +7684,7 @@ E2E005960E331E8882A8C6FD /* StakingRebagConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRebagConfirmWireframe.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 = ""; }; + E4752D80077E85563CF3AD5D /* StakingTypeViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingTypeViewFactory.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 = ""; }; @@ -7198,6 +7699,7 @@ E6FE9E98CB265815986BE909 /* ReferendumVotersProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVotersProtocols.swift; sourceTree = ""; }; E765FD856EB60BBF344205F3 /* TokensAddSelectNetworkWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensAddSelectNetworkWireframe.swift; sourceTree = ""; }; E7B01586B548A0AB92695B40 /* CommonDelegationTracksViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommonDelegationTracksViewController.swift; sourceTree = ""; }; + E7F38FDB907F69A26B93B4E6 /* StakingSelectPoolViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingSelectPoolViewController.swift; sourceTree = ""; }; E83916DF00BA41516B94304E /* DelegationReferendumVotersViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegationReferendumVotersViewFactory.swift; sourceTree = ""; }; E88BFD7BD1254C2EC1F7E997 /* StakingDashboardPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingDashboardPresenter.swift; sourceTree = ""; }; E8B10C37813EFE7D7663605E /* ParitySignerAddressesWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerAddressesWireframe.swift; sourceTree = ""; }; @@ -7205,6 +7707,8 @@ E9636093217ABE05A7FAC9B9 /* AccountCreateViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountCreateViewFactory.swift; sourceTree = ""; }; E9F49B3B261FBA0B568A5320 /* ParaStkRebondPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkRebondPresenter.swift; sourceTree = ""; }; E9FBF368FBB46AD4DE606DB1 /* ReferendumVotersWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVotersWireframe.swift; sourceTree = ""; }; + EA72A61F65B09CB0D79A5BB2 /* Pods-novawalletAll-novawallet.staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawallet.staging.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawallet/Pods-novawalletAll-novawallet.staging.xcconfig"; sourceTree = ""; }; + EAF2717F0CC0A4529C925814 /* NominationPoolBondMoreSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreSetupViewLayout.swift; sourceTree = ""; }; EB8605FD90D8C3553A9897B4 /* AccountImportPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountImportPresenter.swift; sourceTree = ""; }; EC3B84FA1F22CC12B16C79AE /* GovernanceEditDelegationTracksWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceEditDelegationTracksWireframe.swift; sourceTree = ""; }; ECB56DD69C433B473E65C7EB /* AssetReceiveViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetReceiveViewController.swift; sourceTree = ""; }; @@ -7224,10 +7728,11 @@ EF75B8B6430531D2BF70C6E4 /* LedgerTxConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerTxConfirmPresenter.swift; sourceTree = ""; }; EF8D2EF3141D2AD73D272E20 /* AssetsSearchPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetsSearchPresenter.swift; sourceTree = ""; }; EFB278373745C20822442686 /* ExportSeedPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportSeedPresenter.swift; sourceTree = ""; }; + EFDA1FDA15E7DA2D0952166C /* Pods-novawalletTests.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletTests.dev.xcconfig"; path = "Target Support Files/Pods-novawalletTests/Pods-novawalletTests.dev.xcconfig"; sourceTree = ""; }; F02DBCA4A63A5E52E3739374 /* ControllerAccountConfirmationViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountConfirmationViewFactory.swift; sourceTree = ""; }; F04A75E7196E62F45CA9369D /* AssetDetailsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetDetailsInteractor.swift; sourceTree = ""; }; - F0548D67378CB1EFEC2D5784 /* Pods-novawalletAll-novawalletIntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawalletIntegrationTests.release.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests.release.xcconfig"; sourceTree = ""; }; F080BC55D9575EBE4216283C /* MarkdownDescriptionViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MarkdownDescriptionViewController.swift; sourceTree = ""; }; + F0C9473AB7D1FE4A27403078 /* NominationPoolBondMoreSetupViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreSetupViewController.swift; sourceTree = ""; }; F1A9B9D741BABBCE6C70BE45 /* LedgerDiscoverProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerDiscoverProtocols.swift; sourceTree = ""; }; F23E38DCBC74C528D7839B76 /* CrowdloanContributionSetupInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionSetupInteractor.swift; sourceTree = ""; }; F23EDFB699CAEEADC9263A0D /* DAppAuthSettingsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppAuthSettingsViewFactory.swift; sourceTree = ""; }; @@ -7352,19 +7857,22 @@ F52B8815D6AF5E69B145D245 /* CustomValidatorListViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CustomValidatorListViewFactory.swift; sourceTree = ""; }; F561E2D27FCEF7DEE3FE0B3D /* InAppUpdatesViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = InAppUpdatesViewController.swift; sourceTree = ""; }; F5E4CD58A9006CEB045E8977 /* ChangeWatchOnlyInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChangeWatchOnlyInteractor.swift; sourceTree = ""; }; + F5F1F933F624B01855AA3BA5 /* NPoolsClaimRewardsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsClaimRewardsInteractor.swift; sourceTree = ""; }; F61D8973ADEB461DE2AD3E13 /* RecommendedValidatorListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RecommendedValidatorListViewController.swift; sourceTree = ""; }; F63700316ADAC007DD318EC6 /* TransferSetupViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferSetupViewLayout.swift; sourceTree = ""; }; F69908EEEDB9A392FF756E65 /* CreateWatchOnlyViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateWatchOnlyViewFactory.swift; sourceTree = ""; }; F6A6E9A6472158C37D3A62F5 /* ParaStkYourCollatorsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYourCollatorsPresenter.swift; sourceTree = ""; }; + F6FC364054546921FA6A5D2B /* NPoolsRedeemProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsRedeemProtocols.swift; sourceTree = ""; }; F711415F7C805D43E83644C6 /* ParaStkUnstakeViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkUnstakeViewController.swift; sourceTree = ""; }; F74A26B3915B6E0C8C784423 /* StakingRebagConfirmViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRebagConfirmViewFactory.swift; sourceTree = ""; }; - F74A4F004D830083EB27B9A7 /* Pods-novawalletAll-novawallet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawallet.debug.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawallet/Pods-novawalletAll-novawallet.debug.xcconfig"; sourceTree = ""; }; F7944C833DD6EAB3100F50B2 /* ParaStkCollatorInfoWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkCollatorInfoWireframe.swift; sourceTree = ""; }; F829E7F8B39EE7D977001510 /* ControllerAccountProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ControllerAccountProtocols.swift; sourceTree = ""; }; F8C16637ADA10892106DD304 /* GovernanceDelegateInfoWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceDelegateInfoWireframe.swift; sourceTree = ""; }; + F8D55623AB063B11C1551012 /* Pods-novawalletAll-novawallet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-novawalletAll-novawallet.debug.xcconfig"; path = "Target Support Files/Pods-novawalletAll-novawallet/Pods-novawalletAll-novawallet.debug.xcconfig"; sourceTree = ""; }; FA3F824117720D3CE65A195F /* LedgerDiscoverPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerDiscoverPresenter.swift; sourceTree = ""; }; FA59CE2C7AE548ACA9D66FD7 /* CrowdloanContributionConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanContributionConfirmWireframe.swift; sourceTree = ""; }; FB2EA5D52F51F03FBAB490FE /* ChainAddressDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChainAddressDetailsViewController.swift; sourceTree = ""; }; + FB617C1637B277B83E4C4914 /* StartStakingConfirmViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StartStakingConfirmViewController.swift; sourceTree = ""; }; FBFDF844248CD43AAD13139F /* StakingPayoutConfirmationInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingPayoutConfirmationInteractor.swift; sourceTree = ""; }; FC122CEA0D33584669126731 /* WalletConnectSessionDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletConnectSessionDetailsViewController.swift; sourceTree = ""; }; FC224725C0C1743FDFED5F6E /* DelegationListPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegationListPresenter.swift; sourceTree = ""; }; @@ -7384,7 +7892,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A1621946D1A3383E275225CD /* Pods_novawalletAll_novawalletIntegrationTests.framework in Frameworks */, + B556794F1316E47FF2FDB3E7 /* Pods_novawalletAll_novawalletIntegrationTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7392,7 +7900,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 352B75BEB10A48CC6CE64D4A /* Pods_novawalletAll_novawallet.framework in Frameworks */, + C7C6236CA43E934F2A4EC779 /* Pods_novawalletAll_novawallet.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7400,7 +7908,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 80E265DD62D96597E4EAA44A /* Pods_novawalletTests.framework in Frameworks */, + 4825E3478A67C504E7E03936 /* Pods_novawalletTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -7559,13 +8067,67 @@ 0C02DF832A40935900CE53AC /* Amount */ = { isa = PBXGroup; children = ( + 0CE629D82AA9B68C00E250BD /* AssetBalanceViewModel.swift */, 0CB064692A40572C00BFBA3F /* AmountInputViewModel.swift */, 0C17BD962A42F162004AF9E7 /* WalletViewModelObserverContainer.swift */, 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */, + 0CE629D42AA9B5E200E250BD /* BalanceViewModel.swift */, + 0CE629D52AA9B5E200E250BD /* BalanceViewModelFactory.swift */, ); path = Amount; sourceTree = ""; }; + 0C13D2F82A7D469E0054BB6F /* Recommendation */ = { + isa = PBXGroup; + children = ( + 0C79C8942A7BE01100B171E3 /* RelaychainStakingRecommendation.swift */, + 0C13D2F62A7D45F40054BB6F /* RelaychainStakingRestrictions.swift */, + 0C79C89B2A7BE6A200B171E3 /* DirectStakingRecommendationFactory.swift */, + 0C79C89D2A7BEF1200B171E3 /* NominationPoolRecommendationFactory.swift */, + 0C13D2FB2A7D4B220054BB6F /* RelaychainStakingRestrictionsBuilder.swift */, + 0C13D2F92A7D473B0054BB6F /* DirectStakingRestrictionsBuilder.swift */, + 0C13D2FD2A7D4F500054BB6F /* PoolStakingRestrictionsBuilder.swift */, + 0C79C89F2A7BF80700B171E3 /* RelaychainStakingRecommendationMediator.swift */, + 0C13D2FF2A7D50C10054BB6F /* PoolStakingRecommendationMediator.swift */, + 0C13D2F42A7D2B440054BB6F /* DirectStakingRecommendationMediator.swift */, + 0C13D3012A7D53C10054BB6F /* HybridStakingRecommendationMediator.swift */, + 0C13D3032A7D56CC0054BB6F /* StakingRecommendationMediatorFactory.swift */, + 0C13D3122A80D06B0054BB6F /* StakingRecommendationValidationFactory.swift */, + ); + path = Recommendation; + sourceTree = ""; + }; + 0C13D3052A7FB9170054BB6F /* NominationPools */ = { + isa = PBXGroup; + children = ( + 0C13D3062A7FB92C0054BB6F /* NominationPoolsJoin.swift */, + 0CB261E12A9B215B00287305 /* NominationPoolUnstake.swift */, + 0CB261F22A9E182300287305 /* NominationPoolClaimRewards.swift */, + 0CB261F42A9E188300287305 /* NominationPoolsBondExtraCall.swift */, + 0C7E7FAA2A9F27FB00596628 /* NominationPoolsRedeemCall.swift */, + ); + path = NominationPools; + sourceTree = ""; + }; + 0C13D31B2A8227850054BB6F /* Presenter */ = { + isa = PBXGroup; + children = ( + 0C13D31C2A82279D0054BB6F /* StartStakingDirectConfirmPresenter.swift */, + 0C13D31E2A8227AB0054BB6F /* StartStakingPoolConfirmPresenter.swift */, + 0C13D3202A822B220054BB6F /* StartStakingDirectConfirmWireframe.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 0C13D3222A823D6D0054BB6F /* ExtrinsicProxy */ = { + isa = PBXGroup; + children = ( + 0C13D3232A823D810054BB6F /* StartStakingExtrinsicProxy.swift */, + 0C13D3252A82753F0054BB6F /* StartStakingFeeIdFactory.swift */, + ); + path = ExtrinsicProxy; + sourceTree = ""; + }; 0C1FE4F22A52EDD5003769E7 /* Model */ = { isa = PBXGroup; children = ( @@ -7575,6 +8137,25 @@ path = Model; sourceTree = ""; }; + 0C2F86872A723E4200593C01 /* NominationPools */ = { + isa = PBXGroup; + children = ( + 0C2F86882A723E5400593C01 /* NominationPoolsOperationFactory.swift */, + 77895CA22A8F8CFD006870FB /* NominationPoolsFilters.swift */, + 0C626D1E2A92AA0F00CDAF4E /* NominationPoolsDataProviding.swift */, + ); + path = NominationPools; + sourceTree = ""; + }; + 0C2F86942A72804D00593C01 /* NominationPools */ = { + isa = PBXGroup; + children = ( + 0C2F86952A72807E00593C01 /* NominationPoolsRewardEngine.swift */, + 0C2F86972A728EE900593C01 /* NPoolsRewardEngineFactory.swift */, + ); + path = NominationPools; + sourceTree = ""; + }; 0C3205BC2A867A46002EB914 /* GasPriceProviders */ = { isa = PBXGroup; children = ( @@ -7637,6 +8218,122 @@ path = Effects; sourceTree = ""; }; + 0C59E8CA2AA5D621001E11F3 /* ExternalBalanceUpdater */ = { + isa = PBXGroup; + children = ( + 0C59E8CB2AA5D631001E11F3 /* PooledBalanceService */, + 880855EE28D099BD004255E7 /* CrowdloanService */, + ); + path = ExternalBalanceUpdater; + sourceTree = ""; + }; + 0C59E8CB2AA5D631001E11F3 /* PooledBalanceService */ = { + isa = PBXGroup; + children = ( + 0C59E8CC2AA5D673001E11F3 /* PooledBalanceUpdatingService.swift */, + 0C59E8CE2AA5D744001E11F3 /* PooledBalanceUpdatingState.swift */, + ); + path = PooledBalanceService; + sourceTree = ""; + }; + 0C59E8D62AA6047A001E11F3 /* ExternalAssetBalance */ = { + isa = PBXGroup; + children = ( + 0C59E8D72AA60490001E11F3 /* ExternalAssetBalanceStreambleSource.swift */, + ); + path = ExternalAssetBalance; + sourceTree = ""; + }; + 0C59E8DD2AA60D90001E11F3 /* ExternalAssetBalanceFactory */ = { + isa = PBXGroup; + children = ( + 0C59E8D92AA6085B001E11F3 /* ExternalAssetBalanceLocalSubscriptionFactory.swift */, + 0C59E8DE2AA60DAB001E11F3 /* ExternalAssetBalanceServiceFactoryProtocol.swift */, + 0C59E8E02AA60FF0001E11F3 /* CrowdloanExternalServiceFactory.swift */, + 0C59E8E22AA61252001E11F3 /* NominationPoolExternalServiceFactory.swift */, + ); + path = ExternalAssetBalanceFactory; + sourceTree = ""; + }; + 0C59E8EE2AA76331001E11F3 /* OperationDataProviders */ = { + isa = PBXGroup; + children = ( + 0C59E8EF2AA76361001E11F3 /* OperationDetailsDataProviderProtocol.swift */, + 0C59E8F12AA76436001E11F3 /* OperationDetailsTransferProvider.swift */, + 0C59E8F32AA7649E001E11F3 /* OperationDetailsBaseProvider.swift */, + 0C59E8F52AA76772001E11F3 /* OperationDetailsExtrinsicProvider.swift */, + 0C59E8F72AA76833001E11F3 /* OperationDetailsContractProvider.swift */, + 0C59E8F92AA76A4A001E11F3 /* OperationDetailsDirectStakingProvider.swift */, + 0C59E8FB2AA76C4A001E11F3 /* OperationDetailsPoolStakingProvider.swift */, + 0C59E8FD2AA773D6001E11F3 /* OperationDetailsDataProviderFactory.swift */, + ); + path = OperationDataProviders; + sourceTree = ""; + }; + 0C6610292A73814900E44634 /* StakingSharedState */ = { + isa = PBXGroup; + children = ( + 0C66102A2A73816000E44634 /* StakingSharedStateFactory.swift */, + 0C66102C2A73828800E44634 /* RelaychainStakingSharedState.swift */, + 841E5544282D793900C8438F /* ParachainStakingSharedState.swift */, + 0C66102E2A78E9D700E44634 /* RelaychainStartStakingState.swift */, + 0CAC44AB2A7A7FFD001EDE61 /* RelaychainConsensusStateDepending.swift */, + 0C9C64372A8D6949004DC078 /* NPoolsStakingSharedState.swift */, + ); + path = StakingSharedState; + sourceTree = ""; + }; + 0C77B55D2A83713D00B5AE08 /* StaticValidatorList */ = { + isa = PBXGroup; + children = ( + 0C77B55E2A83717000B5AE08 /* StaticValidatorListViewController.swift */, + 0C77B5602A8371AA00B5AE08 /* StaticValidatorListProtocols.swift */, + 0C77B5622A83747200B5AE08 /* StaticValidatorListViewLayout.swift */, + 0C77B5642A8374EA00B5AE08 /* StaticValidatorListPresenter.swift */, + 0C77B5662A837AC500B5AE08 /* StaticValidatorListWireframe.swift */, + 0C77B5682A837D4000B5AE08 /* StaticValidatorListViewFactory.swift */, + ); + path = StaticValidatorList; + sourceTree = ""; + }; + 0C79C8932A7BDED700B171E3 /* TitleHorizontalMultivalueView */ = { + isa = PBXGroup; + children = ( + 84D1ABDF27E1CB870073C631 /* TitleHorizontalMultiValueView.swift */, + 77864F4B2A6AC5A100FA7BA7 /* TitleHorizontalMultiValueView+Bind.swift */, + ); + path = TitleHorizontalMultivalueView; + sourceTree = ""; + }; + 0C7C88622A94DFE000DD96A1 /* NominationPools */ = { + isa = PBXGroup; + children = ( + 0C7C88632A94E09F00DD96A1 /* NPoolsPendingRewardDataSource.swift */, + ); + path = NominationPools; + sourceTree = ""; + }; + 0C893E6B2A65629E00781503 /* NominationPools */ = { + isa = PBXGroup; + children = ( + 0C893E6C2A6562B400781503 /* NominationPools.swift */, + 0C893E6E2A65702A00781503 /* NominationPools+CodingPath.swift */, + 0CB06E722A6800F500C7EC99 /* NominationPools+Functions.swift */, + ); + path = NominationPools; + sourceTree = ""; + }; + 0C962F842AA859DE00C0B551 /* LocalFilter */ = { + isa = PBXGroup; + children = ( + 0C962F852AA859F200C0B551 /* TransactionHistoryLocalFilter.swift */, + 0CCE25202A44306200286709 /* TransactionHistoryPhishingFilter.swift */, + 0C962F872AA85C7F00C0B551 /* TransactionHistoryAccountPrefixFilter.swift */, + 0C962F892AA8614500C0B551 /* TransactionHistoryLocalFilterFactory.swift */, + ); + path = LocalFilter; + sourceTree = ""; + }; 0C9C642B2A8CE2D4004DC078 /* SystemAccounts */ = { isa = PBXGroup; children = ( @@ -7645,6 +8342,98 @@ path = SystemAccounts; sourceTree = ""; }; + 0C9C642E2A8D675B004DC078 /* NominationPools */ = { + isa = PBXGroup; + children = ( + 0C9C643B2A8E19F4004DC078 /* ViewModel */, + 0C9C642F2A8D6779004DC078 /* StakingNPoolsPresenter.swift */, + 0C9C64312A8D67A0004DC078 /* StakingNPoolsInteractor.swift */, + 0C9C64332A8D67AF004DC078 /* StakingNPoolsWireframe.swift */, + 0C9C64352A8D67FB004DC078 /* StakingNPoolsProtocols.swift */, + 0C9C64392A8DF97E004DC078 /* StakingNPoolsError.swift */, + 0C626D1A2A8F519100CDAF4E /* StakingMainPresenterFactory+NominationPools.swift */, + ); + path = NominationPools; + sourceTree = ""; + }; + 0C9C643B2A8E19F4004DC078 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 0C626D1C2A915E2F00CDAF4E /* StakingNominationPoolsStatics.swift */, + 0C626D202A933A7D00CDAF4E /* StakingNPoolsViewModelFactory.swift */, + 0C7C88602A94A54E00DD96A1 /* StakingNPoolsViewModelFactory+Alerts.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 0CB261D52A97A4D300287305 /* NominationPools */ = { + isa = PBXGroup; + children = ( + 139168F2E6530E3946753501 /* Redeem */, + F4CFBAC4468FAD5728719A7D /* ClaimRewards */, + 0CB261D62A9893BD00287305 /* Unstake */, + D1AABAFE54CA7C8321264E64 /* BondMore */, + ); + path = NominationPools; + sourceTree = ""; + }; + 0CB261D62A9893BD00287305 /* Unstake */ = { + isa = PBXGroup; + children = ( + 0CB261E82A9C93F500287305 /* Operation */, + 0CB261E52A9C7C8100287305 /* ViewModel */, + 0CB261DC2A989D1700287305 /* Model */, + DF1349A0832B89817308F2A1 /* Setup */, + C74D63D387B28857B9B02289 /* Confirm */, + 0CB261D72A9893C400287305 /* Base */, + ); + path = Unstake; + sourceTree = ""; + }; + 0CB261D72A9893C400287305 /* Base */ = { + isa = PBXGroup; + children = ( + 0CB261D82A9893E500287305 /* NPoolsUnstakeBaseProtocols.swift */, + 0CB261DA2A98943800287305 /* NPoolsUnstakeBaseError.swift */, + 0CB261DF2A98BEBD00287305 /* NPoolsUnstakeBaseInteractor.swift */, + 0CB261E32A9BE31B00287305 /* NPoolsUnstakeBasePresenter.swift */, + ); + path = Base; + sourceTree = ""; + }; + 0CB261DC2A989D1700287305 /* Model */ = { + isa = PBXGroup; + children = ( + 0CB261DD2A989D2A00287305 /* NominationPoolsUnstakeLimits.swift */, + ); + path = Model; + sourceTree = ""; + }; + 0CB261E52A9C7C8100287305 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 0CB261E62A9C7C9D00287305 /* NPoolsUnstakeHintsFactory.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 0CB261E82A9C93F500287305 /* Operation */ = { + isa = PBXGroup; + children = ( + 0CB261E92A9C940A00287305 /* NominationPoolsUnstakeOperationFactory.swift */, + ); + path = Operation; + sourceTree = ""; + }; + 0CB261ED2A9E102700287305 /* Model */ = { + isa = PBXGroup; + children = ( + 0CB261EE2A9E103900287305 /* NPoolsClaimRewardsStrategy.swift */, + 0CB261F02A9E149C00287305 /* NPoolsClaimRewardsError.swift */, + ); + path = Model; + sourceTree = ""; + }; 0CE550B42A4973BA00F0A7AC /* StakingUnbondSetup */ = { isa = PBXGroup; children = ( @@ -7711,6 +8500,21 @@ path = DelegateVotedReferenda; sourceTree = ""; }; + 139168F2E6530E3946753501 /* Redeem */ = { + isa = PBXGroup; + children = ( + F6FC364054546921FA6A5D2B /* NPoolsRedeemProtocols.swift */, + 2A651A934B32A7978850D639 /* NPoolsRedeemWireframe.swift */, + D256E075B9F6E26225C20B8C /* NPoolsRedeemPresenter.swift */, + A084C41472C2E504575C709B /* NPoolsRedeemInteractor.swift */, + 862545D6BD501914AE04D776 /* NPoolsRedeemViewController.swift */, + 23EF6FA61F2B7E3B2ADD3200 /* NPoolsRedeemViewLayout.swift */, + 88B13FCAD745A663807219B1 /* NPoolsRedeemViewFactory.swift */, + 0CB261F82A9F1F2200287305 /* NPoolsRedeemError.swift */, + ); + path = Redeem; + sourceTree = ""; + }; 1398F9A5DE987487202A580E /* WalletConnect */ = { isa = PBXGroup; children = ( @@ -7785,6 +8589,24 @@ path = DAppAddFavorite; sourceTree = ""; }; + 17432B4B5D8D9DC5C22CA238 /* StakingType */ = { + isa = PBXGroup; + children = ( + 77799AEA2A7CFB4500B7E564 /* Model */, + 77EFFC922A72A288009E28F8 /* StakingTypeBaseBannerView.swift */, + 77EFFC8E2A714C21009E28F8 /* StakingTypeBannerView.swift */, + B50ACC876575577A9D477A77 /* StakingTypeProtocols.swift */, + 8D99FECD7EDCFE3A23E1AEAF /* StakingTypeWireframe.swift */, + A76E1F9D7AD364D8AD4CC721 /* StakingTypePresenter.swift */, + 663DD0E9F1F758C06E7EC829 /* StakingTypeInteractor.swift */, + 3930902D540DB0B9A2CFD21C /* StakingTypeViewController.swift */, + 64E66B662DD15424D2B383A7 /* StakingTypeViewLayout.swift */, + E4752D80077E85563CF3AD5D /* StakingTypeViewFactory.swift */, + 77CC82A62A986CF1002D022F /* StakingSelectValidatorsDelegate.swift */, + ); + path = StakingType; + sourceTree = ""; + }; 17C8516C4BEF61B6DE078C60 /* DAppAuthSettings */ = { isa = PBXGroup; children = ( @@ -7849,18 +8671,18 @@ 2698CD398B0412EB85D620AB /* Pods */ = { isa = PBXGroup; children = ( - F74A4F004D830083EB27B9A7 /* Pods-novawalletAll-novawallet.debug.xcconfig */, - 79BC90C31B17F8935FE8456F /* Pods-novawalletAll-novawallet.dev.xcconfig */, - A2AEC47F0599E0AC45237639 /* Pods-novawalletAll-novawallet.release.xcconfig */, - AC14FB018B14852F670133DE /* Pods-novawalletAll-novawallet.staging.xcconfig */, - 5EAF3AEE27F7901458B39A7A /* Pods-novawalletAll-novawalletIntegrationTests.debug.xcconfig */, - A4865344B432B891D5B48825 /* Pods-novawalletAll-novawalletIntegrationTests.dev.xcconfig */, - F0548D67378CB1EFEC2D5784 /* Pods-novawalletAll-novawalletIntegrationTests.release.xcconfig */, - A8683EB10308DBF8D445266F /* Pods-novawalletAll-novawalletIntegrationTests.staging.xcconfig */, - 93C4B831433B7A96FF654763 /* Pods-novawalletTests.debug.xcconfig */, - 8F9C67FCF466D9EF48ED35D2 /* Pods-novawalletTests.dev.xcconfig */, - 8ADA5C374888879D27DBAA29 /* Pods-novawalletTests.release.xcconfig */, - 78B08033E84541B42A6EAFEE /* Pods-novawalletTests.staging.xcconfig */, + F8D55623AB063B11C1551012 /* Pods-novawalletAll-novawallet.debug.xcconfig */, + 33DE8A571FE8D0431FD934A9 /* Pods-novawalletAll-novawallet.dev.xcconfig */, + DFDF265271A0E8EB0C777522 /* Pods-novawalletAll-novawallet.release.xcconfig */, + EA72A61F65B09CB0D79A5BB2 /* Pods-novawalletAll-novawallet.staging.xcconfig */, + 5ACCF5C31EF0E346D0763897 /* Pods-novawalletAll-novawalletIntegrationTests.debug.xcconfig */, + 48E5BB1EB494B5DB92FC3053 /* Pods-novawalletAll-novawalletIntegrationTests.dev.xcconfig */, + A2DB310746B3C2B7DC09389F /* Pods-novawalletAll-novawalletIntegrationTests.release.xcconfig */, + 2E5C5EE99A4B73789BE23039 /* Pods-novawalletAll-novawalletIntegrationTests.staging.xcconfig */, + 60EAEB7059DC148A36865DA8 /* Pods-novawalletTests.debug.xcconfig */, + EFDA1FDA15E7DA2D0952166C /* Pods-novawalletTests.dev.xcconfig */, + 50C441FA177F6838F6902786 /* Pods-novawalletTests.release.xcconfig */, + D6C738EAB236FB5D854A8D77 /* Pods-novawalletTests.staging.xcconfig */, ); path = Pods; sourceTree = ""; @@ -8320,22 +9142,6 @@ path = StakingRewardFilters; sourceTree = ""; }; - 68DEED3BF634220D4BF7A9C8 /* StakingAmount */ = { - isa = PBXGroup; - children = ( - 84C2F27B25E2971A0050A4AD /* Model */, - 8449660825E15EBC00F2E9F5 /* ViewModel */, - 999C15317E0B4FC67B9C17C5 /* StakingAmountProtocols.swift */, - 6216F6F1B91F798F07695FB6 /* StakingAmountWireframe.swift */, - 40B47961B2254E8A4D8EC588 /* StakingAmountPresenter.swift */, - 312DE7ADA5ABC3214AD3D4AD /* StakingAmountInteractor.swift */, - DB76FEC075A6FE1D246BA5DD /* StakingAmountViewController.swift */, - D39D54DC9992CF9CB6699AA3 /* StakingAmountViewFactory.swift */, - 84D17ECB2803F7EF00F7BAFF /* StakingAmountLayout.swift */, - ); - path = StakingAmount; - sourceTree = ""; - }; 6B9FCD7484C894D77EE13328 /* DelegateSearch */ = { isa = PBXGroup; children = ( @@ -8391,6 +9197,33 @@ path = TransactionQr; sourceTree = ""; }; + 6F5BD6E623DF3F7DFEDC3EFA /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0CC41A9A168AF0F1FBE5F799 /* Pods_novawalletAll_novawallet.framework */, + 89CA10C284A0C9853E043A9D /* Pods_novawalletAll_novawalletIntegrationTests.framework */, + 3D931049E1775C8D9C4EEC9D /* Pods_novawalletTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 7066B343B912F72345D541F2 /* StakingSetupAmount */ = { + isa = PBXGroup; + children = ( + 77F033912A814285006BC67E /* View */, + 0C13D2F82A7D469E0054BB6F /* Recommendation */, + 77799AE32A792ACB00B7E564 /* Model */, + B73F89021BEE1F4576128305 /* StakingSetupAmountProtocols.swift */, + A71030ADF84393172807E434 /* StakingSetupAmountWireframe.swift */, + 8145FD81205F06EB6B8B51DA /* StakingSetupAmountInteractor.swift */, + 6F80E6C0C3AC88D7CDB8204F /* StakingSetupAmountPresenter.swift */, + 6708EF7D7F70868969EFADA9 /* StakingSetupAmountViewController.swift */, + CD38E012F7837AC692CA2C41 /* StakingSetupAmountViewLayout.swift */, + DD71DD9CFF2B4BECCDEAF8C0 /* StakingSetupAmountViewFactory.swift */, + ); + path = StakingSetupAmount; + sourceTree = ""; + }; 71E66F0C5FF36EBFFEA3D807 /* DelegationReferendumVoters */ = { isa = PBXGroup; children = ( @@ -8406,6 +9239,15 @@ path = DelegationReferendumVoters; sourceTree = ""; }; + 770F57892A8A48F7005FD7C1 /* Model */ = { + isa = PBXGroup; + children = ( + 77F033A72A86136D006BC67E /* StakingSelectPoolViewModelFactory.swift */, + 770F578A2A8A48FF005FD7C1 /* ButtonViewModel.swift */, + ); + path = Model; + sourceTree = ""; + }; 7726E232D196BDD627329E24 /* ParaStkStakeSetup */ = { isa = PBXGroup; children = ( @@ -8422,6 +9264,26 @@ path = ParaStkStakeSetup; sourceTree = ""; }; + 7738FB5B2A4C5C2000797439 /* ParachainStaking */ = { + isa = PBXGroup; + children = ( + 775F19522A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift */, + 775F19502A5811FA009915B6 /* StartStakingParachainInteractor.swift */, + 0CAC44A92A7A79C2001EDE61 /* StartStakingInfoParachainWireframe.swift */, + ); + path = ParachainStaking; + sourceTree = ""; + }; + 7738FB5C2A4C5C3600797439 /* RelaychainStaking */ = { + isa = PBXGroup; + children = ( + 775F194C2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift */, + 7738FB692A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift */, + 77799ADE2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift */, + ); + path = RelaychainStaking; + sourceTree = ""; + }; 775692822A24CA5100220756 /* AssetOperation */ = { isa = PBXGroup; children = ( @@ -8435,6 +9297,36 @@ path = AssetOperation; sourceTree = ""; }; + 77799AE32A792ACB00B7E564 /* Model */ = { + isa = PBXGroup; + children = ( + 0CF193D02A843DA9003F12F6 /* StakingTypeBalanceFactory.swift */, + 77799ADC2A74219A00B7E564 /* ButtonState.swift */, + 77799AE42A792AE700B7E564 /* StakingTypeViewModel.swift */, + 77799AE62A792B6F00B7E564 /* StakingTypeAccountViewModel.swift */, + 77F033982A8142E5006BC67E /* DirectStakingTypeAccountViewModel.swift */, + 77F0339A2A814505006BC67E /* StakingSelectionMethod.swift */, + 77EFFC8C2A6EECFD009E28F8 /* StakingAmountViewModelFactory.swift */, + 0CF193D22A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift */, + 0CE629E12AA9BE3000E250BD /* StakingSetupTypeEntityFacade.swift */, + ); + path = Model; + sourceTree = ""; + }; + 77799AEA2A7CFB4500B7E564 /* Model */ = { + isa = PBXGroup; + children = ( + 77799AE82A7C99D200B7E564 /* StakingTypeViewModelFactory.swift */, + 77799AEB2A7CFB5700B7E564 /* PoolStakingTypeViewModel.swift */, + 77799AED2A7CFB6A00B7E564 /* DirectStakingTypeViewModel.swift */, + 77799AEF2A7CFB7C00B7E564 /* ValidatorViewModel.swift */, + 77799AF12A7CFB8D00B7E564 /* PoolAccountViewModel.swift */, + 77F0339C2A837AB3006BC67E /* StakingTypeSelection.swift */, + 7726CD542A9728D700CE9064 /* StakingTypeSelectedStakingViewModelFactory.swift */, + ); + path = Model; + sourceTree = ""; + }; 777BD85E29F972FF004969A2 /* Model */ = { isa = PBXGroup; children = ( @@ -8444,6 +9336,20 @@ path = Model; sourceTree = ""; }; + 777CA80B2AA0B8DD00E99751 /* Confirm */ = { + isa = PBXGroup; + children = ( + 894A6BB613EA991A09976B30 /* NominationPoolBondMoreConfirmProtocols.swift */, + DA576BBCED647C22E93F0202 /* NominationPoolBondMoreConfirmWireframe.swift */, + 71D3243E13077BFD9DAC8FFC /* NominationPoolBondMoreConfirmPresenter.swift */, + 73D6B3F74819B5D45324F714 /* NominationPoolBondMoreConfirmInteractor.swift */, + 2ADA652B86975A2044ABB065 /* NominationPoolBondMoreConfirmViewController.swift */, + 2F5A8E5D1C5A7F0B5B1570B9 /* NominationPoolBondMoreConfirmViewLayout.swift */, + C3E8ACEB2D157E376D53C6DE /* NominationPoolBondMoreConfirmViewFactory.swift */, + ); + path = Confirm; + sourceTree = ""; + }; 778D97A02A24D459002BA681 /* AssetSearch */ = { isa = PBXGroup; children = ( @@ -8614,6 +9520,80 @@ path = Model; sourceTree = ""; }; + 77F033912A814285006BC67E /* View */ = { + isa = PBXGroup; + children = ( + 77EFFC902A7276F1009E28F8 /* StakingTypeAccountView.swift */, + 77F033922A814296006BC67E /* GenericStakingTypeAccountView.swift */, + 77F033942A8142B0006BC67E /* StakingTypeValidatorView.swift */, + 77F033962A8142D1006BC67E /* StakingSetupAmountStyles.swift */, + ); + path = View; + sourceTree = ""; + }; + 77F033A02A84E005006BC67E /* View */ = { + isa = PBXGroup; + children = ( + 939D6CB66B9D5B86FEE5256E /* StakingSelectPoolViewLayout.swift */, + 77F033A32A84E028006BC67E /* StakingSelectPoolListHeaderView.swift */, + 77F033A12A84E00F006BC67E /* StakingPoolView.swift */, + 77F033A52A84EAC3006BC67E /* StakingPoolTableViewCell.swift */, + 770F57872A8A2CE0005FD7C1 /* StakingSelectPoolViewStyles.swift */, + ); + path = View; + sourceTree = ""; + }; + 77F1893C2A4996F300E8B933 /* View */ = { + isa = PBXGroup; + children = ( + 77F1893D2A4996FC00E8B933 /* ParagraphView.swift */, + 77F1893F2A49972300E8B933 /* UILabel+bind.swift */, + 77F189432A49974A00E8B933 /* UITextView+bind.swift */, + B9D4A8C06D94C748124E6AA5 /* StartStakingInfoViewLayout.swift */, + ); + path = View; + sourceTree = ""; + }; + 77F189452A49BD5E00E8B933 /* Model */ = { + isa = PBXGroup; + children = ( + 77EFFC892A6E7A24009E28F8 /* DefaultStakingRewardDestination.swift */, + 77EFFC882A6E7A24009E28F8 /* AccountExistense.swift */, + 0C2F868C2A725E4F00593C01 /* AccountExistense.swift */, + 0C2F868D2A725E4F00593C01 /* DefaultStakingRewardDestination.swift */, + 0C6F0C9C2A69723B007170C6 /* StartStakingStateProtocol.swift */, + 77F189462A49BD6700E8B933 /* StartStakingViewModel.swift */, + 77F189482A4A299800E8B933 /* StartStakingViewModelFactory.swift */, + ); + path = Model; + sourceTree = ""; + }; + 77F9FB042A9D96A700820625 /* Base */ = { + isa = PBXGroup; + children = ( + 779C8BE72AA1DD1B001A4A3C /* NominationPoolsBondMoreHintsFactory.swift */, + 0BA85EE628D7029AC940DFA3 /* NominationPoolBondMoreBasePresenter.swift */, + 84033496FC259BCA5420D52B /* NominationPoolBondMoreBaseInteractor.swift */, + 6A4009AE7EF95E9CE1EB88B8 /* NominationPoolBondMoreBaseWireframe.swift */, + 77F9FB0C2A9D9C5600820625 /* NominationPoolBondMoreBaseProtocols.swift */, + ); + path = Base; + sourceTree = ""; + }; + 77F9FB052A9D96DF00820625 /* Setup */ = { + isa = PBXGroup; + children = ( + 77F9FB062A9D96E900820625 /* NominationPoolBondMoreSetupPresenter.swift */, + 77F9FB082A9D971000820625 /* NominationPoolBondMoreSetupInteractor.swift */, + 77F9FB0A2A9D97A100820625 /* NominationPoolBondMoreSetupWireframe.swift */, + 725529DC6B70BF5B091B3748 /* NominationPoolBondMoreSetupProtocols.swift */, + EAF2717F0CC0A4529C925814 /* NominationPoolBondMoreSetupViewLayout.swift */, + F0C9473AB7D1FE4A27403078 /* NominationPoolBondMoreSetupViewController.swift */, + 91C760EDD8CDD8073063D76D /* NominationPoolBondMoreSetupViewFactory.swift */, + ); + path = Setup; + sourceTree = ""; + }; 77FC42A7DE84D3B2DC6EEE54 /* GovernanceSelectTracks */ = { isa = PBXGroup; children = ( @@ -8908,10 +9888,10 @@ 84155DE8253980D700A27058 /* Services */ = { isa = PBXGroup; children = ( + 0C59E8CA2AA5D621001E11F3 /* ExternalBalanceUpdater */, 8455F1912A1DC631003F072D /* Multistaking */, 8490111229E68FAD005D688B /* WalletConnect */, 8863C7AA29D499BC0068AD54 /* Web3Name */, - 880855EE28D099BD004255E7 /* CrowdloanService */, 8411707D285B15C8006F4DFB /* XcmService */, 8466781427EC9B6C007935D3 /* PersistExtrinsicService */, 841AAC2B26F7311200F0A25E /* RemoteSubscription */, @@ -8994,7 +9974,6 @@ 841E5562282E9EF400C8438F /* ParachainStakingCommonData.swift */, 841E5564282EA76C00C8438F /* ParachainStakingBaseState.swift */, 841E5566282EAC1000C8438F /* ParachainStakingInitState.swift */, - 841E5568282EAC2600C8438F /* ParachainStakingNoStakingState.swift */, 841E556A282EAC3600C8438F /* ParachainStakingDelegatorState.swift */, ); path = States; @@ -9003,6 +9982,7 @@ 841E6B0825EC1B560007DDFE /* Operations */ = { isa = PBXGroup; children = ( + 0C2F86872A723E4200593C01 /* NominationPools */, 846952A02852A1160083E0B4 /* StakingDuration */, 84C3420528314D5A00156569 /* ParachainStaking */, 84A2A60826B82B1C000C6C6C /* ValidatorOperationFactory */, @@ -9022,6 +10002,7 @@ isa = PBXGroup; children = ( 84216FD32827982800479375 /* SelectedRoundCollators.swift */, + 0C2F86852A72352400593C01 /* NominationPoolModel.swift */, ); path = Model; sourceTree = ""; @@ -9208,8 +10189,8 @@ children = ( 842A735D27DB2EC4006EE1EA /* OperationDetailsModel.swift */, 842A736127DB3032006EE1EA /* OperationExtrinsicModel.swift */, - 842A736327DB31A3006EE1EA /* OperationRewardModel.swift */, - 842A736527DB485E006EE1EA /* OperationSlashModel.swift */, + 842A736327DB31A3006EE1EA /* OperationRewardOrSlashModel.swift */, + 77AB55582AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift */, 842A736727DB4883006EE1EA /* OperationTransferModel.swift */, 84E0C51D29CA40DA000B65C8 /* OperationContractCallModel.swift */, ); @@ -9222,8 +10203,8 @@ 842A736A27DB7A2E006EE1EA /* OperationDetailsViewModel.swift */, 842A736C27DB7B5E006EE1EA /* OperationTransferViewModel.swift */, 842A736E27DB7E57006EE1EA /* OperationExtrinsicViewModel.swift */, - 842A737027DB7EF1006EE1EA /* OperationSlashViewModel.swift */, - 842A737227DB7F75006EE1EA /* OperationRewardViewModel.swift */, + 842A737227DB7F75006EE1EA /* OperationRewardOrSlashViewModel.swift */, + 77AB555A2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift */, 842A737427DB8338006EE1EA /* OperationDetailsViewModelFactory.swift */, 84E0C51F29CA413C000B65C8 /* OperationContractCallViewModel.swift */, ); @@ -9236,6 +10217,7 @@ 842A737B27DCC488006EE1EA /* OperationDetailsTransferView.swift */, 842A737D27DCD1A0006EE1EA /* OperationDetailsExtrinsicView.swift */, 842A737F27DCD427006EE1EA /* OperationDetailsRewardView.swift */, + 77AB555C2AA24BA90058814E /* OperationDetailsPoolRewardView.swift */, 84E0C52129CA421A000B65C8 /* OperationDetailsContractView.swift */, ); path = View; @@ -9386,7 +10368,7 @@ 842EBB2C289096D300B952D8 /* Common */ = { isa = PBXGroup; children = ( - 88CD320F28E2137200542F0D /* CrowdloanContributionId.swift */, + 88CD320F28E2137200542F0D /* ExternalBalanceContribution.swift */, A8FC21B4670E7B22B787357D /* WalletsListProtocols.swift */, 6F404EE82BC45BFE0F42E0A4 /* WalletsListWireframe.swift */, 0D6E67AD564867E121601F18 /* WalletsListPresenter.swift */, @@ -9463,7 +10445,6 @@ 8430AB1626023D2D005B1066 /* BaseStashNextState.swift */, 8430AACB2602249B005B1066 /* InitialStakingState.swift */, 8430AAD32602285B005B1066 /* StakingStateCommonData.swift */, - 8430AADB26022C58005B1066 /* NoStashState.swift */, 8430AAE826022F69005B1066 /* StashState.swift */, 8430AAF02602306A005B1066 /* BondedState.swift */, 8430AAF526023087005B1066 /* NominatorState.swift */, @@ -9661,7 +10642,11 @@ 8813702B29C13E7900829458 /* Web3NamesOperationFactoryTests.swift */, 8455F1992A1DEEAF003F072D /* SubqueryMultistakingTests.swift */, 844304662A28F7A500DE36DE /* MultistakingSyncTests.swift */, + 0CE150532A70EA2200B61CC1 /* NominationPoolsSyncTests.swift */, + 0C2F86922A72648D00593C01 /* ActiveNominationPoolsTests.swift */, + 0C2F86992A72948100593C01 /* NominationPoolsApyTests.swift */, 0C3205C12A868236002EB914 /* EvmGasPriceIntegrationTests.swift */, + 0C59E8EA2AA71C3E001E11F3 /* ExternalAssetBalanceIntegrationTests.swift */, ); path = novawalletIntegrationTests; sourceTree = ""; @@ -9669,6 +10654,7 @@ 8438E1DC24C18F11001BDB13 /* Types */ = { isa = PBXGroup; children = ( + 0C893E6B2A65629E00781503 /* NominationPools */, 8498534D2A1738EC00993977 /* Assets */, 885547E929C89104008782C1 /* Web3Names */, 84C1DBC129C27A5600F295A5 /* Paras */, @@ -9878,6 +10864,7 @@ 84403D8625E92A6300494FD4 /* Model */ = { isa = PBXGroup; children = ( + 0C6610292A73814900E44634 /* StakingSharedState */, 84A3B8A42836E05600DE2669 /* ParachainStaking */, 84A3B8A32836E00500DE2669 /* Relaychain */, 8425EB1A25EADD8600C307C9 /* StakingConstants.swift */, @@ -9885,6 +10872,9 @@ 841E5533282CD9A900C8438F /* StakingType.swift */, 847ABE3028532E1B00851218 /* ConsesusType.swift */, F4A12BF6260B61E900392C33 /* StakingManageOption.swift */, + 0C9525E22A7AAB2A00BD724D /* StakingTimeModel.swift */, + 0C9525E42A7AF82B00BD724D /* DirectStakingMinStakeBuilder.swift */, + 0CE629DF2AA9B70200E250BD /* CalculatedReward.swift */, ); path = Model; sourceTree = ""; @@ -10048,18 +11038,6 @@ path = Operation; sourceTree = ""; }; - 8449660825E15EBC00F2E9F5 /* ViewModel */ = { - isa = PBXGroup; - children = ( - 8449660925E15ECA00F2E9F5 /* RewardDestinationViewModel.swift */, - 84C2F27625E296CD0050A4AD /* RewardDestinationViewModelFactory.swift */, - 8463A71125E30C95003B8160 /* BalanceViewModelFactory.swift */, - 8463A71925E3116A003B8160 /* BalanceViewModel.swift */, - 84EA0B2925E579DF00AFB0DC /* AssetBalanceViewModel.swift */, - ); - path = ViewModel; - sourceTree = ""; - }; 844ADE7C28CB34F600EE29F7 /* AutomationTime */ = { isa = PBXGroup; children = ( @@ -10212,8 +11190,10 @@ 8455F1922A1DC64E003F072D /* Multistaking.swift */, 8455F19B2A1DF088003F072D /* ChainsStore+Multistaking.swift */, 8455F19D2A1E4956003F072D /* RelaychainMultistakingUpdateService.swift */, + 0C893E692A65591C00781503 /* PoolsMultistakingUpdateService.swift */, 846DA55A2A20A56D006CD6C1 /* OffchainMultistakingUpdateService.swift */, 8455F19F2A1E7265003F072D /* Multistaking+Relaychain.swift */, + 0CC4CCF32A67C9C400F63041 /* Multistaking+NominationPools.swift */, 849346AE2A1F7F7D00CB75B7 /* MultistakingServices.swift */, 8473B4712A1F9154003DE213 /* Multistaking+Model.swift */, 845FB6722A22285C0003BCA6 /* MultistakingSyncService.swift */, @@ -10256,6 +11236,10 @@ 8473B4792A2083B1003DE213 /* StakingDashboardItemMapper.swift */, 846DA5582A2098BE006CD6C1 /* StakingResolvedAccountMapper.swift */, 8443045F2A28946B00DE36DE /* StakingDashboardParachainMapper.swift */, + 0CB06E742A68139C00C7EC99 /* StakingDashboardNominationPoolMapper.swift */, + 0CC2E55D2A6AB2B7004092E7 /* StashItemMapper.swift */, + 0C59E8D22AA5FBE2001E11F3 /* PooledAssetBalanceMapper.swift */, + 0C59E8D42AA5FC96001E11F3 /* ExternalAssetBalanceMapper.swift */, ); path = EntityToModel; sourceTree = ""; @@ -10389,6 +11373,7 @@ 845BB8C725E45D0600E5FCDC /* Calls */ = { isa = PBXGroup; children = ( + 0C13D3052A7FB9170054BB6F /* NominationPools */, 843ADAC12A38FC4C003AE2B5 /* Staking */, 88B1C33D29B9C85200DCA101 /* BagList */, 845B080B2918D4E9005785D3 /* Democracy */, @@ -10461,6 +11446,7 @@ 8463A70725E2FFE2003B8160 /* DataProvider */ = { isa = PBXGroup; children = ( + 0C59E8DD2AA60D90001E11F3 /* ExternalAssetBalanceFactory */, 84EF8D3A288FD60E00265346 /* Base */, 84CD82BC263C3A64001A6F01 /* Subscription */, 84F98D9425E3E29B0040418E /* Repositories */, @@ -10483,9 +11469,9 @@ 848CCB432832EE9B00A1FD00 /* GeneralStorageSubscriptionFactory.swift */, 842643BC28785A940031B5B5 /* TuringStakingLocalSubscriptionFactory.swift */, 84EF8D38288FCE8100265346 /* WalletListLocalSubscriptionFactory.swift */, - 880855F928D0BAA2004255E7 /* CrowdloanContributionLocalSubscriptionFactory.swift */, 8412219A28F04EA600715C82 /* GovMetadataLocalSubscriptionFactory.swift */, 842E9E972A2A28A700759972 /* StakingDashboardProviderFactory.swift */, + 0CC2E5632A6E5C72004092E7 /* NPoolsLocalSubscriptionFactory.swift */, ); path = DataProvider; sourceTree = ""; @@ -10493,6 +11479,8 @@ 8463A70825E30000003B8160 /* Sources */ = { isa = PBXGroup; children = ( + 0C59E8D62AA6047A001E11F3 /* ExternalAssetBalance */, + 0C7C88622A94DFE000DD96A1 /* NominationPools */, 8412219C28F0512B00715C82 /* Governance */, 846A682A274693D400D1A47A /* Crowdloan */, 84EE6826264AD37B0026E6D3 /* Rewards */, @@ -10669,7 +11657,6 @@ 84452A5F25D037AE00F47EC5 /* ChainStorage+Decodable.swift */, 84452FA425D679F200F47EC5 /* CDRuntimeMetadataItem+CoreDataCodable.swift */, 8463A6F825E2F82E003B8160 /* CDSingleValue+CoreDataCodable.swift */, - 84786E1E25FA6C390089DFF7 /* CDStashItem+CoreDataCodable.swift */, ); path = Storage; sourceTree = ""; @@ -10914,6 +11901,8 @@ 843125CC299A71B20063745B /* StackButtonsCell.swift */, 84D184E92A04D9980060C1BD /* StackStatusCell.swift */, 844C3E682A07BC4500C4305F /* StackWalletAmountCell.swift */, + 0C7C886D2A962B0D00DD96A1 /* StackAddressCell.swift */, + 0CB261F62A9E2D8400287305 /* StackSwitchCell.swift */, ); path = StackTable; sourceTree = ""; @@ -11506,6 +12495,8 @@ 848DAF012822AA1100D56F55 /* ParachainStakingCollatorService.swift */, 848DAF032822B7FE00D56F55 /* ParachainStakingCollatorService+Fetch.swift */, 84216FD0282797EC00479375 /* ParachainStakingCollatorServiceProtocol.swift */, + 0C2F86812A7233DC00593C01 /* EraNominationPoolsService.swift */, + 0C2F86832A72343800593C01 /* EraNominationPoolsServiceProtocol.swift */, ); path = EraValidatorsService; sourceTree = ""; @@ -11540,7 +12531,7 @@ 8438E1D024BFAAD2001BDB13 /* novawalletIntegrationTests */, 849013A924A80984008F705E /* Products */, 2698CD398B0412EB85D620AB /* Pods */, - BFB3965A583F11EF3045E064 /* Frameworks */, + 6F5BD6E623DF3F7DFEDC3EFA /* Frameworks */, ); sourceTree = ""; }; @@ -11772,6 +12763,7 @@ 849A4EF3279A7AC600AB6709 /* AssetType.swift */, 849A4EF5279A7AEF00AB6709 /* StateminAssetExtras.swift */, 849A4EF7279ABBDD00AB6709 /* AssetBalance.swift */, + 0C59E8C82AA5C7EC001E11F3 /* ExternalAssetBalance.swift */, 84884B5E27A1336500FAC549 /* OrmlAssetExtras.swift */, 84EBA4EF27AD26A5000AEEAD /* AssetBalanceId.swift */, 8499FE7427BE490700712589 /* UniquesAccountKey.swift */, @@ -11795,6 +12787,7 @@ 0C17BD9A2A43025E004AF9E7 /* Pagination.swift */, 8455F1A32A1F606B003F072D /* OnchainStorage.swift */, 0C3205EB2A8A122D002EB914 /* FeeOutputModel.swift */, + 0C59E8D02AA5FAC5001E11F3 /* PooledAssetBalance.swift */, ); path = Model; sourceTree = ""; @@ -11899,6 +12892,7 @@ 8495C6EA29BA24350014293F /* MessageSheetPresentable+ErrorPresentable.swift */, 8846F71F29D56A0700B8B776 /* Web3NameAddressListPresentable.swift */, 844C3E762A09228200C4305F /* WalletChoosePresentable.swift */, + 0CC2E5692A6E6EBB004092E7 /* LocalStorageProviderObserving.swift */, ); path = Protocols; sourceTree = ""; @@ -11975,6 +12969,7 @@ 842D8B792A416A9A00660005 /* UIView+Shimmering.swift */, 849F1AFF2A41A5B3003A2CF7 /* ScrullableView+Animation.swift */, 0C463FC72A58126A003E71C9 /* UIView+MotionEffect.swift */, + 77CC82A42A984EDA002D022F /* UINavigaionController+Pop.swift */, ); path = UIKit; sourceTree = ""; @@ -12036,6 +13031,8 @@ 844C3E722A09184300C4305F /* BalancesStore+Default.swift */, 88F19DDD28D8D0A100F6E459 /* Either.swift */, 0C8A25582A553A6C0072882A /* KeyboardAppearanceState.swift */, + 0C9525E62A7AFA2C00BD724D /* ValueResolver.swift */, + 0CF193D42A861925003F12F6 /* PredefinedTimeShortcut.swift */, ); path = Helpers; sourceTree = ""; @@ -12080,6 +13077,7 @@ 0C1BE1A12A46F93B0010933C /* BigUInt+Scientific.swift */, 0CE550B22A49658700F0A7AC /* StakingDuration+Localizable.swift */, 77E0DC9D2A6940C400D03724 /* Calendar+Helpers.swift */, + 0C59E8DB2AA60C3B001E11F3 /* NSPredicate+ExternalAssetBalance.swift */, ); path = Foundation; sourceTree = ""; @@ -12159,6 +13157,7 @@ 8490149D24AA7F9A008F705E /* View */ = { isa = PBXGroup; children = ( + 0C79C8932A7BDED700B171E3 /* TitleHorizontalMultivalueView */, 84F96FA32A136E0400EB70E2 /* GladingView */, 84B24FA12A2F1C6C00F9BF59 /* CollectionView */, 84D184E62A04D5880060C1BD /* ScrollableContainerView */, @@ -12216,7 +13215,6 @@ 847F2D5827AB201200AFD476 /* GradientIconView.swift */, 84F1CB3D27CF4F5A0095D523 /* BorderedLabelView.swift */, 8456C08327CFA4A7001282DE /* ImagePlaceholderView.swift */, - 84D1ABDF27E1CB870073C631 /* TitleHorizontalMultiValueView.swift */, 8466781227EC5446007935D3 /* MultilineBalanceView.swift */, 8487580627EDEB9600495306 /* BorderedIconLabelView.swift */, 8487580827EDEDB200495306 /* RawChainView.swift */, @@ -12711,6 +13709,8 @@ 84E25BEF27E8EFB500290BF1 /* SubqueryAccumulateReward.swift */, 847EA1D32A1CA43A00F1CBD8 /* SubqueryNodes.swift */, 847EA1D52A1CA47500F1CBD8 /* SubqueryMultistaking.swift */, + 0C7C88652A95030800DD96A1 /* SubqueryStakingType.swift */, + 0C12A2462AA1D96000C7FA49 /* SubqueryMultistakingTypeFactory.swift */, ); path = Models; sourceTree = ""; @@ -12868,9 +13868,10 @@ 84BAB60F2642C286007782D0 /* SelectedRebondVariant.swift */, F418E886264D308700699085 /* StakingAlert.swift */, 849E689426AF388500E0E7BE /* ElectedValidatorInfo+Selected.swift */, - 8493D3E52705994200157009 /* StakingSharedState.swift */, - 847ABE322853333A00851218 /* StakingSharedState+Duration.swift */, 8436C7A029ACDCA50024B409 /* VotersStakingInfo.swift */, + 0C79C8912A7BD9BB00B171E3 /* SelectedStakingOption.swift */, + 0C543E962AAB1B350035F45F /* ElectedAndPrefValidators.swift */, + 0CC6C8D72AAB401200AD8D9B /* CustomValidatorsFullList.swift */, ); path = Relaychain; sourceTree = ""; @@ -12878,7 +13879,6 @@ 84A3B8A42836E05600DE2669 /* ParachainStaking */ = { isa = PBXGroup; children = ( - 841E5544282D793900C8438F /* ParachainStakingSharedState.swift */, 841E555C282E72F500C8438F /* ParachainStakingNetworkInfo.swift */, 84A3B8A52836E08600DE2669 /* CollatorSelectionInfo.swift */, 846FB02B28C74F9700CA5444 /* ParaStkYieldBoostState.swift */, @@ -13705,6 +14705,8 @@ 84D1111226B932C40016D962 /* ChainNodeModel.swift */, 849F144A29444C3E00D9F9BA /* AssetModel+Id.swift */, 8419F053295ED55000A14E05 /* LocalChainExternalApi.swift */, + 0C9525EA2A7B7F5000BD724D /* ChainModel+Additional.swift */, + 0C79C8982A7BE46A00B171E3 /* AssetModel+Staking.swift */, ); path = LocalChain; sourceTree = ""; @@ -13741,7 +14743,6 @@ 8406B5AA26FBD9EB00635B61 /* AccountInfoUpdatingService.swift */, 849A4EF1279A787200AB6709 /* AssetsUpdatingService.swift */, 845C40782702571200BFA50B /* StakingRemoteSubscriptionService.swift */, - 84786E0F25FA20D30089DFF7 /* StakingAccountResolver.swift */, 84786E2325FBA2A50089DFF7 /* StakingAccountSubscription.swift */, 845C407A27027AA000BFA50B /* SubscriptionStorageKeys.swift */, 845C407C2702812E00BFA50B /* StakingAccountUpdatingService.swift */, @@ -13756,6 +14757,9 @@ 849E07F3284A04F400DE0440 /* ParaStkAccountSubscribeHandlingFactory.swift */, 842643BA2878572D0031B5B5 /* TuringStakingRemoteSubscriptionService.swift */, 884048D328C723F00085FFA6 /* OrmlTokenSubscriptionHandlingFactory.swift */, + 0CC2E55F2A6E44E7004092E7 /* NominationPoolsRemoteSubscriptionService.swift */, + 0CC2E5612A6E5C43004092E7 /* NominationPoolsAccountUpdatingService.swift */, + 0CE1504F2A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift */, ); path = Substrate; sourceTree = ""; @@ -13896,14 +14900,6 @@ path = AccountDetailsSelection; sourceTree = ""; }; - 84C2F27B25E2971A0050A4AD /* Model */ = { - isa = PBXGroup; - children = ( - 84C2F27C25E297350050A4AD /* CalculatedReward.swift */, - ); - path = Model; - sourceTree = ""; - }; 84C3420528314D5A00156569 /* ParachainStaking */ = { isa = PBXGroup; children = ( @@ -13982,6 +14978,7 @@ 84C7DA5325EE2DF000F8C318 /* StakingErrorPresentable.swift */, 84350AD3284580F50031EF24 /* StakingTotalStakePresentable.swift */, 84350AD52845836C0031EF24 /* IdentityPresentable.swift */, + 77171CA72A98BBA10032B387 /* NominationPoolErrorPresentable.swift */, ); path = Protocols; sourceTree = ""; @@ -14068,13 +15065,16 @@ 848CCB472832EF4400A1FD00 /* GeneralLocalStorageHandler.swift */, 84EF8D3D288FDA2100265346 /* WalletListLocalStorageSubscriber.swift */, 84EF8D3F288FDA7700265346 /* WalletListLocalStorageSubscriptionHandler.swift */, - 880855FB28D0C3DF004255E7 /* CrowdloansLocalStorageSubscriber.swift */, 8412219F28F051EE00715C82 /* GovMetadataLocalStorageSubscriber.swift */, 841221A128F0520300715C82 /* GovMetadataLocalStorageHandler.swift */, 847A258D29B5D25E0054F90C /* GovJsonLocalStorageSubscriber.swift */, 847A258F29B5D2710054F90C /* GovJsonLocalStorageHandler.swift */, 842E9E932A2A277D00759972 /* StakingDashboardLocalStorageSubscriber.swift */, 842E9E952A2A279800759972 /* StakingDashboardLocalStorageHandler.swift */, + 0CC2E5652A6E64EC004092E7 /* NPoolsLocalStorageSubscriber.swift */, + 0CC2E5672A6E64FD004092E7 /* NPoolsLocalStorageHandler.swift */, + 0C59E8E42AA6191E001E11F3 /* ExternalAssetBalanceSubscriber.swift */, + 0C59E8E62AA61933001E11F3 /* ExternalAssetBalanceSubscriptionHandler.swift */, ); path = Subscription; sourceTree = ""; @@ -14473,6 +15473,9 @@ 848077D12837CAE5003B7C79 /* ParachainStakingErrorPresentable.swift */, 8460E717284CF124002896E9 /* ParachainStakingValidatorFactoryProtocol.swift */, 8460E719284D3AF5002896E9 /* ParaStkMinDelegationParams.swift */, + 0C13D3102A80CA9A0054BB6F /* StakingDataValidatorFactory+Plank.swift */, + 77171CA92A98BC420032B387 /* NominationPoolDataValidatorFactory.swift */, + 0C12A2482AA35AE300C7FA49 /* RelaychainStakingValidatorFacade.swift */, ); path = Validation; sourceTree = ""; @@ -14714,6 +15717,7 @@ 8452585227ABCA07004F9082 /* HideZeroBalancesChanged.swift */, 84C342122831BE8200156569 /* BlockTimeChanged.swift */, 84C2063D28D1E8CE006D0D52 /* AssetBalanceChanged.swift */, + 0C2F868A2A725C3C00593C01 /* EraNominationPoolsChanged.swift */, ); path = Events; sourceTree = ""; @@ -14892,6 +15896,7 @@ 84F43B8725DE9F8500AEDA56 /* Staking */ = { isa = PBXGroup; children = ( + 0CB261D52A97A4D300287305 /* NominationPools */, B34182C356E2DACDF66A71FC /* Dashboard */, 84D41186283517A1001D6E06 /* Parachain */, 8493D3E727059B2B00157009 /* Services */, @@ -14906,7 +15911,6 @@ 84CFF1D726526FBC00DB7CF7 /* StakingBondMoreConfirmation */, AEE5FB0D26443859002B8FDC /* StakingRewardDestinationSetup */, 8472C5A2265CF9C400E2481B /* StakingRewardDestConfirm */, - 68DEED3BF634220D4BF7A9C8 /* StakingAmount */, B7B0681CCE8F8F0127E8676C /* StakingMain */, C9F3889FC7620D7F436E38ED /* StakingPayoutConfirmation */, 8401AEB42642A71D000B03E3 /* StakingRebondConfirmation */, @@ -14921,6 +15925,12 @@ 5E8CD0BF639BD87FB423B57B /* StakingRewardFilters */, 5A992016B7B8EE42C6CC61F7 /* StakingRebagConfirm */, 441CFEEE3C55585F7764C9EE /* StakingMoreOptions */, + CCD822B8B9ED8FF81C834DD5 /* StartStakingInfo */, + 7066B343B912F72345D541F2 /* StakingSetupAmount */, + 17432B4B5D8D9DC5C22CA238 /* StakingType */, + ADE9FF1DB92333FA89C7F683 /* StakingSelectPool */, + 92CDAD21CEED554306CAF5D8 /* StartStakingConfirm */, + F3286B3F0CD8E7D358AC93EA /* NominationPoolSearch */, ); path = Staking; sourceTree = ""; @@ -15203,7 +16213,6 @@ 880855EF28D099F2004255E7 /* CrowdloanOnChainSyncService.swift */, 880855F128D09A0B004255E7 /* CrowdloanContributionData.swift */, 880855F328D09A26004255E7 /* RemoteCrowdloanContribution.swift */, - 880855F528D09A3C004255E7 /* CrowdloanContributionStreamableSource.swift */, 88A0C52028D49A090083A524 /* CrowdloanOffChainSyncService.swift */, ); path = CrowdloanService; @@ -15686,8 +16695,7 @@ 84ABB3362A16164100B5E95A /* HistoryRewardContext.swift */, 88AC5AD92948A8CC0056DD40 /* TransactionSectionModel.swift */, 88AC5ADD2948CB540056DD40 /* TransactionHistoryViewModelFactory.swift */, - 88FB7DD62951B1AF00784E08 /* WalletHistoryFilter+CallCodingPath.swift */, - 0CCE25202A44306200286709 /* TransactionHistoryPhishingFilter.swift */, + 0C59E8EC2AA75C84001E11F3 /* HistoryPoolRewardContext.swift */, ); path = Model; sourceTree = ""; @@ -15808,6 +16816,23 @@ path = ParaStkStakeConfirm; sourceTree = ""; }; + 92CDAD21CEED554306CAF5D8 /* StartStakingConfirm */ = { + isa = PBXGroup; + children = ( + 0C13D3222A823D6D0054BB6F /* ExtrinsicProxy */, + 0C13D31B2A8227850054BB6F /* Presenter */, + A28FAB72B50D8C736AF67A39 /* StartStakingConfirmProtocols.swift */, + 2876FA98B3E8F7EBCF5DEED0 /* StartStakingConfirmWireframe.swift */, + 71040ECB206855280257D4F2 /* StartStakingConfirmPresenter.swift */, + 2AA546FF04D7C9F741125567 /* StartStakingConfirmInteractor.swift */, + FB617C1637B277B83E4C4914 /* StartStakingConfirmViewController.swift */, + DB9AB4A7C57057ECFA1DA03E /* StartStakingConfirmViewLayout.swift */, + 6589230FD54838BFCF6E3FD8 /* StartStakingConfirmViewFactory.swift */, + 0C13D3192A8222D20054BB6F /* StartStakingConfirmInteractorError.swift */, + ); + path = StartStakingConfirm; + sourceTree = ""; + }; 9482C532739CF72E0D6C96E3 /* DAppAuthConfirm */ = { isa = PBXGroup; children = ( @@ -15835,6 +16860,7 @@ 96566FFD869F45400C4963A1 /* OperationDetails */ = { isa = PBXGroup; children = ( + 0C59E8EE2AA76331001E11F3 /* OperationDataProviders */, 842A737A27DCC444006EE1EA /* View */, 842A736927DB7A17006EE1EA /* ViewModel */, 842A735C27DB2EB3006EE1EA /* Model */, @@ -16025,6 +17051,21 @@ path = AddToken; sourceTree = ""; }; + ADE9FF1DB92333FA89C7F683 /* StakingSelectPool */ = { + isa = PBXGroup; + children = ( + 770F57892A8A48F7005FD7C1 /* Model */, + 77F033A02A84E005006BC67E /* View */, + 2A5A2FE372A88ECF23B620D2 /* StakingSelectPoolProtocols.swift */, + 6654DEA68D7ED47AE8E52206 /* StakingSelectPoolWireframe.swift */, + 060D52F9BFFB646BF9FCC968 /* StakingSelectPoolPresenter.swift */, + 86F7ACFB151C31B3A5044DCD /* StakingSelectPoolInteractor.swift */, + E7F38FDB907F69A26B93B4E6 /* StakingSelectPoolViewController.swift */, + 4E553E3D45A7A759C917A4B2 /* StakingSelectPoolViewFactory.swift */, + ); + path = StakingSelectPool; + sourceTree = ""; + }; ADEC075C60AA6D00785D2BDF /* AssetList */ = { isa = PBXGroup; children = ( @@ -16074,6 +17115,7 @@ 841E5537282CF3F400C8438F /* StakingMainViewModelFactory.swift */, 84C342082831645800156569 /* EraCountdownDisplay.swift */, 848CCB4D2833CC4D00A1FD00 /* StakingMainStaticViewModel.swift */, + 0C7C886B2A9622F800DD96A1 /* StakingSelectedEntityViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -16217,6 +17259,7 @@ isa = PBXGroup; children = ( AEA0C8C02681180900F9666F /* InitBondingCustomValidatorListWireframe.swift */, + 772B1C7C2A93837800A19061 /* StartStakingCustomValidatorListWireframe.swift */, AEA0C8C22681186500F9666F /* ChangeTargetsCustomValidatorListWireframe.swift */, 96D540DFC00C25D8F73CFDC3 /* CustomValidatorListWireframe.swift */, ); @@ -16229,6 +17272,7 @@ AEA0C8A5267B6B2600F9666F /* SelectedValidatorListWireframe.swift */, AEA0C8C5268131C500F9666F /* InitiatedBondingSelectedValidatorsListWireframe.swift */, AEA0C8C7268131DD00F9666F /* ChangeTargetsSelectedValidatorsListWireframe.swift */, + 77CC82A22A984BC3002D022F /* StartStakingSelectedValidatorsListWireframe.swift */, ); path = Wireframe; sourceTree = ""; @@ -16286,6 +17330,7 @@ AEC8C485266A5634005904F4 /* SelectValidatorsFlow */ = { isa = PBXGroup; children = ( + 0C77B55D2A83713D00B5AE08 /* StaticValidatorList */, AE6F7FD62685E812002BBC3E /* ValidatorListFilter */, AEA2C1AF2681E9690069492E /* ValidatorSearch */, AEA0C8A0267B6AE600F9666F /* SelectedValidatorList */, @@ -16405,6 +17450,7 @@ AEF7404C25E6DC6600407D41 /* RewardCalculatorService */ = { isa = PBXGroup; children = ( + 0C2F86942A72804D00593C01 /* NominationPools */, 84FEADF028783EC9001DFC26 /* RelayChain */, 84FEADEF28783EB1001DFC26 /* Parachain */, ); @@ -16426,6 +17472,8 @@ 8489A6D927FDC49D0040C066 /* StakingUnbondingsView.swift */, 8489A6DD27FEA1930040C066 /* StakingUnbondingItemView.swift */, 7731E9C32A14DA3F0085B5FF /* BorderedActionControlView.swift */, + 0C7C88672A95563100DD96A1 /* StakingClaimableRewardView.swift */, + 0C7C88692A95591900DD96A1 /* StakingTotalRewardView.swift */, ); path = View; sourceTree = ""; @@ -16497,6 +17545,7 @@ B7B0681CCE8F8F0127E8676C /* StakingMain */ = { isa = PBXGroup; children = ( + 0C9C642E2A8D675B004DC078 /* NominationPools */, 841E5543282D78ED00C8438F /* Parachain */, 8473F4B0282BD564007CC55A /* Relaychain */, AEFC6D602600A754000BD310 /* View */, @@ -16563,16 +17612,6 @@ path = AccountImport; sourceTree = ""; }; - BFB3965A583F11EF3045E064 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 1313BBDD3A28AC9786B5B00E /* Pods_novawalletAll_novawallet.framework */, - 86F9063B2DF46E7B65B5248E /* Pods_novawalletAll_novawalletIntegrationTests.framework */, - D79EAD799CBB1ABB9541A232 /* Pods_novawalletTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; C67688CBF31B183F11CE7226 /* ChangeWatchOnly */ = { isa = PBXGroup; children = ( @@ -16587,6 +17626,20 @@ path = ChangeWatchOnly; sourceTree = ""; }; + C74D63D387B28857B9B02289 /* Confirm */ = { + isa = PBXGroup; + children = ( + 793356FB65FB73CE7097C6F1 /* NPoolsUnstakeConfirmProtocols.swift */, + 397F057FD5B16A58E5F30F07 /* NPoolsUnstakeConfirmWireframe.swift */, + 9D9131BF410C62A93646CA0A /* NPoolsUnstakeConfirmPresenter.swift */, + 080DF5C2C6DEC79D8324F084 /* NPoolsUnstakeConfirmInteractor.swift */, + D790172C70C98CAE984F7183 /* NPoolsUnstakeConfirmViewController.swift */, + 12C822BAC40271396E36ACBB /* NPoolsUnstakeConfirmViewLayout.swift */, + AAC5294102607A80F62F9848 /* NPoolsUnstakeConfirmViewFactory.swift */, + ); + path = Confirm; + sourceTree = ""; + }; C7AEDB8341B78EC46F6F98DC /* UsernameSetup */ = { isa = PBXGroup; children = ( @@ -16604,6 +17657,7 @@ C9850B4B70AEFEABB96269FF /* TransactionHistory */ = { isa = PBXGroup; children = ( + 0C962F842AA859DE00C0B551 /* LocalFilter */, 35542AFCBEC3F0BD298F5030 /* HistoryFilter */, 84981EE029D31A4600948306 /* Service */, 88AC5AD62948A8980056DD40 /* View */, @@ -16634,6 +17688,24 @@ path = StakingPayoutConfirmation; sourceTree = ""; }; + CCD822B8B9ED8FF81C834DD5 /* StartStakingInfo */ = { + isa = PBXGroup; + children = ( + 7738FB5C2A4C5C3600797439 /* RelaychainStaking */, + 7738FB5B2A4C5C2000797439 /* ParachainStaking */, + 77F189452A49BD5E00E8B933 /* Model */, + 77F1893C2A4996F300E8B933 /* View */, + C92B3D5B314FB3EAE65FA471 /* StartStakingInfoProtocols.swift */, + A2BC4EFADD593325FF122765 /* StartStakingInfoBaseInteractor.swift */, + 0D65686560E2E6C18A5C34CB /* StartStakingInfoWireframe.swift */, + 0E4D3F275296975CA39280D5 /* StartStakingInfoBasePresenter.swift */, + 6F5788D702D2B2203949329E /* StartStakingInfoViewController.swift */, + 694E15BA7E0C59E46D0A6612 /* StartStakingInfoViewFactory.swift */, + 0CF193D62A861D7E003F12F6 /* StartStakingInfoConstants.swift */, + ); + path = StartStakingInfo; + sourceTree = ""; + }; CEA238CBBD1DB61D399A69C0 /* NftList */ = { isa = PBXGroup; children = ( @@ -16681,6 +17753,16 @@ path = DelegateSetup; sourceTree = ""; }; + D1AABAFE54CA7C8321264E64 /* BondMore */ = { + isa = PBXGroup; + children = ( + 77F9FB042A9D96A700820625 /* Base */, + 77F9FB052A9D96DF00820625 /* Setup */, + 777CA80B2AA0B8DD00E99751 /* Confirm */, + ); + path = BondMore; + sourceTree = ""; + }; D3537A6D35CC43956A18325B /* ParaStkSelectCollators */ = { isa = PBXGroup; children = ( @@ -16791,6 +17873,20 @@ path = StakingRedeem; sourceTree = ""; }; + DF1349A0832B89817308F2A1 /* Setup */ = { + isa = PBXGroup; + children = ( + D9E599C8CB87A955659DAEBF /* NPoolsUnstakeSetupProtocols.swift */, + 1FA6F8EC6245BA34F26AE276 /* NPoolsUnstakeSetupWireframe.swift */, + 2899566C339BA9562BE61F4F /* NPoolsUnstakeSetupPresenter.swift */, + 01D9C4A550C6DDF85491B8A0 /* NPoolsUnstakeSetupInteractor.swift */, + 2864B5E1A5AFC6A8C1B4B9E5 /* NPoolsUnstakeSetupViewController.swift */, + BE86742207CDAD889A01B40A /* NPoolsUnstakeSetupViewLayout.swift */, + 3246115B53EFE9461CD2F68B /* NPoolsUnstakeSetupViewFactory.swift */, + ); + path = Setup; + sourceTree = ""; + }; E01524612035A176DA0793E7 /* CrowdloanContributionSetup */ = { isa = PBXGroup; children = ( @@ -16958,6 +18054,22 @@ path = Purchase; sourceTree = ""; }; + F3286B3F0CD8E7D358AC93EA /* NominationPoolSearch */ = { + isa = PBXGroup; + children = ( + A31B8F292DA050D1D19B9F5F /* NominationPoolSearchProtocols.swift */, + 06D9A7B84C1B985D33E72D84 /* NominationPoolSearchWireframe.swift */, + 1090AF9D9F18846E5A73F73C /* NominationPoolSearchPresenter.swift */, + 44AA632DE49B746BC38B959F /* NominationPoolSearchInteractor.swift */, + 834314A1286CB91F8BB43F06 /* NominationPoolSearchViewController.swift */, + D686A91FF92C89FE8937EF5A /* NominationPoolSearchViewLayout.swift */, + 15AC3897EC736F5096949BBC /* NominationPoolSearchViewFactory.swift */, + 77895C9E2A8F5D40006870FB /* NominationPoolSearchManager.swift */, + 77895CA02A8F7360006870FB /* NominationPoolSearchOperationFactory.swift */, + ); + path = NominationPoolSearch; + sourceTree = ""; + }; F3E50DB617DFDE30FE6FA185 /* ParaStkUnstake */ = { isa = PBXGroup; children = ( @@ -17229,6 +18341,21 @@ path = ContributionConfirm; sourceTree = ""; }; + F4CFBAC4468FAD5728719A7D /* ClaimRewards */ = { + isa = PBXGroup; + children = ( + 0CB261ED2A9E102700287305 /* Model */, + E12E4AA5C56575FD3ABA7693 /* NPoolsClaimRewardsProtocols.swift */, + CF389223A781CA2088C7A4DD /* NPoolsClaimRewardsWireframe.swift */, + B213965350582497E2F86E26 /* NPoolsClaimRewardsPresenter.swift */, + F5F1F933F624B01855AA3BA5 /* NPoolsClaimRewardsInteractor.swift */, + 50AFCEE78CDFC4FE3238E158 /* NPoolsClaimRewardsViewController.swift */, + 6A7302440137F083F7AEC64E /* NPoolsClaimRewardsViewLayout.swift */, + 30E42BA40ACDEA2914DE6435 /* NPoolsClaimRewardsViewFactory.swift */, + ); + path = ClaimRewards; + sourceTree = ""; + }; F4DCAE4526207ECD00CCA6BF /* PayoutRewardsService */ = { isa = PBXGroup; children = ( @@ -17275,11 +18402,14 @@ F4F2297A260DC03400ACFDB8 /* ViewModel */ = { isa = PBXGroup; children = ( + 0CE629DB2AA9B6BE00E250BD /* RewardDestinationViewModel.swift */, + 0CE629DC2AA9B6BE00E250BD /* RewardDestinationViewModelFactory.swift */, F4F2297B260DC05600ACFDB8 /* StakingRewardTokenUsdViewModel.swift */, F4F22986260DC4F300ACFDB8 /* StakingRewardDetailsSimpleLabelViewModel.swift */, F4617414260E23E900E8FA3D /* StakingRewardStatusViewModel.swift */, 84222985283621F2009F0086 /* StakingRewardInfoViewModel.swift */, 8436B6DB2848998200F24360 /* StakingAccountDetailsViewModelFactory.swift */, + 0C13D3172A8216A10054BB6F /* NominationPoolsIconFactory.swift */, ); path = ViewModel; sourceTree = ""; @@ -17362,11 +18492,11 @@ isa = PBXNativeTarget; buildConfigurationList = 8438E1D624BFAAD2001BDB13 /* Build configuration list for PBXNativeTarget "novawalletIntegrationTests" */; buildPhases = ( - 27F085415B4943CAE09FE560 /* [CP] Check Pods Manifest.lock */, + B296F5F654EA299699479AE4 /* [CP] Check Pods Manifest.lock */, 8438E1CB24BFAAD2001BDB13 /* Sources */, 8438E1CC24BFAAD2001BDB13 /* Frameworks */, 8438E1CD24BFAAD2001BDB13 /* Resources */, - 167BC16B67C4EB9E9B02F201 /* [CP] Embed Pods Frameworks */, + 9AB453D5E48F4E4331292FC9 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -17382,7 +18512,7 @@ isa = PBXNativeTarget; buildConfigurationList = 849013C724A80986008F705E /* Build configuration list for PBXNativeTarget "novawallet" */; buildPhases = ( - BB864FD3004FEB055054B4A1 /* [CP] Check Pods Manifest.lock */, + EEA0C402E0BBD829D8FC447E /* [CP] Check Pods Manifest.lock */, AE2060202636DA5900357578 /* Inject keys */, F472A8D6261758DE003C58BC /* SwiftFormat */, 849013CD24A92260008F705E /* Swiftlint */, @@ -17390,15 +18520,13 @@ 849013A424A80984008F705E /* Sources */, 849013A524A80984008F705E /* Frameworks */, 849013A624A80984008F705E /* Resources */, - 814F6F81A24CB16EC99A1485 /* [CP] Embed Pods Frameworks */, + C8525C8798D5D5AB9F0FAE36 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = novawallet; - packageProductDependencies = ( - ); productName = fearless; productReference = 849013A824A80984008F705E /* novawallet.app */; productType = "com.apple.product-type.application"; @@ -17407,13 +18535,13 @@ isa = PBXNativeTarget; buildConfigurationList = 849013CA24A80986008F705E /* Build configuration list for PBXNativeTarget "novawalletTests" */; buildPhases = ( - 37CB4B90B98CFA452A29AA46 /* [CP] Check Pods Manifest.lock */, + 781EE62034766AF1971E1B00 /* [CP] Check Pods Manifest.lock */, 842D1E8824D207C700C30A7A /* Modules Mock */, 842D1E8924D207D900C30A7A /* Common Mock */, 849013BA24A80986008F705E /* Sources */, 849013BB24A80986008F705E /* Frameworks */, 849013BC24A80986008F705E /* Resources */, - 63EE755C93B651D0D5388B14 /* [CP] Embed Pods Frameworks */, + FA9ADF1F9B8A1BA6950A0416 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -17458,8 +18586,6 @@ ru, ); mainGroup = 8490139F24A80984008F705E; - packageReferences = ( - ); productRefGroup = 849013A924A80984008F705E /* Products */; projectDirPath = ""; projectRoot = ""; @@ -17551,24 +18677,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 167BC16B67C4EB9E9B02F201 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 27F085415B4943CAE09FE560 /* [CP] Check Pods Manifest.lock */ = { + 781EE62034766AF1971E1B00 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -17583,14 +18692,14 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-novawalletAll-novawalletIntegrationTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-novawalletTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 37CB4B90B98CFA452A29AA46 /* [CP] Check Pods Manifest.lock */ = { + 842D1E8824D207C700C30A7A /* Modules Mock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -17598,55 +18707,53 @@ inputFileListPaths = ( ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Check Pods Manifest.lock"; + name = "Modules Mock"; outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-novawalletTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "#! /bin/sh\n# Define output file. Change \"$PROJECT_DIR/${PROJECT_NAME}Tests\" to your test's root source folder, if it's not the default name.\nOUTPUT_FILE=\"${PROJECT_NAME}Tests/Mocks/ModuleMocks.swift\"\necho \"Generated Mocks File = $OUTPUT_FILE\"\n\n# Define input directory. Change \"${PROJECT_DIR}/${PROJECT_NAME}\" to your project's root source folder, if it's not the default name.\nINPUT_DIR=\"${PROJECT_NAME}\"\necho \"Mocks Input Directory = $INPUT_DIR\"\n\n# Generate mock files, include as many input files as you'd like to create mocks for.\n\"Pods/Cuckoo/run\" generate --no-header --testable \"${PROJECT_NAME}\" \\\n--exclude \"RootPresenterFactoryProtocol, UsernameSetupViewFactoryProtocol, OnboardingMainViewFactoryProtocol, AccountCreateViewFactoryProtocol, AccountImportViewFactoryProtocol, AccountConfirmViewFactoryProtocol, PinViewFactoryProtocol, ProfileViewFactoryProtocol, AccountManagementViewFactoryProtocol, AccountInfoViewFactoryProtocol, NetworkManagementViewFactoryProtocol, NetworkInfoViewFactoryProtocol, AddConnectionViewFactoryProtocol, AccountExportPasswordViewFactoryProtocol, ExportRestoreJsonViewFactoryProtocol, ExportMnemonicViewFactoryProtocol, StakingMainViewFactoryProtocol, SelectValidatorsStartViewFactoryProtocol, SelectValidatorsConfirmViewFactoryProtocol, RecommendedValidatorListViewFactoryProtocol, SelectedValidatorListViewFactoryProtocol, CustomValidatorListViewFactoryProtocol, ValidatorInfoViewFactoryProtocol, WalletHistoryFilterViewFactoryProtocol, StakingPayoutConfirmationViewFactoryProtocol, StakingRewardDetailsViewFactoryProtocol, StakingRewardPayoutsViewFactoryProtocol, StakingPayoutConfirmViewModelFactoryProtocol, YourValidatorListViewFactoryProtocol, StakingUnbondSetupViewFactoryProtocol, StakingUnbondConfirmViewFactoryProtocol, StakingRedeemViewFactoryProtocol, StakingBondMoreViewFactoryProtocol, StakingRebondSetupViewFactoryProtocol, ValidatorListFilterViewFactoryProtocol, ValidatorSearchViewFactoryProtocol\" \\\n--output \"${OUTPUT_FILE}\" \\\n\"$INPUT_DIR/../Pods/SoraFoundation/SoraFoundation/Classes/Localization/Localizable.swift\" \\\n\"$INPUT_DIR/Common/Protocols/LoadableViewProtocol.swift\" \\\n\"$INPUT_DIR/Common/Protocols/ControllerBackedProtocol.swift\" \\\n\"$INPUT_DIR/Common/Protocols/WebPresentable.swift\" \\\n\"$INPUT_DIR/Common/Protocols/AlertPresentable.swift\" \\\n\"$INPUT_DIR/Common/Protocols/ModalAlertPresenting.swift\" \\\n\"$INPUT_DIR/Common/Protocols/SharingPresentable.swift\" \\\n\"$INPUT_DIR/Common/Protocols/AccountSelectionPresentable.swift\" \\\n\"$INPUT_DIR/Common/Protocols/AuthorizationPresentable.swift\" \\\n\"$INPUT_DIR/Common/ViewController/SearchController/TableSearchProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Root/RootProtocol.swift\" \\\n\"$INPUT_DIR/Modules/UsernameSetup/UsernameSetupProtocols.swift\" \\\n\"$INPUT_DIR/Modules/OnbordingMain/OnboardingMainProtocol.swift\" \\\n\"$INPUT_DIR/Modules/AccountCreate/AccountCreateProtocols.swift\" \\\n\"$INPUT_DIR/Modules/AccountImport/AccountImportProtocols.swift\" \\\n\"$INPUT_DIR/Modules/AccountConfirm/AccountConfirmProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Pincode/PinSetup/PinSetupProtocol.swift\" \\\n\"$INPUT_DIR/Modules/Settings/SettingsProtocols.swift\" \\\n\"$INPUT_DIR/Modules/AccountManagement/AccountManagementProtocols.swift\" \\\n\"$INPUT_DIR/Modules/AccountInfo/AccountInfoProtocols.swift\" \\\n\"$INPUT_DIR/Modules/NetworkManagement/NetworkManagementProtocols.swift\" \\\n\"$INPUT_DIR/Modules/NetworkInfo/NetworkInfoProtocols.swift\" \\\n\"$INPUT_DIR/Modules/AddConnection/AddConnectionProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Export/AccountExportPassword/AccountExportPasswordProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Export/ExportGenericView/ExportGenericProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Export/ExportMnemonic/ExportMnemonicProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Export/ExportRestoreJson/ExportRestoreJsonProtocols.swift\" \\\n\"$INPUT_DIR/Modules/TransactionHistory/HistoryFilter/WalletHistoryFilterProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/Operations/ValidatorOperationFactory/ValidatorOperationFactoryProtocol.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingMain/StakingMainProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingMain/Relaychain/StakingRelaychainProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/Operations/NetworkStakingInfoOperationFactory.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingRewardDetails/StakingRewardDetailsProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingRewardPayouts/StakingRewardPayoutsProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingBalance/StakingBalanceProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingRedeem/StakingRedeemProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingBondMore/StakingBondMoreProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingRebondSetup/StakingRebondSetupProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/ControllerAccount/ControllerAccountProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/ValidatorListFilter/ValidatorListFilterProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/ValidatorSearch/ValidatorSearchProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/SelectValidatorsConfirmProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/RecommendedValidatorListProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/SelectedValidatorListProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/CustomValidatorListProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/ValidatorInfo/ValidatorInfoProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Vote/Crowdloan/CrowdloanList/CrowdloanListProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Vote/Crowdloan/CrowdloanContribution/CrowdloanContributionProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Vote/Crowdloan/CrowdloanContributionSetup/CrowdloanContributionSetupProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Vote/Crowdloan/CrowdloanContributionConfirm/CrowdloanContributionConfirmProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Vote/Crowdloan/CustomCrowdloan/CustomCrowdloanDelegate.swift\" \\\n\"$INPUT_DIR/Modules/Vote/Crowdloan/ReferralCrowdloan/ReferralCrowdloanProtocols.swift\" \\\n\"$INPUT_DIR/Common/ViewController/SelectionListViewController/SelectionListProtocols.swift\" \\\n\"$INPUT_DIR/Modules/AssetSelection/AssetSelectionProtocols.swift\" \\\n\"$INPUT_DIR/Modules/AdvancedWallet/AdvancedWalletProtocols.swift\" \\\n\"$INPUT_DIR/Modules/DApp/DAppAuthConfirm/DAppAuthConfirmProtocols.swift\" \\\n\"$INPUT_DIR/Modules/DApp/DAppList/DAppListProtocols.swift\" \\\n\"$INPUT_DIR/Modules/DApp/DAppBrowser/DAppBrowserProtocols.swift\" \\\n\"$INPUT_DIR/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmProtocols.swift\" \\\n\"$INPUT_DIR/Modules/DApp/DAppSearch/DAppSearchProtocols.swift\" \\\n\"$INPUT_DIR/Modules/AssetsSettings/AssetsSettingsProtocols.swift\" \\\n\"$INPUT_DIR/Modules/OperationDetails/OperationDetailsProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Vote/Crowdloan/CrowdloanYourContributions/CrowdloanYourContributionsProtocols.swift\" \\\n\"$INPUT_DIR/Modules/SecurityLayer/SecurityLayerProtocols.swift\" \\\n\"$INPUT_DIR/Modules/DApp/WalletConnect/Service/WalletConnectProtocols.swift\" \\\n"; }; - 63EE755C93B651D0D5388B14 /* [CP] Embed Pods Frameworks */ = { + 842D1E8924D207D900C30A7A /* Common Mock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-novawalletTests/Pods-novawalletTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + ); + name = "Common Mock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-novawalletTests/Pods-novawalletTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-novawalletTests/Pods-novawalletTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "#! /bin/sh\n# Define output file. Change \"$PROJECT_DIR/${PROJECT_NAME}Tests\" to your test's root source folder, if it's not the default name.\nOUTPUT_FILE=\"${PROJECT_NAME}Tests/Mocks/CommonMocks.swift\"\necho \"Generated Mocks File = $OUTPUT_FILE\"\n\n# Define input directory. Change \"${PROJECT_DIR}/${PROJECT_NAME}\" to your project's root source folder, if it's not the default name.\nINPUT_DIR=\"${PROJECT_NAME}\"\necho \"Mocks Input Directory = $INPUT_DIR\"\n\n# Generate mock files, include as many input files as you'd like to create mocks for.\n\"Pods/Cuckoo/run\" generate --no-header --testable \"${PROJECT_NAME},SoraKeystore\" \\\n--exclude \"JSONRPCBatchHandler\" \\\n--output \"${OUTPUT_FILE}\" \\\n\"$INPUT_DIR/Common/Helpers/Scheduler.swift\" \\\n\"$INPUT_DIR/Common/LocalAuthentication/BiometryAuth.swift\" \\\n\"$INPUT_DIR/Common/EventCenter/EventProtocols.swift\" \\\n\"$INPUT_DIR/Common/Network/Misc/SubstrateOperationFactory.swift\" \\\n\"$INPUT_DIR/Common/Helpers/AccountRepositoryFactory.swift\" \\\n\"$INPUT_DIR/../Pods/SoraKeystore/SoraKeystore/Classes/Keychain/KeystoreProtocols.swift\" \\\n\"$INPUT_DIR/../Pods/SubstrateSdk/SubstrateSdk/Classes/Network/JSONRPCEngine.swift\" \\\n\"$INPUT_DIR/Common/Operation/EraCountdownOperationFactory/EraCountdownOperationFactory.swift\" \\\n\"$INPUT_DIR/Common/Network/JSONRPC/ConnectionAutobalancing.swift\" \\\n\"$INPUT_DIR/Common/Network/JSONRPC/ConnectionStateReporting.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/ConnectionPool/ConnectionFactory.swift\" \\\n\"$INPUT_DIR/Common/Network/Misc/DataOperationFactory.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/RuntimeFilesOperationFactory.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProviderPool.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProviderFactory.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeCodingService.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSyncService.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/RuntimeProviderPool/CommonTypesSyncService.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProvider.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/ConnectionPool/ConnectionPool.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/SpecVersionSubscriptionFactory.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/SpecVersionSubscription.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/ChainRegistry.swift\" \\\n\"$INPUT_DIR/Common/Services/RemoteSubscription/Substrate/CrowdloanRemoteSubscriptionService.swift\" \\\n\"$INPUT_DIR/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscriptionService.swift\" \\\n\"$INPUT_DIR/Common/Services/RemoteSubscription/Substrate/StakingRemoteSubscriptionService.swift\" \\\n\"$INPUT_DIR/Common/Services/RemoteSubscription/Substrate/StakingAccountUpdatingService.swift\" \\\n\"$INPUT_DIR/Modules/Staking/Services/StakingServiceFactory.swift\" \\\n\"$INPUT_DIR/Common/Services/ApplicationService.swift\" \\\n\"$INPUT_DIR/Common/Currency/CurrencyRepository.swift\" \\\n"; }; - 814F6F81A24CB16EC99A1485 /* [CP] Embed Pods Frameworks */ = { + 849013CD24A92260008F705E /* Swiftlint */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-novawalletAll-novawallet/Pods-novawalletAll-novawallet-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + ); + name = Swiftlint; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-novawalletAll-novawallet/Pods-novawalletAll-novawallet-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-novawalletAll-novawallet/Pods-novawalletAll-novawallet-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\" --config \".swiftlint.yml\"\n"; }; - 842D1E8824D207C700C30A7A /* Modules Mock */ = { + 849013CE24A92280008F705E /* R.swift */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -17654,35 +18761,36 @@ inputFileListPaths = ( ); inputPaths = ( + "$TEMP_DIR/rswift-lastrun", ); - name = "Modules Mock"; + name = R.swift; outputFileListPaths = ( ); outputPaths = ( + $PROJECT_DIR/R.generated.swift, ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "#! /bin/sh\n# Define output file. Change \"$PROJECT_DIR/${PROJECT_NAME}Tests\" to your test's root source folder, if it's not the default name.\nOUTPUT_FILE=\"${PROJECT_NAME}Tests/Mocks/ModuleMocks.swift\"\necho \"Generated Mocks File = $OUTPUT_FILE\"\n\n# Define input directory. Change \"${PROJECT_DIR}/${PROJECT_NAME}\" to your project's root source folder, if it's not the default name.\nINPUT_DIR=\"${PROJECT_NAME}\"\necho \"Mocks Input Directory = $INPUT_DIR\"\n\n# Generate mock files, include as many input files as you'd like to create mocks for.\n\"Pods/Cuckoo/run\" generate --no-header --testable \"${PROJECT_NAME}\" \\\n--exclude \"RootPresenterFactoryProtocol, UsernameSetupViewFactoryProtocol, OnboardingMainViewFactoryProtocol, AccountCreateViewFactoryProtocol, AccountImportViewFactoryProtocol, AccountConfirmViewFactoryProtocol, PinViewFactoryProtocol, ProfileViewFactoryProtocol, AccountManagementViewFactoryProtocol, AccountInfoViewFactoryProtocol, NetworkManagementViewFactoryProtocol, NetworkInfoViewFactoryProtocol, AddConnectionViewFactoryProtocol, AccountExportPasswordViewFactoryProtocol, ExportRestoreJsonViewFactoryProtocol, ExportMnemonicViewFactoryProtocol, StakingMainViewFactoryProtocol, SelectValidatorsStartViewFactoryProtocol, SelectValidatorsConfirmViewFactoryProtocol, RecommendedValidatorListViewFactoryProtocol, SelectedValidatorListViewFactoryProtocol, CustomValidatorListViewFactoryProtocol, ValidatorInfoViewFactoryProtocol, WalletHistoryFilterViewFactoryProtocol, StakingPayoutConfirmationViewFactoryProtocol, StakingRewardDetailsViewFactoryProtocol, StakingRewardPayoutsViewFactoryProtocol, StakingPayoutConfirmViewModelFactoryProtocol, YourValidatorListViewFactoryProtocol, StakingUnbondSetupViewFactoryProtocol, StakingUnbondConfirmViewFactoryProtocol, StakingRedeemViewFactoryProtocol, StakingBondMoreViewFactoryProtocol, StakingRebondSetupViewFactoryProtocol, ValidatorListFilterViewFactoryProtocol, ValidatorSearchViewFactoryProtocol\" \\\n--output \"${OUTPUT_FILE}\" \\\n\"$INPUT_DIR/../Pods/SoraFoundation/SoraFoundation/Classes/Localization/Localizable.swift\" \\\n\"$INPUT_DIR/Common/Protocols/LoadableViewProtocol.swift\" \\\n\"$INPUT_DIR/Common/Protocols/ControllerBackedProtocol.swift\" \\\n\"$INPUT_DIR/Common/Protocols/WebPresentable.swift\" \\\n\"$INPUT_DIR/Common/Protocols/AlertPresentable.swift\" \\\n\"$INPUT_DIR/Common/Protocols/ModalAlertPresenting.swift\" \\\n\"$INPUT_DIR/Common/Protocols/SharingPresentable.swift\" \\\n\"$INPUT_DIR/Common/Protocols/AccountSelectionPresentable.swift\" \\\n\"$INPUT_DIR/Common/Protocols/AuthorizationPresentable.swift\" \\\n\"$INPUT_DIR/Common/ViewController/SearchController/TableSearchProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Root/RootProtocol.swift\" \\\n\"$INPUT_DIR/Modules/UsernameSetup/UsernameSetupProtocols.swift\" \\\n\"$INPUT_DIR/Modules/OnbordingMain/OnboardingMainProtocol.swift\" \\\n\"$INPUT_DIR/Modules/AccountCreate/AccountCreateProtocols.swift\" \\\n\"$INPUT_DIR/Modules/AccountImport/AccountImportProtocols.swift\" \\\n\"$INPUT_DIR/Modules/AccountConfirm/AccountConfirmProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Pincode/PinSetup/PinSetupProtocol.swift\" \\\n\"$INPUT_DIR/Modules/Settings/SettingsProtocols.swift\" \\\n\"$INPUT_DIR/Modules/AccountManagement/AccountManagementProtocols.swift\" \\\n\"$INPUT_DIR/Modules/AccountInfo/AccountInfoProtocols.swift\" \\\n\"$INPUT_DIR/Modules/NetworkManagement/NetworkManagementProtocols.swift\" \\\n\"$INPUT_DIR/Modules/NetworkInfo/NetworkInfoProtocols.swift\" \\\n\"$INPUT_DIR/Modules/AddConnection/AddConnectionProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Export/AccountExportPassword/AccountExportPasswordProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Export/ExportGenericView/ExportGenericProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Export/ExportMnemonic/ExportMnemonicProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Export/ExportRestoreJson/ExportRestoreJsonProtocols.swift\" \\\n\"$INPUT_DIR/Modules/TransactionHistory/HistoryFilter/WalletHistoryFilterProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/Operations/ValidatorOperationFactory/ValidatorOperationFactoryProtocol.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingMain/StakingMainProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingMain/Relaychain/StakingRelaychainProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/Operations/NetworkStakingInfoOperationFactory.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingRewardDetails/StakingRewardDetailsProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingRewardPayouts/StakingRewardPayoutsProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingBalance/StakingBalanceProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingRedeem/StakingRedeemProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingBondMore/StakingBondMoreProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingRebondSetup/StakingRebondSetupProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/ControllerAccount/ControllerAccountProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/ValidatorListFilter/ValidatorListFilterProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/ValidatorSearch/ValidatorSearchProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/SelectValidatorsConfirmProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/RecommendedValidatorListProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/SelectedValidatorListProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/CustomValidatorListProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Staking/SelectValidatorsFlow/ValidatorInfo/ValidatorInfoProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Vote/Crowdloan/CrowdloanList/CrowdloanListProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Vote/Crowdloan/CrowdloanContribution/CrowdloanContributionProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Vote/Crowdloan/CrowdloanContributionSetup/CrowdloanContributionSetupProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Vote/Crowdloan/CrowdloanContributionConfirm/CrowdloanContributionConfirmProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Vote/Crowdloan/CustomCrowdloan/CustomCrowdloanDelegate.swift\" \\\n\"$INPUT_DIR/Modules/Vote/Crowdloan/ReferralCrowdloan/ReferralCrowdloanProtocols.swift\" \\\n\"$INPUT_DIR/Common/ViewController/SelectionListViewController/SelectionListProtocols.swift\" \\\n\"$INPUT_DIR/Modules/AssetSelection/AssetSelectionProtocols.swift\" \\\n\"$INPUT_DIR/Modules/AdvancedWallet/AdvancedWalletProtocols.swift\" \\\n\"$INPUT_DIR/Modules/DApp/DAppAuthConfirm/DAppAuthConfirmProtocols.swift\" \\\n\"$INPUT_DIR/Modules/DApp/DAppList/DAppListProtocols.swift\" \\\n\"$INPUT_DIR/Modules/DApp/DAppBrowser/DAppBrowserProtocols.swift\" \\\n\"$INPUT_DIR/Modules/DApp/DAppOperationConfirm/DAppOperationConfirmProtocols.swift\" \\\n\"$INPUT_DIR/Modules/DApp/DAppSearch/DAppSearchProtocols.swift\" \\\n\"$INPUT_DIR/Modules/AssetsSettings/AssetsSettingsProtocols.swift\" \\\n\"$INPUT_DIR/Modules/OperationDetails/OperationDetailsProtocols.swift\" \\\n\"$INPUT_DIR/Modules/Vote/Crowdloan/CrowdloanYourContributions/CrowdloanYourContributionsProtocols.swift\" \\\n\"$INPUT_DIR/Modules/SecurityLayer/SecurityLayerProtocols.swift\" \\\n\"$INPUT_DIR/Modules/DApp/WalletConnect/Service/WalletConnectProtocols.swift\" \\\n"; + shellScript = "\"$PODS_ROOT/R.swift/rswift\" generate \"$PROJECT_DIR/R.generated.swift\"\n"; }; - 842D1E8924D207D900C30A7A /* Common Mock */ = { + 9AB453D5E48F4E4331292FC9 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); - name = "Common Mock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "#! /bin/sh\n# Define output file. Change \"$PROJECT_DIR/${PROJECT_NAME}Tests\" to your test's root source folder, if it's not the default name.\nOUTPUT_FILE=\"${PROJECT_NAME}Tests/Mocks/CommonMocks.swift\"\necho \"Generated Mocks File = $OUTPUT_FILE\"\n\n# Define input directory. Change \"${PROJECT_DIR}/${PROJECT_NAME}\" to your project's root source folder, if it's not the default name.\nINPUT_DIR=\"${PROJECT_NAME}\"\necho \"Mocks Input Directory = $INPUT_DIR\"\n\n# Generate mock files, include as many input files as you'd like to create mocks for.\n\"Pods/Cuckoo/run\" generate --no-header --testable \"${PROJECT_NAME},SoraKeystore\" \\\n--exclude \"JSONRPCBatchHandler\" \\\n--output \"${OUTPUT_FILE}\" \\\n\"$INPUT_DIR/Common/Helpers/Scheduler.swift\" \\\n\"$INPUT_DIR/Common/LocalAuthentication/BiometryAuth.swift\" \\\n\"$INPUT_DIR/Common/EventCenter/EventProtocols.swift\" \\\n\"$INPUT_DIR/Common/Network/Misc/SubstrateOperationFactory.swift\" \\\n\"$INPUT_DIR/Common/Helpers/AccountRepositoryFactory.swift\" \\\n\"$INPUT_DIR/../Pods/SoraKeystore/SoraKeystore/Classes/Keychain/KeystoreProtocols.swift\" \\\n\"$INPUT_DIR/../Pods/SubstrateSdk/SubstrateSdk/Classes/Network/JSONRPCEngine.swift\" \\\n\"$INPUT_DIR/Common/Operation/EraCountdownOperationFactory/EraCountdownOperationFactory.swift\" \\\n\"$INPUT_DIR/Common/Network/JSONRPC/ConnectionAutobalancing.swift\" \\\n\"$INPUT_DIR/Common/Network/JSONRPC/ConnectionStateReporting.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/ConnectionPool/ConnectionFactory.swift\" \\\n\"$INPUT_DIR/Common/Network/Misc/DataOperationFactory.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/RuntimeFilesOperationFactory.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProviderPool.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProviderFactory.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeCodingService.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeSyncService.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/RuntimeProviderPool/CommonTypesSyncService.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/RuntimeProviderPool/RuntimeProvider.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/ConnectionPool/ConnectionPool.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/SpecVersionSubscriptionFactory.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/SpecVersionSubscription.swift\" \\\n\"$INPUT_DIR/Common/Services/ChainRegistry/ChainRegistry.swift\" \\\n\"$INPUT_DIR/Common/Services/RemoteSubscription/Substrate/CrowdloanRemoteSubscriptionService.swift\" \\\n\"$INPUT_DIR/Common/Services/RemoteSubscription/Substrate/WalletRemoteSubscriptionService.swift\" \\\n\"$INPUT_DIR/Common/Services/RemoteSubscription/Substrate/StakingRemoteSubscriptionService.swift\" \\\n\"$INPUT_DIR/Common/Services/RemoteSubscription/Substrate/StakingAccountUpdatingService.swift\" \\\n\"$INPUT_DIR/Modules/Staking/Services/StakingServiceFactory.swift\" \\\n\"$INPUT_DIR/Common/Services/ApplicationService.swift\" \\\n\"$INPUT_DIR/Common/Currency/CurrencyRepository.swift\" \\\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-novawalletAll-novawalletIntegrationTests/Pods-novawalletAll-novawalletIntegrationTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; - 849013CD24A92260008F705E /* Swiftlint */ = { + AE2060202636DA5900357578 /* Inject keys */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -17691,16 +18799,18 @@ ); inputPaths = ( ); - name = Swiftlint; + name = "Inject keys"; outputFileListPaths = ( ); outputPaths = ( + "$(DERIVED_FILE_DIR)/CIKeys.generated.swift", + $PROJECT_DIR/CIKeys.generated.swift, ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\" --config \".swiftlint.yml\"\n"; + shellScript = "#check if env-vars.sh exists\nif [ -f $PROJECT_DIR/$PROJECT_NAME/env-vars.sh ]\nthen\nsource $PROJECT_DIR/$PROJECT_NAME/env-vars.sh\nfi\n#no `else` case needed if the CI works as expected\n\nWORK_DIR=\"$PROJECT_DIR/$PROJECT_NAME\"\necho \"Sourcery Work Directory = $WORK_DIR\"\n\nOUT_FILE=\"$PROJECT_DIR/CIKeys.generated.swift\"\necho \"Sourcery Output File = $OUT_FILE\"\n\n\"$PODS_ROOT/Sourcery/bin/sourcery\" --templates \"$WORK_DIR\" --sources \"$WORK_DIR\" --output \"$OUT_FILE\" --args mercuryoSecretKey=$MERCURYO_PRODUCTION_SECRET,mercuryoTestSecretKey=$MERCURYO_TEST_SECRET,acalaAuthToken=$ACALA_AUTH_TOKEN,moonbeamHistoryApiKey=$MOONBEAM_HISTORY_API_KEY,moonriverHistoryApiKey=$MOONRIVER_HISTORY_API_KEY,etherscanHistoryApiKey=$ETHERSCAN_HISTORY_API_KEY,acalaAuthTestToken=$ACALA_TEST_AUTH_TOKEN,moonbeamApiKey=$MOONBEAM_API_KEY,moonbeamApiTestKey=$MOONBEAM_TEST_API_KEY,infuraApiKey=$INFURA_API_KEY,wcProjectId=$WC_PROJECT_ID\n"; }; - 849013CE24A92280008F705E /* R.swift */ = { + B296F5F654EA299699479AE4 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -17708,39 +18818,38 @@ inputFileListPaths = ( ); inputPaths = ( - "$TEMP_DIR/rswift-lastrun", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = R.swift; + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( - $PROJECT_DIR/R.generated.swift, + "$(DERIVED_FILE_DIR)/Pods-novawalletAll-novawalletIntegrationTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"$PODS_ROOT/R.swift/rswift\" generate \"$PROJECT_DIR/R.generated.swift\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - AE2060202636DA5900357578 /* Inject keys */ = { + C8525C8798D5D5AB9F0FAE36 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-novawalletAll-novawallet/Pods-novawalletAll-novawallet-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); - name = "Inject keys"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/CIKeys.generated.swift", - $PROJECT_DIR/CIKeys.generated.swift, + "${PODS_ROOT}/Target Support Files/Pods-novawalletAll-novawallet/Pods-novawalletAll-novawallet-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "#check if env-vars.sh exists\nif [ -f $PROJECT_DIR/$PROJECT_NAME/env-vars.sh ]\nthen\nsource $PROJECT_DIR/$PROJECT_NAME/env-vars.sh\nfi\n#no `else` case needed if the CI works as expected\n\nWORK_DIR=\"$PROJECT_DIR/$PROJECT_NAME\"\necho \"Sourcery Work Directory = $WORK_DIR\"\n\nOUT_FILE=\"$PROJECT_DIR/CIKeys.generated.swift\"\necho \"Sourcery Output File = $OUT_FILE\"\n\n\"$PODS_ROOT/Sourcery/bin/sourcery\" --templates \"$WORK_DIR\" --sources \"$WORK_DIR\" --output \"$OUT_FILE\" --args mercuryoSecretKey=$MERCURYO_PRODUCTION_SECRET,mercuryoTestSecretKey=$MERCURYO_TEST_SECRET,acalaAuthToken=$ACALA_AUTH_TOKEN,moonbeamHistoryApiKey=$MOONBEAM_HISTORY_API_KEY,moonriverHistoryApiKey=$MOONRIVER_HISTORY_API_KEY,etherscanHistoryApiKey=$ETHERSCAN_HISTORY_API_KEY,acalaAuthTestToken=$ACALA_TEST_AUTH_TOKEN,moonbeamApiKey=$MOONBEAM_API_KEY,moonbeamApiTestKey=$MOONBEAM_TEST_API_KEY,infuraApiKey=$INFURA_API_KEY,wcProjectId=$WC_PROJECT_ID\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-novawalletAll-novawallet/Pods-novawalletAll-novawallet-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; - BB864FD3004FEB055054B4A1 /* [CP] Check Pods Manifest.lock */ = { + EEA0C402E0BBD829D8FC447E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -17780,6 +18889,23 @@ shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/SwiftFormat/CommandLineTool/swiftformat\" \"$SRCROOT/novawallet\"\n"; }; + FA9ADF1F9B8A1BA6950A0416 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-novawalletTests/Pods-novawalletTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-novawalletTests/Pods-novawalletTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-novawalletTests/Pods-novawalletTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -17788,6 +18914,7 @@ buildActionMask = 2147483647; files = ( 844133BB2860528500845987 /* XcmTransfersFeeTests.swift in Sources */, + 0C2F86932A72648D00593C01 /* ActiveNominationPoolsTests.swift in Sources */, 849E16E42714D1BD0065B305 /* EthereumBaseIntegrationTests.swift in Sources */, 84BB3CE9267C9ADE00676FFE /* CrowdloanTests.swift in Sources */, 8479F31426CD9A0E005D8D24 /* ChainRegistryIntegrationTests.swift in Sources */, @@ -17796,8 +18923,10 @@ 849ABE8A262833C000011A2A /* PayoutRewardsServiceTests.swift in Sources */, 8461CC8526BC1306007460E4 /* MortalEraFactoryTests.swift in Sources */, 840874E02978882700ACFA55 /* Gov2DelegationTests.swift in Sources */, + 0C2F869A2A72948100593C01 /* NominationPoolsApyTests.swift in Sources */, 8438E1D224BFAAD2001BDB13 /* JSONRPCTests.swift in Sources */, 8455F19A2A1DEEAF003F072D /* SubqueryMultistakingTests.swift in Sources */, + 0C59E8EB2AA71C3E001E11F3 /* ExternalAssetBalanceIntegrationTests.swift in Sources */, AEE4E37225ECF83F00D6DF31 /* StakingInfoTests.swift in Sources */, E2F3E726280823CF00CF31B5 /* ETHAccountInjection.swift in Sources */, F441BE13263986BE0096B67B /* ExtrinsicServiceTests.swift in Sources */, @@ -17820,6 +18949,7 @@ 847A25C328D84A9C006AC9F5 /* ReferendumFetchTests.swift in Sources */, 84161DB627C39013005DF668 /* NftSyncIntegrationTests.swift in Sources */, 84FACB6625F56D5000F32ED4 /* CalculatorServiceTests.swift in Sources */, + 0CE150542A70EA2200B61CC1 /* NominationPoolsSyncTests.swift in Sources */, 8813702C29C13E7900829458 /* Web3NamesOperationFactoryTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -17844,6 +18974,7 @@ 886A278029E939D9003B269F /* EquilibriumAccountData.swift in Sources */, 88F34FD228FF045400712BDE /* BindableView.swift in Sources */, 8857E03128A6A22500260BA2 /* PriceAssetInfoFactory.swift in Sources */, + 0CF193D52A861926003F12F6 /* PredefinedTimeShortcut.swift in Sources */, 846802A3265DA5530034F9B5 /* CrowdloanContributionSetupViewModel.swift in Sources */, 88D02FE52942EA7800E26390 /* AssetDetailsStyles.swift in Sources */, 84CAC1CC298A715900F78169 /* GovernanceOffchainVotesLocal.swift in Sources */, @@ -17853,6 +18984,7 @@ 84BEE059255F4D5700D05EB3 /* AccountImportPreferredInfo.swift in Sources */, 84C41F3828EDB80700DB1CD3 /* ReferendaCurve.swift in Sources */, 8490141524A92F6D008F705E /* OnboardingMainPresenter.swift in Sources */, + 0C626D1B2A8F519100CDAF4E /* StakingMainPresenterFactory+NominationPools.swift in Sources */, 846449FF2567090B004EAA4B /* TriangularedView+Inspectable.swift in Sources */, 8460E11D2542011200826F55 /* DetailsTriangularedView.swift in Sources */, D9D1636827451C2400681C1F /* AcalaLiquidContributionResponse.swift in Sources */, @@ -17861,9 +18993,11 @@ 8842105A289BBA8D00306F2C /* CurrencyWireframe.swift in Sources */, 849014BD24AA87E4008F705E /* PinViewFactory.swift in Sources */, 8428765524ADDE0200D91AD8 /* SettingsAccountViewModel.swift in Sources */, + 0CC2E5662A6E64EC004092E7 /* NPoolsLocalStorageSubscriber.swift in Sources */, 777BD86629F9C426004969A2 /* ReferendumsFiltersDelegate.swift in Sources */, 842D8B7A2A416A9A00660005 /* UIView+Shimmering.swift in Sources */, 842876B624AE05C700D91AD8 /* EmailPresentable.swift in Sources */, + 0C59E8FC2AA76C4A001E11F3 /* OperationDetailsPoolStakingProvider.swift in Sources */, 847F2D5327AAB59200AFD476 /* GradientFactory.swift in Sources */, 84D1110E26B931C20016D962 /* ChainModel.swift in Sources */, 84216FD1282797ED00479375 /* ParachainStakingCollatorServiceProtocol.swift in Sources */, @@ -17894,10 +19028,12 @@ 841E5559282E6A7700C8438F /* ParaStkDurationOperationFactory.swift in Sources */, 841E553E282D469F00C8438F /* ParachainStakingDelegator.swift in Sources */, 846CD25B265709A800A2E4B6 /* StorageKeySuffixMapper.swift in Sources */, + 0C7C886E2A962B0D00DD96A1 /* StackAddressCell.swift in Sources */, 843612C92790109100DC739E /* SigningWrapperFactory.swift in Sources */, 88FF5C7F29C8364500D1CB5D /* Caip2+ParseError.swift in Sources */, 8473D40D2657E9DC00B394B2 /* MultiSigner.swift in Sources */, D9D163662744F9BF00681C1F /* ExternalContributionSource.swift in Sources */, + 77F033932A814296006BC67E /* GenericStakingTypeAccountView.swift in Sources */, 840DFF5528942CA9001B11EA /* ChainAddressDetailsPresentable.swift in Sources */, 8407716428CE1408007DBD24 /* ParaStkYieldBoostErrorPresentable.swift in Sources */, F477CD26262EAD30004DF739 /* StakingPayoutViewModelFactory.swift in Sources */, @@ -17911,6 +19047,7 @@ 8428766724ADF22000D91AD8 /* TransformAnimator+Common.swift in Sources */, 8419F054295ED55000A14E05 /* LocalChainExternalApi.swift in Sources */, 8476D39F27F4582A004D9A7A /* DAppMetamaskPhishingDetectedState.swift in Sources */, + 77F9FB0D2A9D9C5600820625 /* NominationPoolBondMoreBaseProtocols.swift in Sources */, AEFC6D6F2600D7CD000BD310 /* NetworkInfoViewModelFactory.swift in Sources */, 84AA004526C6A04A00BCB4DC /* CommonTypesSyncService.swift in Sources */, 8490147424A94A37008F705E /* RootProtocol.swift in Sources */, @@ -17935,10 +19072,13 @@ 841494402604E71C000D8D1A /* TotalRewardItem.swift in Sources */, 88DA0CD0295F345E0009F70F /* WKWebView+VewportScript.swift in Sources */, 84DA3B1224C6D29100B5E27F /* RuntimeVersion.swift in Sources */, + 0C962F882AA85C7F00C0B551 /* TransactionHistoryAccountPrefixFilter.swift in Sources */, 840E59BD2A188B7B00BA6ADD /* UIImage+Filter.swift in Sources */, + 0CB06E752A68139C00C7EC99 /* StakingDashboardNominationPoolMapper.swift in Sources */, 8476D3A127F4598D004D9A7A /* DAppBrowserPhishingDetectedState.swift in Sources */, 8472C39E298BE5F600043061 /* ReferendumTrackGroup.swift in Sources */, 8886020D29D1024900C6344C /* KiltWeb3n+schema.swift in Sources */, + 0CC4CCF42A67C9C400F63041 /* Multistaking+NominationPools.swift in Sources */, 8445F41628C8CFEC009E61C4 /* ParaStkYieldBoostSelectionViewModel.swift in Sources */, 84C1706F299643A800CBE531 /* GovernanceDelegateViewModelFactory.swift in Sources */, 84FBED052927B1CA00FBEB83 /* EvmEventParser.swift in Sources */, @@ -17984,12 +19124,13 @@ 778D979F2A24D248002BA681 /* SearchViewProtocol.swift in Sources */, 8460E713284ABE22002896E9 /* UnstakeCallWrapper.swift in Sources */, 843612C3278FF0C700DC739E /* FeeRetryable.swift in Sources */, - 847ABE332853333A00851218 /* StakingSharedState+Duration.swift in Sources */, 8849AD6029C3526700F4F7FF /* Caip2.swift in Sources */, 8424A8C7262EC0E50091BFB1 /* PayoutInfo.swift in Sources */, 84F523FF298AA64A0026AD08 /* ReferendumsModelFactory+Votes.swift in Sources */, 843910C7253F56EA00E3C217 /* BaseOperation+Result.swift in Sources */, + 77AB555B2AA246CA0058814E /* OperationPoolRewardOrSlashViewModel.swift in Sources */, 849D3225291CC4A500D25839 /* MarkdownParsingOperationFactory.swift in Sources */, + 0C79C8952A7BE01100B171E3 /* RelaychainStakingRecommendation.swift in Sources */, 84D6D7FE27A7F4CD0094FC33 /* AssetListChainView.swift in Sources */, 842D1E8424D197C900C30A7A /* KeyboardAdoptable.swift in Sources */, 849DEBD425ED015C00C64C19 /* SelectValidatorsConfirmViewModel.swift in Sources */, @@ -18003,6 +19144,7 @@ F4F2297C260DC05600ACFDB8 /* StakingRewardTokenUsdViewModel.swift in Sources */, 88D271F1298ACD1B000AABF7 /* VotesViewController.swift in Sources */, 84F52401298AF3F50026AD08 /* GovOffchainModelWrapperFactory.swift in Sources */, + 0C79C8922A7BD9BB00B171E3 /* SelectedStakingOption.swift in Sources */, 844DBC6B274E3D6B009F8351 /* AccountImportKeystoreView.swift in Sources */, 847F2D4627A9DB8E00AFD476 /* MultigradientView.swift in Sources */, 844CED2329260737001A7757 /* EvmQueryMessage.swift in Sources */, @@ -18026,6 +19168,7 @@ 84AE7AB927D3F96300495267 /* RMRKV1Collection.swift in Sources */, 84AE7AAF27D38B1800495267 /* DrawableIconViewModel.swift in Sources */, 849976D027B3AC0100B14A6C /* MetamaskEvent.swift in Sources */, + 0CE629DD2AA9B6BF00E250BD /* RewardDestinationViewModel.swift in Sources */, 849976BC27B25A6600B14A6C /* DAppTransportModel.swift in Sources */, 84D2F1B32775A3220040C680 /* ExtrinsicJSONProcessor.swift in Sources */, 8428765624ADDE0200D91AD8 /* SettingsCellViewModel.swift in Sources */, @@ -18037,17 +19180,21 @@ 84452FA525D679F200F47EC5 /* CDRuntimeMetadataItem+CoreDataCodable.swift in Sources */, 84DED4032666537600A153BB /* CrowdloanBonusService.swift in Sources */, 844EFB5A265FCD9F0090ACB1 /* CrowdloanContributionProtocols.swift in Sources */, + 77F033992A8142E5006BC67E /* DirectStakingTypeAccountViewModel.swift in Sources */, AE8B8837267351E300AB0AA9 /* CustomValidatorListComposer.swift in Sources */, 846B749C28B4D3B400C39B93 /* CancelOperationPresentable.swift in Sources */, 846E5018277AD3D50049B659 /* DAppList.swift in Sources */, 845E49832636C87B002F8C22 /* ActionManageViewModel.swift in Sources */, 8887813C28B62B0A00E7290F /* FlexibleSpaceView.swift in Sources */, + 0C13D3132A80D06B0054BB6F /* StakingRecommendationValidationFactory.swift in Sources */, 77A0B2ED2A3B7F3300CBF653 /* DAppCollectionViewCell.swift in Sources */, 8471538D2653B29100CB91D8 /* ChangeRewardDestinationViewModelFactory.swift in Sources */, 84BE207825E7D62100B4748C /* ActiveEraInfo.swift in Sources */, 84216FDA2827B25F00479375 /* StorageItemSyncService.swift in Sources */, 8499FEDE27C0AB0200712589 /* NftSource.swift in Sources */, 84D2F1B12775999F0040C680 /* DAppParsedExtrinsic.swift in Sources */, + 7738FB6A2A4C5D1A00797439 /* StartStakingRelaychainInteractor.swift in Sources */, + 0C59E8D32AA5FBE2001E11F3 /* PooledAssetBalanceMapper.swift in Sources */, 84AE7AAB27D373A200495267 /* StackInfoTableCell.swift in Sources */, 84A59160292B468300BCCF8F /* EvmTransactionService.swift in Sources */, 845BB8C925E45D1500E5FCDC /* BondCall.swift in Sources */, @@ -18084,7 +19231,6 @@ 84715786291136B100D7D003 /* GovernanceUnlocksViewModel.swift in Sources */, 84DD5F64263DFAB700425ACF /* ErrorConditionViolation.swift in Sources */, 8428768324AE046300D91AD8 /* LanguageSelectionViewController.swift in Sources */, - 8493D3E62705994200157009 /* StakingSharedState.swift in Sources */, F452D8CA273E58D5008F7295 /* SettingsTableFooterView.swift in Sources */, 8470D6D2253E3382009E9A5D /* StorageSubscriptionContainer.swift in Sources */, 84038FEE26FFBA6200C73F3F /* PriceLocalSubscriptionHandler.swift in Sources */, @@ -18097,6 +19243,7 @@ 84690783264132AB0030E693 /* WithdrawUnbondedCall.swift in Sources */, 842EBB332890A77C00B952D8 /* WalletSelectionInteractor.swift in Sources */, 8444407128AA57D600446D22 /* LedgerApplication.swift in Sources */, + 77F9FB092A9D971000820625 /* NominationPoolBondMoreSetupInteractor.swift in Sources */, 8490146D24A9487A008F705E /* ErrorPresentable+AlertText.swift in Sources */, 8460E71C284D41E7002896E9 /* ParaStkUnstakePresenter+Protocol.swift in Sources */, 84FBECF92927403100FBEB83 /* EvmAssetContractId.swift in Sources */, @@ -18135,6 +19282,7 @@ 88F33F1529CC1F92006125D5 /* Slip44CoinList.swift in Sources */, 84B018B026E0450F00C75E28 /* ValidatorStateView.swift in Sources */, AEE0C43A272A8B1F009F9AD5 /* AddChainAccount+AccountCreateWireframe.swift in Sources */, + 0C7E7FAB2A9F27FB00596628 /* NominationPoolsRedeemCall.swift in Sources */, 84038FF226FFBE1900C73F3F /* JsonLocalSubscriptionHandler.swift in Sources */, 842898D1265A955A002D5D65 /* ImageViewModel.swift in Sources */, 77A0B2F52A3CA39D00CBF653 /* StakingMoreOptionCollectionViewCell.swift in Sources */, @@ -18156,6 +19304,7 @@ 8466781C27ED9644007935D3 /* AsyncWarningConditionViolation.swift in Sources */, 849067CA299BCCB700B2983E /* GovernanceRevokeDelegationConfirmPresenter+Update.swift in Sources */, 0C3205BE2A867A9C002EB914 /* EvmLegacyGasPriceProvider.swift in Sources */, + 0CE629D92AA9B68C00E250BD /* AssetBalanceViewModel.swift in Sources */, 84452F9325D5EE7300F47EC5 /* DataOperationFactory.swift in Sources */, 841E5538282CF3F400C8438F /* StakingMainViewModelFactory.swift in Sources */, 8442003C28EAA2E400C49C4A /* ReferendumsWireframe.swift in Sources */, @@ -18196,6 +19345,7 @@ 8443FDB12554B7640092893D /* TitledMnemonicView.swift in Sources */, AE2C84CA25EF986E00986716 /* ValidatorInfoProtocols.swift in Sources */, 844518D127CAB6FC00D0C52B /* NftImageViewModel.swift in Sources */, + 0CB261F72A9E2D8400287305 /* StackSwitchCell.swift in Sources */, AEAC690426EB891900346599 /* Logger+FearlessUtils.swift in Sources */, 84FF268228494EA4003EC78D /* ParaStkStakeConfirmPresenter+StakeMore.swift in Sources */, 848E6BE32763346800C91022 /* СhainAssetSelectionControl.swift in Sources */, @@ -18209,6 +19359,7 @@ 846952A22852A1480083E0B4 /* StakingDurationOperationFactoryProtocol.swift in Sources */, F4D054662729944800210294 /* MoonbeamMakeSignatureRequest.swift in Sources */, 84BFFA0C288FFFA100069846 /* WalletsListViewModel.swift in Sources */, + 0CF193D12A843DA9003F12F6 /* StakingTypeBalanceFactory.swift in Sources */, 88F7716428BF6B59008C028A /* GenericMultiValueView.swift in Sources */, 847157802910F30500D7D003 /* GovernanceUnlockInteractorError.swift in Sources */, 843612BB278FD6E000DC739E /* DAppSigningType.swift in Sources */, @@ -18216,6 +19367,7 @@ 849D14CC2994EEA50048E947 /* GovernanceDelegateFlowDisplayInfo.swift in Sources */, F44CD8F426242825005DDF23 /* PayoutRewardsService+Fetch.swift in Sources */, 8489198E2A0529DA008D57A3 /* NetworkTableViewCell.swift in Sources */, + 77CC82A52A984EDA002D022F /* UINavigaionController+Pop.swift in Sources */, 84E4897329A79017008726FA /* SkeletonDecoration+View.swift in Sources */, 2AD0A19525D3D3EC00312428 /* GitHubOperationFactory.swift in Sources */, 845B07FF2916F529005785D3 /* Gov1SubscriptionFactory.swift in Sources */, @@ -18257,6 +19409,7 @@ 84C5ADD928127F4D006D7388 /* WalletAccountSelectionView.swift in Sources */, 843910D6253F8DAD00E3C217 /* NetworkAvailabilityLayerProtocols.swift in Sources */, 849842E626587573006BBB9F /* MultilineTableViewCell.swift in Sources */, + 0CB261E42A9BE31B00287305 /* NPoolsUnstakeBasePresenter.swift in Sources */, 84F3B27A27F4187300D64CF5 /* PhishingSitesSyncService.swift in Sources */, AEA0C8A6267B6B2600F9666F /* SelectedValidatorListWireframe.swift in Sources */, 84963D7026F92F18003FE8E4 /* WalletRemoteSubscriptionService.swift in Sources */, @@ -18347,6 +19500,7 @@ 846CA77C27099DD90011124C /* WeaklyAnalyticsRewardSource.swift in Sources */, 8455F1932A1DC64E003F072D /* Multistaking.swift in Sources */, 84452F9D25D6768000F47EC5 /* RuntimeMetadataItem.swift in Sources */, + 0C59E8F22AA76436001E11F3 /* OperationDetailsTransferProvider.swift in Sources */, 84B28FC428C54441007A1006 /* OnChainTransferAmount.swift in Sources */, 84A3B8A82836E4A100DE2669 /* ParachainStakingCandidateMetadata.swift in Sources */, 849842FE26592C2B006BBB9F /* StatusSectionView.swift in Sources */, @@ -18357,14 +19511,18 @@ 841E5547282D7F5800C8438F /* StakingMainPresenterFactory+Parachain.swift in Sources */, 84F4A90A254CC863000CF0A3 /* KeystoreExportWrapper.swift in Sources */, 843074F928BF6201009D463B /* NoAccountSupportPresentable.swift in Sources */, + 77F189402A49972300E8B933 /* UILabel+bind.swift in Sources */, 8467FCFE24E5C50B005D486C /* KeystoreImportService.swift in Sources */, 847A259029B5D2710054F90C /* GovJsonLocalStorageHandler.swift in Sources */, 84C6801024D6EE4500006BF5 /* PlusIndicatorView.swift in Sources */, 8428765A24ADDE0200D91AD8 /* SettingsViewFactory.swift in Sources */, 849E17E8279143B2002D1744 /* NavigationBarStyle.swift in Sources */, + 77171CAA2A98BC420032B387 /* NominationPoolDataValidatorFactory.swift in Sources */, 84350AD9284604CE0031EF24 /* ParaStkYourCollatorsViewModel.swift in Sources */, 8490141624A92F6D008F705E /* OnboardingMainViewFactory.swift in Sources */, + 0C962F862AA859F200C0B551 /* TransactionHistoryLocalFilter.swift in Sources */, 842348E92614F6EA002127AF /* SkeletonLoadable.swift in Sources */, + 0CE629E02AA9B70200E250BD /* CalculatedReward.swift in Sources */, 849A4EFA279ABC8800AB6709 /* AssetBalanceMapper.swift in Sources */, 8490147724A94A37008F705E /* RootPresenter.swift in Sources */, 8428765724ADDE0200D91AD8 /* SettingsTableViewCell.swift in Sources */, @@ -18380,6 +19538,7 @@ 8489A6DC27FE9F570040C066 /* StakingUnbondingItemViewModel.swift in Sources */, 848DE76626D642670045CD29 /* StorageMigrator.swift in Sources */, F4AE12A1268DD69B0097D1C7 /* MnemonicTextNormalizer.swift in Sources */, + 0C13D31A2A8222D20054BB6F /* StartStakingConfirmInteractorError.swift in Sources */, 8463A72525E3A82A003B8160 /* DataProviderProxyTrigger.swift in Sources */, 84CEF28A290466A800BA25BB /* GovernanceErrorPresentable.swift in Sources */, 8460E720284D452E002896E9 /* ParaStkUnstakePresenter+ModalPicker.swift in Sources */, @@ -18417,6 +19576,7 @@ 84F43C0F25DF016600AEDA56 /* DispatchQueueHelper.swift in Sources */, 8490147824A94A37008F705E /* RootPresenterFactory.swift in Sources */, 849014C224AA87E4008F705E /* LocalAuthPresenter.swift in Sources */, + 0C7C88642A94E09F00DD96A1 /* NPoolsPendingRewardDataSource.swift in Sources */, 88BB21A028D34C660019C6B4 /* DataProviderChange+Identifier.swift in Sources */, 8446F5F6281916D300B7A86C /* StakingRewardsHeaderCell.swift in Sources */, 84B8AA8929FA2FE500347A37 /* WalletConnectTransportError.swift in Sources */, @@ -18455,12 +19615,14 @@ AEE5FAFD26415DE2002B8FDC /* StakingRebondSetupViewController.swift in Sources */, 84CAC1CE298A7A4800F78169 /* GovernanceDelegationAdditions.swift in Sources */, F47F5A7A2626E91A009BCFF4 /* StakingRewardHistoryViewModel.swift in Sources */, + 77F033A82A86136D006BC67E /* StakingSelectPoolViewModelFactory.swift in Sources */, AE7129B4260872ED000AA3F5 /* EraStakersInfoChanged.swift in Sources */, 842D1E9624D2DD6700C30A7A /* MnemonicDisplayView.swift in Sources */, 8411707A285B10F5006F4DFB /* XcmAssetTransferFee.swift in Sources */, 845B07F729162AB3005785D3 /* Democracy+ConstantPath.swift in Sources */, 84F4B2372A31D2F300113DDD /* ParitySignerType.swift in Sources */, 84DBEA56265ED62700FDF73C /* BaseErrorPresentable.swift in Sources */, + 0C2F86862A72352400593C01 /* NominationPoolModel.swift in Sources */, 845B811928F43D4C0040CE84 /* Treasury+CodingPath.swift in Sources */, 842876B224AE059700D91AD8 /* SupportData.swift in Sources */, 84D8754228EB5D66004065BD /* ChainBalanceViewModel.swift in Sources */, @@ -18469,6 +19631,7 @@ 8443045C2A28734B00DE36DE /* ParachainMultistakingUpdateService.swift in Sources */, 84893C0524DA8663008F6A3F /* AccountCreationError.swift in Sources */, 84282FBD26D05A54002CA322 /* ChainRegistryFacade.swift in Sources */, + 0C59E8C92AA5C7EC001E11F3 /* ExternalAssetBalance.swift in Sources */, 842EBB3B2890B06500B952D8 /* WalletSelectionPresenter.swift in Sources */, 88D1F1B22981212700316A1A /* InAppUpdatesServiceWireframeProtocol.swift in Sources */, 84C1DBC329C27A6600F295A5 /* Paras.swift in Sources */, @@ -18486,6 +19649,7 @@ 8401620B25E144D50087A5F3 /* AmountInputAccessoryView.swift in Sources */, 88F19DE028D8D0F600F6E459 /* LoadableViewModelState+Addition.swift in Sources */, 844C3E652A07627E00C4305F /* DAppWalletAuthViewModel.swift in Sources */, + 77799AE52A792AE700B7E564 /* StakingTypeViewModel.swift in Sources */, 84A3B8A22836DA2600DE2669 /* LastAccountIdKeyWrapper.swift in Sources */, 847A258E29B5D25E0054F90C /* GovJsonLocalStorageSubscriber.swift in Sources */, 84EDF67329C66015002173E6 /* EvmNativeTransactionHistoryUpdater.swift in Sources */, @@ -18530,6 +19694,7 @@ 8448F7A22882ABF50080CEA9 /* CustomSearchView.swift in Sources */, 84D2F1AF2774CEF00040C680 /* DAppURLBarView.swift in Sources */, 849D3223291CC43D00D25839 /* MarkdownText.swift in Sources */, + 0C77B5632A83747200B5AE08 /* StaticValidatorListViewLayout.swift in Sources */, 84FFE45B2862076F002432BB /* XcmUnweightedTransferRequest.swift in Sources */, 84D1110C26B922D50016D962 /* ConnectionPool.swift in Sources */, 840DFF4F28940444001B11EA /* ChainAddressDetailsModel.swift in Sources */, @@ -18544,6 +19709,7 @@ 840D92A7278EE8690007B979 /* DAppSignBytesConfirmInteractor.swift in Sources */, 84EC2D1A276C6600009B0BE1 /* PolkadotExtensionExtrinsic.swift in Sources */, 84A8FD8E265FDA76002ADB58 /* CrowdloanContributionConfirmData.swift in Sources */, + 0C59E8E32AA61252001E11F3 /* NominationPoolExternalServiceFactory.swift in Sources */, 84B8AA8729F9115400347A37 /* WalletConnectTransportMessage.swift in Sources */, 84F98D9625E3E2B10040418E /* InMemoryDataProviderRepository.swift in Sources */, 8446F5FA28192FF500B7A86C /* ListLoadingView.swift in Sources */, @@ -18554,6 +19720,8 @@ 8428768624AE046300D91AD8 /* LanguageSelectionPresenter.swift in Sources */, 84CA407A2A036886004BB71E /* PlainBaseTableViewCell.swift in Sources */, 8490152724ABCC40008F705E /* NumberFormatter.swift in Sources */, + 77CC82A32A984BC3002D022F /* StartStakingSelectedValidatorsListWireframe.swift in Sources */, + 0C77B5692A837D4000B5AE08 /* StaticValidatorListViewFactory.swift in Sources */, 846A2C4325271CDE00731018 /* TransactionType.swift in Sources */, 84893C0124DA861D008F6A3F /* AccountCreationMetadata.swift in Sources */, 84468A0D28666BFF00BCBE00 /* CrossChainTransferSetupWireframe.swift in Sources */, @@ -18562,10 +19730,8 @@ 845870A829B1D76A0017281A /* TableSearchResultViewModel.swift in Sources */, 2A84E87825D425750006FE9C /* AlertControllerFactory.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 */, 840882B02514024800177E20 /* SelectedConnectionChanged.swift in Sources */, 84FD3DB72540EF0700A234E3 /* TransactionSubscription.swift in Sources */, 8482F628280C49940006C3A0 /* DAppsAuthViewModelFactory.swift in Sources */, @@ -18595,6 +19761,7 @@ 88E8CF5E28E3789600C90112 /* CrowdloanEmptyView.swift in Sources */, 77204EA42A1CD59100BBDE4A /* GenericBorderedView.swift in Sources */, 84C6801824D7053B00006BF5 /* BorderedSubtitleActionView.swift in Sources */, + 0C66102F2A78E9D700E44634 /* RelaychainStartStakingState.swift in Sources */, 84468A0B286663E500BCBE00 /* CrossChainTransferSetupPresenter.swift in Sources */, 84A2C90C24E192F50020D3B7 /* ShakeAnimator.swift in Sources */, AEE0C43E272A8C16009F9AD5 /* AddChainAccount+AccountConfirmInteractor.swift in Sources */, @@ -18631,12 +19798,13 @@ 8472C5B4265CF9C500E2481B /* StakingRewardDestConfirmPresenter.swift in Sources */, 880059DC28EF092F00E87B9B /* SegmentedSliderView.swift in Sources */, 8402CC9C275B92AC00E5BF30 /* ControlView.swift in Sources */, + 0C66102B2A73816000E44634 /* StakingSharedStateFactory.swift in Sources */, 2AB7A7FF25CD0E8000767D87 /* GitHubPhishingAPIService.swift in Sources */, + 77F033972A8142D1006BC67E /* StakingSetupAmountStyles.swift in Sources */, AEF507F72625A3280098574D /* ValidatorState+Status.swift in Sources */, 8434B5FA298D284300FACF4C /* GovernanceBaseEditDelegationViewController.swift in Sources */, 843910D9253F8DFB00E3C217 /* StatusPresentable.swift in Sources */, 847C963D255351BB002D288F /* ScrollableContainerView.swift in Sources */, - 8463A71A25E3116A003B8160 /* BalanceViewModel.swift in Sources */, 842A736B27DB7A2E006EE1EA /* OperationDetailsViewModel.swift in Sources */, 8425EA9A25EA83FA00C307C9 /* ChainData+Value.swift in Sources */, 8887813E28B7AA3100E7290F /* RoundedIconTitleCollectionHeaderView.swift in Sources */, @@ -18646,21 +19814,24 @@ 8824D42629032B410022D778 /* BlurredView.swift in Sources */, 84585A2F251BFC8400390F7A /* TriangularedButton+Style.swift in Sources */, 84C6801A24D75E2A00006BF5 /* BorderedSubtitleActionView+Inspectable.swift in Sources */, + 0C2F868F2A725E4F00593C01 /* DefaultStakingRewardDestination.swift in Sources */, + 0C77B5672A837AC500B5AE08 /* StaticValidatorListWireframe.swift in Sources */, 84EB6C4E281999E100CFD8B2 /* PayoutTimeViewModelFactory.swift in Sources */, 8430AAD42602285B005B1066 /* StakingStateCommonData.swift in Sources */, F4F2296C260DBDCE00ACFDB8 /* StakingPayoutLabelTableCell.swift in Sources */, + 0C13D3242A823D810054BB6F /* StartStakingExtrinsicProxy.swift in Sources */, 8804AD89295B75F8001C4E09 /* Styles.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 */, 847DD8DC26034B99003DE053 /* LocalizableViewProtocol.swift in Sources */, 84C98A5729A153DA00F5328B /* DelegateVotedReferendaOperationFactory.swift in Sources */, F4CE0F9527343E630094CD8A /* AcalaContributionConfirmPresenter.swift in Sources */, 889F7F3B292373560024CB1E /* RemoteAssetModel+Evm.swift in Sources */, 849D3227291CE25E00D25839 /* MarkdownViewContainer.swift in Sources */, + 0C59E8E12AA60FF0001E11F3 /* CrowdloanExternalServiceFactory.swift in Sources */, 847297CF260B4035009B86D0 /* ChangeTargetConfirmInteractor.swift in Sources */, 8455F19C2A1DF088003F072D /* ChainsStore+Multistaking.swift in Sources */, 8428768724AE046300D91AD8 /* LanguageSelectionViewFactory.swift in Sources */, @@ -18696,22 +19867,25 @@ 849014DD24AA8F60008F705E /* MainTabBarViewFactory.swift in Sources */, AE7129C12608CAE7000AA3F5 /* NetworkStakingInfo.swift in Sources */, 849976C427B286AF00B14A6C /* MetamaskMessage.swift in Sources */, + 0CB261DB2A98943800287305 /* NPoolsUnstakeBaseError.swift in Sources */, 847A25BD28D7C0E7006AC9F5 /* TokenTransferedEvent.swift in Sources */, 8401F24F24E524900081D8F8 /* String+Helpers.swift in Sources */, F4113B3F260C77FF00DF4DBA /* StakingRewardPayoutsViewLayout.swift in Sources */, AEA0C8A8267B6B3200F9666F /* SelectedValidatorListPresenter.swift in Sources */, 840D8918262429C900AB231B /* LocalStorageRequestFactory.swift in Sources */, 8463A71F25E39E07003B8160 /* StorageProviderSource.swift in Sources */, - 8430AADC26022C58005B1066 /* NoStashState.swift in Sources */, 84F4A9182550331D000CF0A3 /* SecretSource.swift in Sources */, 84FD3DB12540C09800A234E3 /* TransactionHistoryMergeManager.swift in Sources */, 88C7165A28C8D3450015D1E9 /* LocksHeaderView.swift in Sources */, + 77799AE72A792B6F00B7E564 /* StakingTypeAccountViewModel.swift in Sources */, 840B3D6E289A56BA00DA1DA9 /* ParitySignerScanWireframe.swift in Sources */, F48EB5462722BB7000AE15ED /* AcalaBonusService.swift in Sources */, + 0C626D212A933A7D00CDAF4E /* StakingNPoolsViewModelFactory.swift in Sources */, 842E9E982A2A28A700759972 /* StakingDashboardProviderFactory.swift in Sources */, 845B07ED291594E1005785D3 /* DemocracyReferendum.swift in Sources */, 84466B4028B77B4500FA1E0D /* SignatureVerificationWrapper.swift in Sources */, 844DBC62274D1E29009F8351 /* SecretTypeTableViewCell.swift in Sources */, + 0CB261F92A9F1F2200287305 /* NPoolsRedeemError.swift in Sources */, 847297A2260B3146009B86D0 /* ChangeTargetsSelectValidatorsStartWireframe.swift in Sources */, 0C2F86802A7119D400593C01 /* AuraSessionLengthOperationFactory.swift in Sources */, 849D755B2756910A007726C3 /* RoundedView+Styles.swift in Sources */, @@ -18748,7 +19922,6 @@ 848CC93B28D9F6A5009EB4B0 /* OnChainScheduler.swift in Sources */, 84953F662934C7D90033F47D /* EtherscanERC20HistoryResponse.swift in Sources */, AEAC68F526E9F93B00346599 /* CoingeckoDefinitions.swift in Sources */, - 88FB7DD72951B1AF00784E08 /* WalletHistoryFilter+CallCodingPath.swift in Sources */, 8410DBCB26EA31DE00FE1738 /* AccountProviderFactory.swift in Sources */, 841E554F282E2C0300C8438F /* StakingParachainInteractor+InputProtocol.swift in Sources */, 84B1318C29ED70BF004EA1FF /* EvmFallbackGasLimit.swift in Sources */, @@ -18760,6 +19933,7 @@ 84FD91B229B08F8F007851D3 /* BaseTableSearchViewLayout.swift in Sources */, 847F2D4827A9DDC700AFD476 /* MultigradientView+Default.swift in Sources */, 8448F7A6288314250080CEA9 /* AssetListAssetsViewModelFactory.swift in Sources */, + 0C59E8F42AA7649E001E11F3 /* OperationDetailsBaseProvider.swift in Sources */, 848FFE8B25E69A6000652AA5 /* EraValidatorServiceProtocol.swift in Sources */, AEA0C8C12681180900F9666F /* InitBondingCustomValidatorListWireframe.swift in Sources */, 849976C127B2823F00B14A6C /* DAppMetamaskBaseState.swift in Sources */, @@ -18777,6 +19951,7 @@ 849014C124AA87E4008F705E /* LocalAuthProtocol.swift in Sources */, 841E6B0A25EC1C140007DDFE /* ValidatorOperationFactory.swift in Sources */, 8472C5B5265CF9C500E2481B /* StakingRewardDestConfirmProtocols.swift in Sources */, + 0C59E8F02AA76361001E11F3 /* OperationDetailsDataProviderProtocol.swift in Sources */, 84E83AA2286328660000B418 /* XcmOrmlTransfer.swift in Sources */, 84CEAAEF26D6DF160021B881 /* SingleToMultiassetMigrationPolicy.swift in Sources */, 842BDB23278C229D00AB4B5A /* DAppBrowserStateDataSource.swift in Sources */, @@ -18850,8 +20025,10 @@ 84BAD20D293A1B6E00C55C49 /* TopCustomSearchView.swift in Sources */, 84A58FD928A10ABD003F6ABF /* MultipartQrOperationFactory.swift in Sources */, 84E2589D2892B41500DC8A51 /* WalletSwitchViewModelFactory.swift in Sources */, + 0CB261E02A98BEBD00287305 /* NPoolsUnstakeBaseInteractor.swift in Sources */, 84AE7AA927D3700E00495267 /* StackTableCell.swift in Sources */, 84CFF1E526526FBC00DB7CF7 /* StakingBondMoreViewController.swift in Sources */, + 77F0339B2A814505006BC67E /* StakingSelectionMethod.swift in Sources */, 84E1CD11260DCD44001E81B5 /* SwitchAccount+AccountImportWireframe.swift in Sources */, 8401AEC72642A71D000B03E3 /* StakingRebondConfirmationPresenter.swift in Sources */, 885547E829C8908D008782C1 /* KiltWeb3n+StoragePath.swift in Sources */, @@ -18868,6 +20045,7 @@ 8430AB1726023D2D005B1066 /* BaseStashNextState.swift in Sources */, 84A6AB64290B021E001B57B2 /* CopyPresentable.swift in Sources */, 841AAC2126F6860B00F0A25E /* AssetBalanceFormatterFactory.swift in Sources */, + 0C77B5652A8374EA00B5AE08 /* StaticValidatorListPresenter.swift in Sources */, 8418167528251BBC0007684A /* StorageListSyncResult.swift in Sources */, 84DD261929ACA9880032A598 /* BagList+CodingPath.swift in Sources */, 8465DA3F298EEC6C00C7CFF1 /* GovernanceAddDelegationTracksInteractor.swift in Sources */, @@ -18877,12 +20055,14 @@ 88C5F07C297EE79C001CCADE /* Release.swift in Sources */, 845B89222959620000EE25B0 /* SecurityLayerPresenter.swift in Sources */, 849E07F4284A04F400DE0440 /* ParaStkAccountSubscribeHandlingFactory.swift in Sources */, + 0C59E8D82AA60490001E11F3 /* ExternalAssetBalanceStreambleSource.swift in Sources */, 84BC7047289EFFFA008A9758 /* ChainWalletDisplayAddress.swift in Sources */, 843910CC253F7E6500E3C217 /* SubstrateDataModel.xcdatamodeld in Sources */, 84BB3CF3267D24B000676FFE /* UIStackView+Manage.swift in Sources */, 8419C39626EA8F7300A3179C /* PriceData.swift in Sources */, 84D17EDC28054C9C00F7BAFF /* DAppLocalSubscriptionHandler.swift in Sources */, 84E25BE627E750D500290BF1 /* AccountInputViewModel+Handler.swift in Sources */, + 0C9525EB2A7B7F5000BD724D /* ChainModel+Additional.swift in Sources */, 8407715A28CBD990007DBD24 /* ParaStkYieldBoostCancelInteractor.swift in Sources */, 849014E024AA8F60008F705E /* MainTabBarWireframe.swift in Sources */, AE6F7FE42685E812002BBC3E /* ValidatorListFilterViewFactory.swift in Sources */, @@ -18900,7 +20080,7 @@ 84EE2FA72891207500A98816 /* WalletManageViewLayout.swift in Sources */, 8467FCFC24E5C3BD005D486C /* URLHandlingService.swift in Sources */, 8428228D289B2D5300163031 /* BaseUsernameSetupPresenter.swift in Sources */, - 842A736427DB31A3006EE1EA /* OperationRewardModel.swift in Sources */, + 842A736427DB31A3006EE1EA /* OperationRewardOrSlashModel.swift in Sources */, 0C5364A02A4D6EB700990478 /* AssetListBuilder.swift in Sources */, 8487584727F1816300495306 /* QRExtractionService.swift in Sources */, 849013DB24A927E2008F705E /* Logger.swift in Sources */, @@ -18933,7 +20113,8 @@ 84D2F19D2771E5610040C680 /* ExtrinsicBuilder+Signing.swift in Sources */, 8430AAFB260230C5005B1066 /* ValidatorState.swift in Sources */, 8424308D265B1814003E07EC /* CrowdloanOperationFactory.swift in Sources */, - 880855F628D09A3C004255E7 /* CrowdloanContributionStreamableSource.swift in Sources */, + 77EFFC8D2A6EECFD009E28F8 /* StakingAmountViewModelFactory.swift in Sources */, + 0C7C88682A95563100DD96A1 /* StakingClaimableRewardView.swift in Sources */, 0C9C642D2A8CE30A004DC078 /* SystemAccountValidating.swift in Sources */, 88D02FF42943207400E26390 /* BigInt+Decimal.swift in Sources */, 847999B82889510C00D1BAD2 /* TextInputViewDelegate.swift in Sources */, @@ -18967,12 +20148,12 @@ 842A738C27DE576C006EE1EA /* NovaTintImageProcessor.swift in Sources */, 84FA835A265CE5BE00FDF727 /* TitleMultiValueView.swift in Sources */, 849014BB24AA87E4008F705E /* PinSetupWireframe.swift in Sources */, + 0C77B55F2A83717000B5AE08 /* StaticValidatorListViewController.swift in Sources */, AEE4E34825E90C6000D6DF31 /* RewardCalculatorService.swift in Sources */, F40D0AF2260CCE5800CBD43B /* StakingPayoutBaseTableCell.swift in Sources */, 8490145824A9406D008F705E /* LegalData.swift in Sources */, 8472C601265D7A1F00E2481B /* WebSocketProviderSource.swift in Sources */, 8425EA9025EA7E5800C307C9 /* ElectedValidatorInfo.swift in Sources */, - 7728E5932A13290D007901E0 /* GenericLens.swift in Sources */, 84216FCF28264A1E00479375 /* ParaStakingRewardCalculatorEngine.swift in Sources */, 84DA03D62759341200E8B326 /* ChainAccountControl.swift in Sources */, 84329ED02832461D0020BC1C /* TimeInterval+Localization.swift in Sources */, @@ -18980,6 +20161,7 @@ 84468A072866530100BCBE00 /* AssetStorageInfoOperationFactory.swift in Sources */, 84744953289268770042FD80 /* WalletSwitchViewModel.swift in Sources */, 84FACCD925F8C22A00F32ED4 /* BigInt+Hex.swift in Sources */, + 77799AF02A7CFB7C00B7E564 /* ValidatorViewModel.swift in Sources */, 849F33BC29F7C659001AEFA4 /* DAppInteractionProtocols.swift in Sources */, F409672626B29B04008CD244 /* UIScrollView+ScrollToPage.swift in Sources */, 84E0EE0E292D69A9008B2953 /* CallMetadata+TypeCheck.swift in Sources */, @@ -19004,7 +20186,9 @@ 8849C5ED29806F4500DE35CC /* VersionTableViewCell.swift in Sources */, 84EC2D1D276C684D009B0BE1 /* PolkadotExtensionSignerResult.swift in Sources */, 84F5105B263AB9F2005D15AE /* NetworkFeeView.swift in Sources */, + 0CC6C8D82AAB401200AD8D9B /* CustomValidatorsFullList.swift in Sources */, 84786E2425FBA2A50089DFF7 /* StakingAccountSubscription.swift in Sources */, + 0CB261E22A9B215B00287305 /* NominationPoolUnstake.swift in Sources */, 84754C852510A1A400854599 /* ModalAlertPresenting.swift in Sources */, 8494424A265306BD0016E7BD /* ChangeRewardDestinationViewModel.swift in Sources */, 84A7AC8029465BF9001A39CF /* TokenAddErrorPresentable.swift in Sources */, @@ -19012,6 +20196,7 @@ 845B821526EF657700D25C72 /* PersistentValueSettings.swift in Sources */, F4DCAE4726207EF900CCA6BF /* PayoutRewardsServiceProtocol.swift in Sources */, 848919902A052C18008D57A3 /* ModalNetworksFactory.swift in Sources */, + 77F0339D2A837AB3006BC67E /* StakingTypeSelection.swift in Sources */, 8463A6F925E2F82E003B8160 /* CDSingleValue+CoreDataCodable.swift in Sources */, 84ED6BE6286995F400B3C558 /* TransferCrossChainConfirmPresenter.swift in Sources */, 84EBFCE7285E7A7C0006327E /* XcmMessage.swift in Sources */, @@ -19020,6 +20205,7 @@ 8493D3E927059B6700157009 /* StakingServiceFactory.swift in Sources */, 887C44FB29AC7B8700950F98 /* DelegateSingleVoteHeader.swift in Sources */, 84729758260AA519009B86D0 /* SelectValidatorsConfirmInteractorBase.swift in Sources */, + 0C893E6D2A6562B400781503 /* NominationPools.swift in Sources */, 84BC7049289F10AE008A9758 /* WalletAccountInfoView+Style.swift in Sources */, 848B59BE28BCB4C80009543C /* LedgerAddAccountConfirmationWireframe.swift in Sources */, 846CD24D2656FEB800A2E4B6 /* StorageKeysQueryService.swift in Sources */, @@ -19030,6 +20216,7 @@ 84BD388D28A25CB800A9918E /* UILabel+ExpirationTimer.swift in Sources */, 841E554D282DAA8000C8438F /* ParachainStakingServiceFactory.swift in Sources */, 84D8F16324D8194100AF43E9 /* TitleWithSubtitleTableViewCell.swift in Sources */, + 0C9C64322A8D67A0004DC078 /* StakingNPoolsInteractor.swift in Sources */, F462B35C260C86880005AB01 /* ViewHolder.swift in Sources */, 8846F73E29D7561100B8B776 /* Data+base36.swift in Sources */, 84948C36287DD1C800E6DD3E /* NftListRMRKV2ViewModel.swift in Sources */, @@ -19042,7 +20229,6 @@ 0C3205C62A877594002EB914 /* EvmGasPriceWithFallbackProvider.swift in Sources */, 8470D6D4253E35F0009E9A5D /* StorageUpdate.swift in Sources */, 8460E715284AC0AA002896E9 /* ParaStkBaseUnstakeInteractor.swift in Sources */, - 84C2F27D25E297350050A4AD /* CalculatedReward.swift in Sources */, 84355CEE28B614A7004E5C5E /* MessageSheetImageGraphicsView.swift in Sources */, 849E689526AF388500E0E7BE /* ElectedValidatorInfo+Selected.swift in Sources */, 8860F3E4289D50BA00C0BF86 /* Array+SectionProtocol.swift in Sources */, @@ -19058,6 +20244,7 @@ 84FBED0329279CF200FBEB83 /* ContractTransactionHistoryUpdater.swift in Sources */, 84893BFE24DA0000008F6A3F /* FieldStatus.swift in Sources */, 84F51053263AB440005D15AE /* StakingUnbondSetupLayout.swift in Sources */, + 77F033952A8142B0006BC67E /* StakingTypeValidatorView.swift in Sources */, 84BC704B289F1338008A9758 /* ExpirationTimeViewModel.swift in Sources */, 849014BF24AA87E4008F705E /* ScreenAuthorizationProtocol.swift in Sources */, 84CB2250270360AC0041C8C1 /* StakingLocalSubscriptionFactory.swift in Sources */, @@ -19073,14 +20260,16 @@ 84CEAAF526D7ADF20021B881 /* KeystoreMigrator.swift in Sources */, 8428765924ADDE0200D91AD8 /* SettingsProtocols.swift in Sources */, 8499FECA27BF8D2200712589 /* NftModel.swift in Sources */, + 0CAC44AA2A7A79C2001EDE61 /* StartStakingInfoParachainWireframe.swift in Sources */, + 0C893E6F2A65702A00781503 /* NominationPools+CodingPath.swift in Sources */, 841E554B282DA66700C8438F /* StakingParachainProtocols.swift in Sources */, AEE5FB0326415E40002B8FDC /* StakingRebondSetupWireframe.swift in Sources */, 0C3205D22A895E85002EB914 /* EvmDefaultGasLimitProvider.swift in Sources */, 84D8F15D24D8178000AF43E9 /* IconWithTitleViewModel.swift in Sources */, 844DB61F262D9C070025A8F0 /* ChainHistoryRange.swift in Sources */, - 842A737127DB7EF1006EE1EA /* OperationSlashViewModel.swift in Sources */, 84F1D66F29066F740050F4E3 /* ReferendumVotesViewModel.swift in Sources */, 84C5ADE22813E8C1006D7388 /* GradientBannerInfoView.swift in Sources */, + 77CC82A72A986CF1002D022F /* StakingSelectValidatorsDelegate.swift in Sources */, 8490142C24A935FE008F705E /* ErrorPresentable.swift in Sources */, 84DF21A5253473B0005454AE /* ModalInfoFactory.swift in Sources */, 84AF378A272349DA007408D6 /* SiDataTypeMapper.swift in Sources */, @@ -19103,9 +20292,11 @@ 8499FEE027C0AE3200712589 /* KnownChainIds.swift in Sources */, 84FF267E28494B2B003EC78D /* ParaStkStateSetupPresenter+StakeMore.swift in Sources */, 84F4386125D9A83100AEDA56 /* TimeInterval+Time.swift in Sources */, + 0CB06E732A6800F500C7EC99 /* NominationPools+Functions.swift in Sources */, 844EFB5F265FCE180090ACB1 /* CrowdloanContributionInteractor.swift in Sources */, 8499FE6627BD15C800712589 /* DistributedUrlParser.swift in Sources */, 845BB8C025E4508800E5FCDC /* SS58FactoryExtensions.swift in Sources */, + 0C59E8E72AA61933001E11F3 /* ExternalAssetBalanceSubscriptionHandler.swift in Sources */, F461740A260DFD3600E8FA3D /* StakingPayoutConfirmationViewLayout.swift in Sources */, 84E25BF027E8EFB500290BF1 /* SubqueryAccumulateReward.swift in Sources */, 88AC5ADA2948A8CC0056DD40 /* TransactionSectionModel.swift in Sources */, @@ -19149,9 +20340,11 @@ 845532D02684690D00C2645D /* ParachainSlotLease.swift in Sources */, 840874DE29782DD400ACFA55 /* GovernanceDelegateMetadataFactory.swift in Sources */, 77E0DC9E2A6940C400D03724 /* Calendar+Helpers.swift in Sources */, + 0C626D1D2A915E2F00CDAF4E /* StakingNominationPoolsStatics.swift in Sources */, 846A835A28B8C5EC00D92892 /* TransactionExpiredPresentable.swift in Sources */, 842EBB352890A79500B952D8 /* WalletSelectionViewFactory.swift in Sources */, 844C3E602A0635DE00C4305F /* QRScanViewDisplayParams.swift in Sources */, + 0C13D3042A7D56CC0054BB6F /* StakingRecommendationMediatorFactory.swift in Sources */, 8407715828CB802C007DBD24 /* ParaStkYieldBoostBaseProtocols.swift in Sources */, 84DB4E1E25E93B1700A6DF41 /* Identity.swift in Sources */, F47F5A822626FDB9009BCFF4 /* StakingPayoutViewModel.swift in Sources */, @@ -19193,6 +20386,7 @@ AEE4E34D25E915ED00D6DF31 /* RewardCalculatorServiceProtocol.swift in Sources */, 8499FEE627C105DB00712589 /* RMRKV2SyncService.swift in Sources */, 849013DD24A927E2008F705E /* KeystoreExtensions.swift in Sources */, + 0CF193D72A861D7E003F12F6 /* StartStakingInfoConstants.swift in Sources */, 846FB02C28C74F9700CA5444 /* ParaStkYieldBoostState.swift in Sources */, 844B2E7C27C426A7000CC079 /* NftLocalSubscriptionHandler.swift in Sources */, 8487584B27F1834E00495306 /* ImageGalleryPresentable.swift in Sources */, @@ -19207,6 +20401,7 @@ 842A738627DE04F1006EE1EA /* TransactionLocalSubscriptionFactory.swift in Sources */, 8489A6DA27FDC49D0040C066 /* StakingUnbondingsView.swift in Sources */, 846952A42852A1640083E0B4 /* StakingDuration.swift in Sources */, + 0CB261E72A9C7C9D00287305 /* NPoolsUnstakeHintsFactory.swift in Sources */, 84D184EC2A04DA810060C1BD /* GlowingStatusView.swift in Sources */, 8428765F24ADE0BB00D91AD8 /* UserSettings.swift in Sources */, 84FAB0652542CA4200319F74 /* CDContactItem+CoreDataDecodable.swift in Sources */, @@ -19222,6 +20417,7 @@ F436BB9E2726FBF7004B1794 /* Coordinator.swift in Sources */, 848F5FE1298911B80058CD74 /* SubqueryDelegationsOperationFactory.swift in Sources */, 84BB3CF8267D276D00676FFE /* CrowdloanTableViewCell.swift in Sources */, + 0C962F8A2AA8614500C0B551 /* TransactionHistoryLocalFilterFactory.swift in Sources */, 844AE53C2861B3BC0020ECBC /* XcmTransferService.swift in Sources */, 2A9F8D52274E4EC4003720E0 /* AccountCreateViewController.swift in Sources */, 84113B91255B2CA0009BD21A /* MainTransitionHelper.swift in Sources */, @@ -19241,7 +20437,7 @@ 84720730277C335000F593DD /* DAppListFlowLayout.swift in Sources */, AE2C84DF25EF98BA00986716 /* AnyValidatorInfoInteractor.swift in Sources */, 8490142E24A935FE008F705E /* LoadableViewProtocol.swift in Sources */, - 88CD321028E2137300542F0D /* CrowdloanContributionId.swift in Sources */, + 88CD321028E2137300542F0D /* ExternalBalanceContribution.swift in Sources */, 84F2FF0725E7AF8F008338D5 /* EraValidatorInfo.swift in Sources */, 88D841D429436572008A5A0C /* RoundedButton+Styles.swift in Sources */, 84E25BF627E9A51D00290BF1 /* LeaseParam.swift in Sources */, @@ -19253,11 +20449,13 @@ 847A25C628D84BE2006AC9F5 /* Referenda.swift in Sources */, 0C17BD9B2A43025E004AF9E7 /* Pagination.swift in Sources */, 88D02FE82942EB1A00E26390 /* AssetDetailsModel.swift in Sources */, + 77F033A22A84E00F006BC67E /* StakingPoolView.swift in Sources */, 88421055289BBA8D00306F2C /* CurrencyViewLayout.swift in Sources */, 845C407D2702812E00BFA50B /* StakingAccountUpdatingService.swift in Sources */, 8430D6C92801A2B500FFB6AE /* WebSocketProtocols.swift in Sources */, 8459A9CC2746A1E9000D6278 /* CrowdloanOffchainSubscriptionHandler.swift in Sources */, 84D331AF2519E8080078D044 /* TriangularedView+Style.swift in Sources */, + 0C13D2F52A7D2B440054BB6F /* DirectStakingRecommendationMediator.swift in Sources */, 84C74365251E4D60009576C6 /* SigningWrapperProtocol.swift in Sources */, 88AF35DE28C21D28003730DA /* LocksSubscription.swift in Sources */, 8434B5FE298D2CC900FACF4C /* GovernanceBaseEditDelegationProtocols.swift in Sources */, @@ -19273,15 +20471,19 @@ 84C1B98B24F55E8200FE5470 /* AccountTableViewCell.swift in Sources */, 8487580927EDEDB200495306 /* RawChainView.swift in Sources */, 84D184F82A04F9D60060C1BD /* WalletConnectSessionViewModelFactory.swift in Sources */, + 0CC2E55E2A6AB2B7004092E7 /* StashItemMapper.swift in Sources */, 84CA68E126BEAC7C003B9453 /* SpecVersionSubscriptionFactory.swift in Sources */, 842BDB21278C222100AB4B5A /* DAppBrowserBaseState.swift in Sources */, DAB29F2A9C864D7FCF1AF934 /* UsernameSetupProtocols.swift in Sources */, 84D1111126B932480016D962 /* AssetModel.swift in Sources */, + 779C8BE82AA1DD1B001A4A3C /* NominationPoolsBondMoreHintsFactory.swift in Sources */, 84C1FA16278C9BB1008B2711 /* DAppBrowserAccountSubscribingState.swift in Sources */, 84C5ADE628141D21006D7388 /* LinkView.swift in Sources */, + 0C59E8DF2AA60DAB001E11F3 /* ExternalAssetBalanceServiceFactoryProtocol.swift in Sources */, 84C2064028D1EAD2006D0D52 /* AccountAssetBalanceTrigger.swift in Sources */, 8469D5A628F5E8F20074FEE3 /* Staking.swift in Sources */, 848FFE8325E686C200652AA5 /* StorageDecodingOperation.swift in Sources */, + 0C13D3002A7D50C10054BB6F /* PoolStakingRecommendationMediator.swift in Sources */, 8412219E28F0514400715C82 /* ReferendumsMetadataPreviewProviderSource.swift in Sources */, 8488ECDF258CE118004591CC /* PurchaseCompleted.swift in Sources */, 840302E4292CFCF90013F356 /* AssetSelectionBasePresenter.swift in Sources */, @@ -19351,6 +20553,7 @@ 84E8BA2229FFB38600FD9F40 /* EthereumTransaction.swift in Sources */, 846A2601267C768500429A7F /* CrowdloanContributionMapper.swift in Sources */, 84CFF1EA26526FBC00DB7CF7 /* StakingBondMoreConfirmationViewFactory.swift in Sources */, + 77EFFC8A2A6E7A24009E28F8 /* AccountExistense.swift in Sources */, 8466BB472640152A00E065A8 /* StakingUnbondConfirmViewModelFactory.swift in Sources */, 84CE6A312566800000559427 /* ByteLengthProcessor+Username.swift in Sources */, 84B24F9C2A2E297000F9BF59 /* StakingDashboardViewModel.swift in Sources */, @@ -19416,6 +20619,7 @@ 88D1F1B4298127F600316A1A /* InAppUpdatesInteractorError.swift in Sources */, 846C372E26B199D10098F303 /* BabeStakingDurationFactory.swift in Sources */, 9B4F0484B81BBF8DFA618599 /* AccountCreateViewFactory.swift in Sources */, + 0C13D3072A7FB92C0054BB6F /* NominationPoolsJoin.swift in Sources */, 845B811728F43C730040CE84 /* TreasuryProposal.swift in Sources */, 8460E718284CF124002896E9 /* ParachainStakingValidatorFactoryProtocol.swift in Sources */, 846CDECD258D212D009F3E75 /* AlertImageWithTitleView.swift in Sources */, @@ -19443,7 +20647,6 @@ 2A028D2A275688E80061CB4C /* AddChainAccount+AccountCreatePresenter.swift in Sources */, 84EFB78928AB7654003B8396 /* LedgerError.swift in Sources */, 840DCBF125DFEE4800D45C6A /* AmountInputView.swift in Sources */, - 84C2F27725E296CD0050A4AD /* RewardDestinationViewModelFactory.swift in Sources */, 84D97EC82520D32000F07405 /* PolkadotIcon+Image.swift in Sources */, 84C2802126F541DE006E8014 /* WebSocketEngine+Connection.swift in Sources */, 840D55972A0513CF0025D91C /* DAppNetworksViewModelFactory.swift in Sources */, @@ -19454,11 +20657,11 @@ 84DD5F26263D72C400425ACF /* ExtrinsicFeeProxy.swift in Sources */, 77A6F5CB2A30BD71004AFD1A /* BaseKiltTransferAssetRecipientRepository.swift in Sources */, 8442003628EA9DF100C49C4A /* VoteViewFactory.swift in Sources */, + 770F57882A8A2CE0005FD7C1 /* StakingSelectPoolViewStyles.swift in Sources */, 84BC7041289DBF62008A9758 /* QRDisplayView.swift in Sources */, 2AC7BC7E2731604C001D99B0 /* ChainAccountChanged.swift in Sources */, 845B07F329159C15005785D3 /* Democracy+CodingPath.swift in Sources */, 847C96492553614F002D288F /* ExportRestoreJsonPresenter.swift in Sources */, - 8463A71225E30C95003B8160 /* BalanceViewModelFactory.swift in Sources */, 84A04622277DE83E000B24DA /* DAppListErrorView.swift in Sources */, 6D315EFF2B664235D297674E /* AccountImportProtocols.swift in Sources */, F43A8A9E265BA03500A8A5A8 /* CustomValidatorListViewModel.swift in Sources */, @@ -19472,6 +20675,7 @@ 7D281FEA78E2E5F44990C184 /* AccountImportPresenter.swift in Sources */, 6C56AB4AE63AB2DC73DE98E0 /* AccountImportInteractor.swift in Sources */, F4EAC7972642E0D800FBDDC3 /* ControllerAccountViewModelFactory.swift in Sources */, + 775F19512A5811FA009915B6 /* StartStakingParachainInteractor.swift in Sources */, 8828F4F328AD2734009E0B7C /* CrowdloansCalculator.swift in Sources */, 8487583627F06AF300495306 /* QRScannerViewController.swift in Sources */, 846B749A28B4BA1700C39B93 /* LedgerChainAccount.swift in Sources */, @@ -19484,6 +20688,7 @@ 84E8BA2A2A00EF4C00FD9F40 /* XcmBaseMetadataQueryFactory.swift in Sources */, 8454C21D2632A78900657DAD /* EventRecord.swift in Sources */, 84AE7AB527D39DCA00495267 /* NetworkViewModel.swift in Sources */, + 0C9C64342A8D67AF004DC078 /* StakingNPoolsWireframe.swift in Sources */, 84C4C2F9255DB9510045B582 /* PinChangeInteractor.swift in Sources */, 840874D82978284B00ACFA55 /* GovernanceDelegateMetadataRemote.swift in Sources */, 8473F4B2282BD584007CC55A /* StakingRelaychainPresenter.swift in Sources */, @@ -19509,7 +20714,6 @@ AEA0C8BC2681140700F9666F /* YourValidatorList+CustomList.swift in Sources */, 88840D8C29DEDCD3002EFFFD /* KiltTransferAssetRecipientError.swift in Sources */, 84E83AA428632AF50000B418 /* XcmPalletTransfer.swift in Sources */, - 880855FC28D0C3DF004255E7 /* CrowdloansLocalStorageSubscriber.swift in Sources */, 8455F1982A1DD74D003F072D /* SubqueryFilter.swift in Sources */, 8499FE6827BD1EA600712589 /* DistributedStorageFactory.swift in Sources */, 8428228A289B1E5C00163031 /* TableHeaderLayoutUpdatable.swift in Sources */, @@ -19550,6 +20754,7 @@ 848EAEB02659310A00676CEA /* CrowdloanStatus.swift in Sources */, 7796C7052A17859300D56094 /* ReferendumEmptySearchTableViewCell.swift in Sources */, 84F18D4E27A18C1400CA7554 /* OrmlAccountSubscription.swift in Sources */, + 7726CD552A9728D700CE9064 /* StakingTypeSelectedStakingViewModelFactory.swift in Sources */, 8499FED427BFAA0000712589 /* BaseNftSyncService.swift in Sources */, F419FD7A273D05B00061652C /* SettingsSection.swift in Sources */, DAEE468553039B3600F64A0E /* AccountManagementWireframe.swift in Sources */, @@ -19567,6 +20772,7 @@ 84B8AA7329F8EFC700347A37 /* DAppInteractionError.swift in Sources */, 8485035029FBC84300AE6909 /* WalletConnectSignModelFactory.swift in Sources */, 849E17F02791909C002D1744 /* DAppSettings.swift in Sources */, + 0C893E6A2A65591C00781503 /* PoolsMultistakingUpdateService.swift in Sources */, 84DBEA7729DAF5EC00A504A7 /* ConnectionNodeSwitchCode.swift in Sources */, 884048D428C723F00085FFA6 /* OrmlTokenSubscriptionHandlingFactory.swift in Sources */, 846A2C4D2529FBB700731018 /* NSPredicate+Filter.swift in Sources */, @@ -19578,12 +20784,14 @@ 843910B2253ED4D100E3C217 /* CDChainStorageItem+CoreDataDecodable.swift in Sources */, 844DBC64274D2BD6009F8351 /* ModalPickerClosureContext.swift in Sources */, 8451720D298C473500489EF1 /* GovernanceSelectableTrackView.swift in Sources */, + 0C59E8DC2AA60C3B001E11F3 /* NSPredicate+ExternalAssetBalance.swift in Sources */, 84CFF1EE26526FBC00DB7CF7 /* StakingBondMoreConfirmationProtocols.swift in Sources */, 8499FE7B27BE58A000712589 /* IdentityMapper.swift in Sources */, 845B89262959627A00EE25B0 /* SecurityLayerWireframe.swift in Sources */, 8489A6D227FD5FB80040C066 /* StackActionCell.swift in Sources */, F429324F26280F6B00752C2C /* StakingRewardDetailsViewModel.swift in Sources */, F4A11B5A272FEB0B0030E85B /* CrowdloanYourContributionsCell.swift in Sources */, + 0C79C8992A7BE46A00B171E3 /* AssetModel+Staking.swift in Sources */, 8846F72329D6B9DB00B8B776 /* Data+base2.swift in Sources */, 848E6BDF276218E600C91022 /* GlowingView.swift in Sources */, 843910B6253EE62B00E3C217 /* DataProviderChange+Result.swift in Sources */, @@ -19601,11 +20809,13 @@ 8440F4A22959B63300CAFBF9 /* SecuredApplicationHandlerProxy.swift in Sources */, 2AC7BC8B273435CE001D99B0 /* BottomSheetInfoTableCell.swift in Sources */, 844304622A28C9FA00DE36DE /* MultistakingSyncState.swift in Sources */, + 0C626D1F2A92AA0F00CDAF4E /* NominationPoolsDataProviding.swift in Sources */, 8446F5EC2817107D00B7A86C /* TitleAmountView.swift in Sources */, 84CEAAF726D7B8010021B881 /* SettingsMigrator.swift in Sources */, 84ABB32B2A16146600B5E95A /* HistoryItemTableViewCell.swift in Sources */, F4D0546B2729949100210294 /* MoonbeamMakeSignatureResponse.swift in Sources */, D9046DBA27451ED700C29F2E /* ParallelContributionSource.swift in Sources */, + 0CE629DE2AA9B6BF00E250BD /* RewardDestinationViewModelFactory.swift in Sources */, 844DB624262D9C710025A8F0 /* ErasRewardDistribution.swift in Sources */, 84FAB0632542C8D600319F74 /* ContactItem.swift in Sources */, 06590486EED4050BADDD32C5 /* AccountManagementPresenter.swift in Sources */, @@ -19631,6 +20841,7 @@ 5678BAE4B652C5C5E4284F28 /* AccountManagementViewFactory.swift in Sources */, 84CFE441292B8CDA00CDDD7C /* EvmOnChainTransferSetupInteractor.swift in Sources */, 849528E326036997009DC845 /* RewardEstimationView.swift in Sources */, + 77864F4C2A6AC5A100FA7BA7 /* TitleHorizontalMultiValueView+Bind.swift in Sources */, 88784E9D29BF966B004489D5 /* Web3NamesOperationFactory.swift in Sources */, 847999B628894FE200D1BAD2 /* AccountInputViewDelegate.swift in Sources */, F474D388260CBEE600013699 /* StakingRewardDetailsViewLayout.swift in Sources */, @@ -19654,13 +20865,16 @@ 8443CA4A289D04E900FA4A95 /* TransactionSigningPresenting.swift in Sources */, 8475B47B289870A4009B90BC /* ProcessStepView.swift in Sources */, 8465DA41298F05D500C7CFF1 /* GovernanceTrackViewModelFactory.swift in Sources */, + 0CB261EF2A9E103900287305 /* NPoolsClaimRewardsStrategy.swift in Sources */, AE3983A5272C0BC800BC8A85 /* ImportChainAccount+AccountImportPresenter.swift in Sources */, 842EBB372890A7BA00B952D8 /* WalletSelectionWireframe.swift in Sources */, 8436C79F29ACD6D70024B409 /* ElectionProviderMultiPhase+CodingPath.swift in Sources */, + 0C79C89C2A7BE6A200B171E3 /* DirectStakingRecommendationFactory.swift in Sources */, 848B59C228BCC1E60009543C /* LedgerAddAccountConfirmationInteractor.swift in Sources */, 882A5CED28AFCE3600D0D798 /* ReturnInIntervalsViewModel.swift in Sources */, 88C7165428C894510015D1E9 /* CollectionViewDelegate.swift in Sources */, 840302DE292CE3EA0013F356 /* StorageEntryMetadata+TypeCheck.swift in Sources */, + 0CC2E5682A6E64FD004092E7 /* NPoolsLocalStorageHandler.swift in Sources */, F458D3982642911B0055CB75 /* ControllerAccountViewModel.swift in Sources */, 84DAC198268D3DD9002D0DF4 /* SNAddressType.swift in Sources */, 846B32A126DCDB7300250E89 /* SubqueryTotalRewardSource.swift in Sources */, @@ -19672,7 +20886,6 @@ 847C965425536199002D288F /* ExportRestoreJsonProtocols.swift in Sources */, 844CB56826F9BF9800396E13 /* CrowdloanLocalSubscriptionFactory.swift in Sources */, 84F9D4CF2664CCA500F7AAD2 /* CommonInputView.swift in Sources */, - 84786E1025FA20D30089DFF7 /* StakingAccountResolver.swift in Sources */, 849DEBDC25ED134A00C64C19 /* SelectValidatorsConfirmViewModelFactory.swift in Sources */, 84F1CB2F27CE50B50095D523 /* NftListMetadataViewModel.swift in Sources */, 8496ADDA276AFF4600306B24 /* DAppBrowserScript.swift in Sources */, @@ -19690,6 +20903,7 @@ F41CEB88272FFCB700C06154 /* CrowdloanContributionViewModel.swift in Sources */, 8466781327EC5446007935D3 /* MultilineBalanceView.swift in Sources */, 845B822926F0BB6700D25C72 /* CrowdloanChainSettings.swift in Sources */, + 0C13D2FA2A7D473B0054BB6F /* DirectStakingRestrictionsBuilder.swift in Sources */, AEE5FB072641669B002B8FDC /* StakingRebondSetupLayout.swift in Sources */, 847119C0262EF3BD00716580 /* PayoutValidatorsFactoryProtocol.swift in Sources */, 843910DE253F912500E3C217 /* ApplicationStatusView.swift in Sources */, @@ -19701,6 +20915,7 @@ 84F30EA125FD3EE700039D09 /* ChildSubscriptionFactory.swift in Sources */, 84D2F1A027730E6D0040C680 /* DAppOperationConfirmViewModel.swift in Sources */, 84FFE45928620716002432BB /* XcmTransferParties.swift in Sources */, + 0C13D2FE2A7D4F500054BB6F /* PoolStakingRestrictionsBuilder.swift in Sources */, 842EBB3D2891129C00B952D8 /* WalletSelectionViewLayout.swift in Sources */, 840D627729CB487100D5E894 /* EnviromentVariables.swift in Sources */, 8401AEC12642A71D000B03E3 /* StakingRebondConfirmationViewModel.swift in Sources */, @@ -19725,6 +20940,7 @@ 846A682C274693F700D1A47A /* CrowdloanExternalContributionSource.swift in Sources */, AEAC690026EA729300346599 /* CoingeckoPriceData.swift in Sources */, 84C34204283124C600156569 /* StakingParachainWireframe.swift in Sources */, + 0C59E8D12AA5FAC5001E11F3 /* PooledAssetBalance.swift in Sources */, 84953F70293615D20033F47D /* AssetHistoryFactoryFacade.swift in Sources */, 84F13F1C26F2B8C2006725FF /* JSONRPCError+Presentable.swift in Sources */, 84BAFCD226AF5EE700871E86 /* IconDetailsView.swift in Sources */, @@ -19749,9 +20965,8 @@ 849E17E327913220002D1744 /* DAppSearchResult.swift in Sources */, 845B07EB29159190005785D3 /* Democracy.swift in Sources */, 844CB57626FA064700396E13 /* ChainRegistryError.swift in Sources */, - 842A737327DB7F75006EE1EA /* OperationRewardViewModel.swift in Sources */, + 842A737327DB7F75006EE1EA /* OperationRewardOrSlashViewModel.swift in Sources */, 84CA68CF26BD6872003B9453 /* RuntimeSyncService.swift in Sources */, - 84EA0B2A25E579DF00AFB0DC /* AssetBalanceViewModel.swift in Sources */, 84880C462902781E00CADB06 /* ReferendumVotingLocal.swift in Sources */, 84CEAAF326D6ED870021B881 /* KeystoreTag.swift in Sources */, 840DCBF625E0059D00D45C6A /* AmountInputView+Inspectable.swift in Sources */, @@ -19774,6 +20989,7 @@ 84C1DBBA29C0A11200F295A5 /* XcmTransferService+Fee.swift in Sources */, 845B081529190056005785D3 /* Gov1UnlockReferendum.swift in Sources */, 84B24FB02A2F7B6F00F9BF59 /* StakingDashboardMoreOptionsCell.swift in Sources */, + 77799ADD2A74219A00B7E564 /* ButtonState.swift in Sources */, 84BAD222293C655900C55C49 /* TokensManageViewModelFactory.swift in Sources */, 84F3B27427F4130800D64CF5 /* PhishingSites.swift in Sources */, 8430AAF12602306A005B1066 /* BondedState.swift in Sources */, @@ -19782,6 +20998,7 @@ 84333BD7285682EC00C76A4F /* ValidatorsSelectionParams.swift in Sources */, 84C3F77B2601F08B00D47501 /* NominationViewModel.swift in Sources */, 846A2C4B2529F99400731018 /* AccountRepositoryFactory.swift in Sources */, + 0C79C89E2A7BEF1200B171E3 /* NominationPoolRecommendationFactory.swift in Sources */, AE528E4D26852C380058935A /* ValidatorSearchViewModel.swift in Sources */, 842BDB30278C57EB00AB4B5A /* DAppBrowserSigningState.swift in Sources */, 843910C9253F591D00E3C217 /* ScaleDecoderOperation.swift in Sources */, @@ -19840,6 +21057,7 @@ 84038FF626FFDF4E00C73F3F /* CrowdloanSharedState.swift in Sources */, 054C4BCDEC29ED5F74A36E8B /* ExportMnemonicPresenter.swift in Sources */, 39218CF5AA701518BD3B0103 /* ExportMnemonicInteractor.swift in Sources */, + 0C13D3182A8216A10054BB6F /* NominationPoolsIconFactory.swift in Sources */, F4F22967260DBC7200ACFDB8 /* StakingPayoutStatusTableCell.swift in Sources */, 84F523FB298AA5820026AD08 /* ReferendumsModelFactory+State.swift in Sources */, 88F3A9FB9CEA464275F1115E /* ExportMnemonicViewFactory.swift in Sources */, @@ -19847,6 +21065,7 @@ 84333BD92856840E00C76A4F /* SelectionValidatorGroups.swift in Sources */, 84002A9E2992444300A80672 /* GovernanceNewDelegation.swift in Sources */, 845B822126EF8F1A00D25C72 /* ManagedMetaAccountMapper.swift in Sources */, + 0C9C64302A8D6779004DC078 /* StakingNPoolsPresenter.swift in Sources */, 8457F91026EB8288006803E1 /* StorageMigrator+Sync.swift in Sources */, F48220F8DA16EB51313B5D56 /* ExportMnemonicConfirmWireframe.swift in Sources */, 844DBC67274E2B6E009F8351 /* AccountImportMnemonicView.swift in Sources */, @@ -19892,10 +21111,12 @@ 84873AFF26028E2B000A83EE /* StakingStateMachine.swift in Sources */, 8442002528E6FEEE00C49C4A /* ReferendumsProtocols.swift in Sources */, 3D1FB0EF87D42F08D9250552 /* PurchasePresenter.swift in Sources */, + 0C12A2492AA35AE300C7FA49 /* RelaychainStakingValidatorFacade.swift in Sources */, F4F22976260DBF3F00ACFDB8 /* StakingPayoutRewardTableCell.swift in Sources */, 8428229A289BC8E400163031 /* AddAccount+ParitySignerWelcomeWireframe.swift in Sources */, 8471577D2910F18300D7D003 /* GovernanceUnlockProtocols.swift in Sources */, 2CF2F93AF862CF54FC46B560 /* PurchaseInteractor.swift in Sources */, + 77EFFC912A7276F1009E28F8 /* StakingTypeAccountView.swift in Sources */, 90EFE3768F1375470FDBE6F6 /* PurchaseViewFactory.swift in Sources */, F452D895273D22CF008F7295 /* SettingsTableHeaderView.swift in Sources */, 8459A9C827469E4B000D6278 /* AcalaContributionSource.swift in Sources */, @@ -19909,6 +21130,7 @@ 880855ED28D062A9004255E7 /* Array+AddOrReplace.swift in Sources */, 8472C5AD265CF9C500E2481B /* StakingRewardDestConfirmViewModelFactory.swift in Sources */, 7728E58F2A123B70007901E0 /* ReferendumsState.swift in Sources */, + 77F1893E2A4996FC00E8B933 /* ParagraphView.swift in Sources */, 8422F2F328881C9B00C7B840 /* WatchOnlyWallet.swift in Sources */, 8446F5F228172BBC00B7A86C /* StakingUnbonHintView.swift in Sources */, F40966F526B299FC008CD244 /* SubqueryRewardsSource.swift in Sources */, @@ -19918,9 +21140,11 @@ 8406B5AD26FBE69200635B61 /* SelectableIconDetailsListViewModel.swift in Sources */, C01C5F1C8CB67B0D5CBE9FB1 /* StakingMainPresenter.swift in Sources */, 843F657A265854A700829C14 /* CrowdloanDisplayInfo.swift in Sources */, + 0C13D31F2A8227AB0054BB6F /* StartStakingPoolConfirmPresenter.swift in Sources */, 8412AF992789AB76008A6C22 /* PolkadotExtensionMetadata.swift in Sources */, 847999AF2888A45700D1BAD2 /* AddAccount+CreateWatchOnlyWireframe.swift in Sources */, 846E5010277998040049B659 /* DAppAuthRequest.swift in Sources */, + 0C59E8F82AA76833001E11F3 /* OperationDetailsContractProvider.swift in Sources */, 6BAF97802DB9C640515F47C7 /* StakingMainInteractor.swift in Sources */, 84BF12A128CFCD70007AA576 /* ParaStkYieldBoostStartError.swift in Sources */, 8466BB42263F501000E065A8 /* StakingUnbondConfirmViewModel.swift in Sources */, @@ -19929,28 +21153,25 @@ F402BC8B273AD20D0075F803 /* AstarBonusServiceError.swift in Sources */, E5F3DF66415E54AE04D0C9A9 /* StakingMainViewController.swift in Sources */, 840874D62978225700ACFA55 /* SubqueryDelegateStatsResponse.swift in Sources */, + 77EFFC8F2A714C21009E28F8 /* StakingTypeBannerView.swift in Sources */, 84E2ABCC29939E6B00A5D3C1 /* GovernanceDelegateValidatingParams.swift in Sources */, F4B39C612732717100BB6E10 /* AcalaContributionSetupProtocols.swift in Sources */, 7796C7012A177D9000D56094 /* ReferendumsSection+Lens.swift in Sources */, E2F9D7BDDE961B162EF100AB /* StakingMainViewFactory.swift in Sources */, 88DC3E21292CAA0100DBCE4D /* RoundedView+Style.swift in Sources */, 848DAF00282294E600D56F55 /* ParachainStaking.swift in Sources */, + 0C59E8FA2AA76A4A001E11F3 /* OperationDetailsDirectStakingProvider.swift in Sources */, 884A752F299B642E00FEFC30 /* DelegateVotedReferendaOption.swift in Sources */, 886CA9632977E9B300FC255A /* GovernanceDelegateBanner.swift in Sources */, - CD76A6513A708051857FD480 /* StakingAmountProtocols.swift in Sources */, - 61B9688494251703A6373A1B /* StakingAmountWireframe.swift in Sources */, - BEE36A5554B026BD7BCD3199 /* StakingAmountPresenter.swift in Sources */, 8423ADD226B2C9D000057EDD /* ImportantViewProtocol.swift in Sources */, 84A3B8A62836E08600DE2669 /* CollatorSelectionInfo.swift in Sources */, - 841E5569282EAC2600C8438F /* ParachainStakingNoStakingState.swift in Sources */, 8438432E2913B3150048595C /* Gov1OperationFactory.swift in Sources */, 843125CD299A71B20063745B /* StackButtonsCell.swift in Sources */, 84CFF1E926526FBC00DB7CF7 /* StakingBondMoreWireframe.swift in Sources */, 84D17ECE2804290700F7BAFF /* RadioSelectorView.swift in Sources */, 841E5567282EAC1000C8438F /* ParachainStakingInitState.swift in Sources */, - F20C8D17ABF18B7104E14394 /* StakingAmountInteractor.swift in Sources */, - EF02C9661F03C8EF58182997 /* StakingAmountViewController.swift in Sources */, - D8581E5440A19D977E17BFDE /* StakingAmountViewFactory.swift in Sources */, + 77799AEC2A7CFB5700B7E564 /* PoolStakingTypeViewModel.swift in Sources */, + 77AB55592AA244BB0058814E /* OperationPoolRewardOrSlashModel.swift in Sources */, 3250F2C0E12ED42A355853BE /* SelectValidatorsStartProtocols.swift in Sources */, EC978E6C4FBF39BE9ED10C86 /* SelectValidatorsStartWireframe.swift in Sources */, 8444407228AA57D600446D22 /* LedgerConnectionManager.swift in Sources */, @@ -19963,6 +21184,7 @@ 84282296289BC31300163031 /* AddAccount+ParitySignerAddConfirmWireframe.swift in Sources */, 84FB9E1E285C58FF00B42FC0 /* Xcm.swift in Sources */, 84FB9E1A285C57D900B42FC0 /* XcmVersionedMultilocation.swift in Sources */, + 77799ADF2A78DAB200B7E564 /* StartStakingInfoRelaychainWireframe.swift in Sources */, 849ABE6826278A4100011A2A /* NominateMapper.swift in Sources */, 2C3124A5EBC1AD57C01EEA17 /* SelectValidatorsStartInteractor.swift in Sources */, 84D184F62A04F5C10060C1BD /* StackStatusCell+WalletConnect.swift in Sources */, @@ -19993,6 +21215,7 @@ 0678271BE1BA5BBC084F478A /* RecommendedValidatorListWireframe.swift in Sources */, F441BE0E263984DD0096B67B /* BondExtraCall.swift in Sources */, 8824D42829032BF60022D778 /* ReferendumStateLocal+Presenter.swift in Sources */, + 0C13D2FC2A7D4B220054BB6F /* RelaychainStakingRestrictionsBuilder.swift in Sources */, 84FB9E20285C5C9E00B42FC0 /* XcmJunction.swift in Sources */, 84EBA4F027AD26A5000AEEAD /* AssetBalanceId.swift in Sources */, 8436B6DC2848998200F24360 /* StakingAccountDetailsViewModelFactory.swift in Sources */, @@ -20000,6 +21223,7 @@ BA7AEE82627CFC0AFD69B299 /* RecommendedValidatorListPresenter.swift in Sources */, 88AC5AD82948A8A20056DD40 /* TransactionHistoryDataSource.swift in Sources */, 8472C5B3265CF9C500E2481B /* StakingRewardDestConfirmViewController.swift in Sources */, + 0C59E8F62AA76772001E11F3 /* OperationDetailsExtrinsicProvider.swift in Sources */, B071927DF8DD5C3CA84494BA /* RecommendedValidatorListViewController.swift in Sources */, D6511F7C3E55197F82AB552C /* RecommendedValidatorListViewFactory.swift in Sources */, C7D77690E10875CF1856EBA1 /* StakingRewardPayoutsProtocols.swift in Sources */, @@ -20017,9 +21241,11 @@ 3229E306230161AA99B14BDD /* StakingRewardPayoutsViewFactory.swift in Sources */, 8473D4002657E8BB00B394B2 /* CrowdloanFunds.swift in Sources */, 845B08102918D65E005785D3 /* GovernanceExtrinsicFactory.swift in Sources */, + 77895CA32A8F8CFD006870FB /* NominationPoolsFilters.swift in Sources */, 7E1A03082260E0D31AD394CA /* StakingRewardDetailsProtocols.swift in Sources */, 65909D701527D99837B439D9 /* StakingRewardDetailsWireframe.swift in Sources */, 84350ACC284569560031EF24 /* ParaStkCollatorInfoViewController.swift in Sources */, + 775F194D2A56EEAC009915B6 /* StartStakingInfoRelaychainPresenter.swift in Sources */, 6A977B56FD6441F52660771C /* StakingRewardDetailsPresenter.swift in Sources */, 84981EEA29D4349300948306 /* TransactionHistoryRemoteFetcher.swift in Sources */, 849F144B29444C3E00D9F9BA /* AssetModel+Id.swift in Sources */, @@ -20038,6 +21264,7 @@ 88BCD02E29F63F5600C6E7C4 /* SettingsError.swift in Sources */, 847F2D5927AB201200AFD476 /* GradientIconView.swift in Sources */, 84EBFCEE285E82BB0006327E /* XcmExecute.swift in Sources */, + 77F033A42A84E028006BC67E /* StakingSelectPoolListHeaderView.swift in Sources */, 849976C627B2B73900B14A6C /* DAppMetamaskStateMachine.swift in Sources */, 88F34FDF28FFEAE500712BDE /* TimelineRow.swift in Sources */, 844C3E732A09184300C4305F /* BalancesStore+Default.swift in Sources */, @@ -20082,6 +21309,7 @@ 8448336727FAAF780077FB55 /* TransakProvider.swift in Sources */, 84CFF1E326526FBC00DB7CF7 /* StakingBondMoreProtocols.swift in Sources */, 8448148128E46881007F64FF /* ConvictionVotingLocks.swift in Sources */, + 0C2F86842A72343800593C01 /* EraNominationPoolsServiceProtocol.swift in Sources */, 5FE687B860FC10AB08518A6E /* WalletHistoryFilterPresenter.swift in Sources */, 640A79BD1335394818E70366 /* WalletHistoryFilterViewController.swift in Sources */, DD090C2ED91726FF7779F6C7 /* WalletHistoryFilterViewFactory.swift in Sources */, @@ -20139,9 +21367,11 @@ 0FB6781AB0186A1ED474CAD6 /* StakingUnbondConfirmProtocols.swift in Sources */, 8401AEC52642A71D000B03E3 /* StakingRebondConfirmationViewFactory.swift in Sources */, 27FA1D57A06AA3A030D226B6 /* StakingUnbondConfirmWireframe.swift in Sources */, + 77799AEE2A7CFB6A00B7E564 /* DirectStakingTypeViewModel.swift in Sources */, 88A95FA628F8664100BE26F3 /* ReferendumTimelineView.swift in Sources */, 88BB0B3F29C1E17F00D041C1 /* KiltDid+StoragePath.swift in Sources */, 843A2C7326A8641400266F53 /* MultiValueView.swift in Sources */, + 0CB261F52A9E188300287305 /* NominationPoolsBondExtraCall.swift in Sources */, 3FF8EE1158A273D0D50BC7A6 /* StakingUnbondConfirmPresenter.swift in Sources */, 8498534F2A17390900993977 /* PalletAssets.swift in Sources */, 845B08042918C308005785D3 /* Gov1ActionOperationFactory.swift in Sources */, @@ -20151,12 +21381,14 @@ 842AEB81292F34B600C61B0C /* RemoteChainExternalApi.swift in Sources */, 8472C5AE265CF9C500E2481B /* StakingRewardDestConfirmViewModel.swift in Sources */, 88BB0B4729C2FB5300D041C1 /* Caip19.swift in Sources */, + 0C13D3022A7D53C10054BB6F /* HybridStakingRecommendationMediator.swift in Sources */, 842A736227DB3032006EE1EA /* OperationExtrinsicModel.swift in Sources */, 843E9B2827C83985009C143A /* AssetListNftsCell.swift in Sources */, 3E1462D9E1C0D490E81FD288 /* StakingUnbondConfirmViewFactory.swift in Sources */, 9B4BE26140C63E07C256CC97 /* StakingRedeemProtocols.swift in Sources */, 88FB7DD12950720800784E08 /* ContainerProtocols.swift in Sources */, 840AE2E529C9AF9C008FF665 /* EtherscanWalletHistoryDecodable.swift in Sources */, + 772B1C7D2A93837800A19061 /* StartStakingCustomValidatorListWireframe.swift in Sources */, 8465DA35298EC5FB00C7CFF1 /* TitleDetailsSheetLayout.swift in Sources */, AEE5FB1C264A610C002B8FDC /* StakingRewardDestSetupLayout.swift in Sources */, 84350ADB28461E5B0031EF24 /* ParaStkYourCollatorsViewModelFactory.swift in Sources */, @@ -20165,6 +21397,7 @@ C0B0DDF638915E8259B1CD67 /* StakingRedeemPresenter.swift in Sources */, C4A4D40A08DAB4A71C21C1A8 /* StakingRedeemInteractor.swift in Sources */, 88A6BD0128CA15710047E4C2 /* LocksViewInput.swift in Sources */, + 770F578B2A8A48FF005FD7C1 /* ButtonViewModel.swift in Sources */, B1CCC5B7BF30F6ACA309B112 /* StakingRedeemViewController.swift in Sources */, C21129B2B8D8B33BCBD5843E /* StakingRedeemViewFactory.swift in Sources */, 85A093F6055DDD2E2E9253F2 /* ControllerAccountProtocols.swift in Sources */, @@ -20172,11 +21405,11 @@ 84C5ADEA28147BC2006D7388 /* InlineAlertView.swift in Sources */, BE3F6213B26F35EB6324DBD8 /* ControllerAccountWireframe.swift in Sources */, 77A6F5AE2A2E0C7C004AFD1A /* ReceiveAssetOperationWireframe.swift in Sources */, + 0C543E972AAB1B350035F45F /* ElectedAndPrefValidators.swift in Sources */, 8489A6CE27FC5C5E0040C066 /* StakingActionsView.swift in Sources */, 8455F19E2A1E4956003F072D /* RelaychainMultistakingUpdateService.swift in Sources */, 8455F1A42A1F606B003F072D /* OnchainStorage.swift in Sources */, 84DED40B266656D400A153BB /* KaruraBonusService.swift in Sources */, - 842A736627DB485E006EE1EA /* OperationSlashModel.swift in Sources */, 845B07EF2915951A005785D3 /* DemocracyVoteThreshold.swift in Sources */, 885551F78A5926D16D5AF0CB /* ControllerAccountPresenter.swift in Sources */, 8442002F28E9AEFB00C49C4A /* VoteWireframe.swift in Sources */, @@ -20227,6 +21460,7 @@ 84F1CB4027CF6BEF0095D523 /* UniquesClassDetails.swift in Sources */, 84FEADEE287837E8001DFC26 /* TuringRewardCalculatorService.swift in Sources */, 845B08062918C3FB005785D3 /* DemocracyProposalCall.swift in Sources */, + 77895C9F2A8F5D40006870FB /* NominationPoolSearchManager.swift in Sources */, 78D94A761EFECED60F38232D /* CustomValidatorListViewController.swift in Sources */, 849B03682A14FB8C009624D9 /* CoingeckoPriceHistoryData.swift in Sources */, 8804AD87295B75CB001C4E09 /* DAppFavoriteSettingsView.swift in Sources */, @@ -20241,6 +21475,7 @@ 0E6C2939AFB3D125C760D5A0 /* CrowdloanContributionSetupProtocols.swift in Sources */, 8410562C27AF1C15004F5CA3 /* Ethereum+Checksum.swift in Sources */, 4E5CD7B8821FA5298EA1598E /* CrowdloanContributionSetupWireframe.swift in Sources */, + 0CC2E5622A6E5C43004092E7 /* NominationPoolsAccountUpdatingService.swift in Sources */, 8846F72029D56A0700B8B776 /* Web3NameAddressListPresentable.swift in Sources */, 77A6F5C12A2F1724004AFD1A /* MessageSheetPresentable+presentOperationResult.swift in Sources */, 9081D43697D992F51E057ED2 /* CrowdloanContributionSetupPresenter.swift in Sources */, @@ -20248,6 +21483,7 @@ F436BB842726EA94004B1794 /* MoonbeamCoordinator.swift in Sources */, 830A27C5447348F1D202D996 /* CrowdloanContributionSetupInteractor.swift in Sources */, 84AE7AB327D399CF00495267 /* StackNetworkCell.swift in Sources */, + 77799AF22A7CFB8D00B7E564 /* PoolAccountViewModel.swift in Sources */, AEA0C8C32681186500F9666F /* ChangeTargetsCustomValidatorListWireframe.swift in Sources */, 8472072E277C203A00F593DD /* UICollectionView+Reuse.swift in Sources */, 883DB180297A546800EFB7D8 /* GovernanceDelegateActionControl.swift in Sources */, @@ -20274,6 +21510,7 @@ 0C2AA829B5CB89B39E0FA95E /* CrowdloanContributionConfirmProtocols.swift in Sources */, 506F0D372BCC8302E513637C /* CrowdloanContributionConfirmWireframe.swift in Sources */, 84E0EE06292D3A58008B2953 /* AssetSelectionBaseProtocols.swift in Sources */, + 77F9FB072A9D96E900820625 /* NominationPoolBondMoreSetupPresenter.swift in Sources */, D1C6EABB48DC3EE254E5A095 /* CrowdloanContributionConfirmPresenter.swift in Sources */, 84BCC71C2924B69D00354DE0 /* ERC20SubscriptionManager.swift in Sources */, 843A2C7126A85B5B00266F53 /* GenericTitleValueView.swift in Sources */, @@ -20282,6 +21519,7 @@ 84B8AA8329F90E3E00347A37 /* WalletConnectStateProtocols.swift in Sources */, 844C3E5A2A06141F00C4305F /* SettingsSubtitleTableViewCell.swift in Sources */, 88421064289BBD9100306F2C /* Currency.swift in Sources */, + 0CB261D92A9893E500287305 /* NPoolsUnstakeBaseProtocols.swift in Sources */, 921E4891E85C0DC6FDD8A0D0 /* CrowdloanContributionConfirmInteractor.swift in Sources */, 88A6BCFF28CA15400047E4C2 /* LocksBalanceViewModelFactory.swift in Sources */, 5C796EF8ED29F564B5D1126B /* CrowdloanContributionConfirmViewController.swift in Sources */, @@ -20314,6 +21552,7 @@ 880855F028D099F2004255E7 /* CrowdloanOnChainSyncService.swift in Sources */, 843CE3A627D2098100436F4E /* NftDetailsLabel.swift in Sources */, 84216FD42827982800479375 /* SelectedRoundCollators.swift in Sources */, + 0C2F868B2A725C3C00593C01 /* EraNominationPoolsChanged.swift in Sources */, 849E17DC27909179002D1744 /* DAppSearchQueryTableViewCell.swift in Sources */, 2AC7BC842731A214001D99B0 /* AssetLocks+Sort.swift in Sources */, 84ADA60E29B9E12700EB687E /* MultiExtrinsicSubmitRetryInputProtocol.swift in Sources */, @@ -20328,6 +21567,7 @@ 93434E8E407A6C63D8862A21 /* AssetSelectionProtocols.swift in Sources */, 84E25BEC27E87D5400290BF1 /* TransferDataValidatorFactory.swift in Sources */, 8499FE7127BE214A00712589 /* StorageKeyDecodingOperation.swift in Sources */, + 775F19532A5BDFB6009915B6 /* StartStakingInfoParachainPresenter.swift in Sources */, 84E8BA1C29FFB38600FD9F40 /* EthereumTransactionReceipt.swift in Sources */, 84CEF288290462C300BA25BB /* GovernanceValidatorFactory.swift in Sources */, CDB78A5A733E4A4F1A2C48C8 /* AssetSelectionWireframe.swift in Sources */, @@ -20342,11 +21582,13 @@ 84355CF628B63D19004E5C5E /* LedgerCrypto+Conversion.swift in Sources */, 8463781F2979DACB0034162B /* ReferendumsActivityViewModelFactory.swift in Sources */, 84B8AA8B29FA3B0500347A37 /* WalletConnectBaseState.swift in Sources */, + 77799AE92A7C99D200B7E564 /* StakingTypeViewModelFactory.swift in Sources */, 84117074285B0E92006F4DFB /* XcmChain.swift in Sources */, 844D2A42281B24510049CF5E /* StackUrlCell.swift in Sources */, 98DADEB52480817D191188C1 /* AssetListInteractor.swift in Sources */, 8442003A28EAA2D400C49C4A /* ReferendumsInteractor.swift in Sources */, 8448D5B6277D717400FAEEBC /* DAppListDecorationView.swift in Sources */, + 0C13D3112A80CA9A0054BB6F /* StakingDataValidatorFactory+Plank.swift in Sources */, 84CC986B287D7BAA0085431C /* RMRKV2Collection.swift in Sources */, 844384B0285391D800611CE2 /* RewardCalculatorEngineFactory.swift in Sources */, 849E07F22849E70C00DE0440 /* ParaStkScheduledRequestsUpdater.swift in Sources */, @@ -20391,6 +21633,8 @@ E37BB7A393FFEFC350B4EA3D /* AdvancedWalletProtocols.swift in Sources */, 84D6E2FA283AE6590031D6FD /* ExtrinsicSubmissionPresenting.swift in Sources */, 842A737927DC7CEF006EE1EA /* DisplayAddressViewModelFactory.swift in Sources */, + 0C9C64362A8D67FB004DC078 /* StakingNPoolsProtocols.swift in Sources */, + 0CB261F12A9E149C00287305 /* NPoolsClaimRewardsError.swift in Sources */, 22403E58019260719055E122 /* AdvancedWalletWireframe.swift in Sources */, EC328560F66AA0F5CFB3FE58 /* AdvancedWalletPresenter.swift in Sources */, 849F144329435F3000D9F9BA /* EvmContractMetadata.swift in Sources */, @@ -20410,6 +21654,7 @@ A07A987DE3047AF1A786D511 /* DAppListViewLayout.swift in Sources */, 8DF76D04C127E0048B253343 /* DAppListViewFactory.swift in Sources */, 848F8B242864448900204BC4 /* TransferSetupPresenterFactory+OnChain.swift in Sources */, + 0CB261DE2A989D2A00287305 /* NominationPoolsUnstakeLimits.swift in Sources */, 843461EA290C04C400379936 /* Gov2OperationFactory+Protocol.swift in Sources */, 8402CC9E275B946100E5BF30 /* DAppItemViewCell.swift in Sources */, FFE19A19E5B4ED67A2C61951 /* DAppSearchProtocols.swift in Sources */, @@ -20443,6 +21688,7 @@ 845AADA62903B32E00B5AE96 /* ReferendumVoteConfirmError.swift in Sources */, FDE2CA45061C620567AC329C /* DAppBrowserViewFactory.swift in Sources */, 882C29AC28DC7B7F009CA4B6 /* SubstrateStorageVersion.swift in Sources */, + 0CC2E5642A6E5C72004092E7 /* NPoolsLocalSubscriptionFactory.swift in Sources */, 84BC7045289EFF44008A9758 /* TransactionDisplayCode.swift in Sources */, 1D1DC32EFF13F41677A084B7 /* DAppOperationConfirmProtocols.swift in Sources */, 84276689297E73380063E08E /* AddDelegationInteractorError.swift in Sources */, @@ -20462,16 +21708,17 @@ 9F3E2D64D77BF89B474BF1E3 /* DAppOperationConfirmViewController.swift in Sources */, 7BD09D3022967C4D90AB4693 /* DAppOperationConfirmViewLayout.swift in Sources */, F63A83EA1CA85D7A43103098 /* DAppOperationConfirmViewFactory.swift in Sources */, - 84D17ECC2803F7EF00F7BAFF /* StakingAmountLayout.swift in Sources */, D600448CB75095E6873E542F /* DAppTxDetailsProtocols.swift in Sources */, 84E25BE827E751B400290BF1 /* Charset+Encoding.swift in Sources */, 847999B12888A4FF00D1BAD2 /* SwitchAccount+CreateWatchOnlyWireframe.swift in Sources */, 0C3205D42A895EDA002EB914 /* EvmConstantGasLimitProvider.swift in Sources */, 0C3205DC2A89677B002EB914 /* EvmGasLimitProviderFactory.swift in Sources */, 8487580727EDEB9600495306 /* BorderedIconLabelView.swift in Sources */, + 0C6F0C9E2A69723B007170C6 /* StartStakingStateProtocol.swift in Sources */, 84AE7AAD27D3839D00495267 /* StackCellViewModel.swift in Sources */, 848CC94628D9FC46009EB4B0 /* ConvictionVotingTally.swift in Sources */, 84953F6A2934C9E20033F47D /* EtherscanERC20OperationFactory.swift in Sources */, + 0CE629D72AA9B5E200E250BD /* BalanceViewModelFactory.swift in Sources */, CD9359A2720F2EE1D4E09DF6 /* DAppTxDetailsWireframe.swift in Sources */, 0E364B6F05D390069D049CC2 /* DAppTxDetailsPresenter.swift in Sources */, 84D184EF2A04E2760060C1BD /* WalletConnectSessionViewModel.swift in Sources */, @@ -20487,6 +21734,7 @@ 84A6AB5E290AA7DF001B57B2 /* ReferendumFullDetailsViewModel.swift in Sources */, 575A729D07A6B984851E6DD0 /* DAppAuthConfirmPresenter.swift in Sources */, 846E501527799B3E0049B659 /* DAppAuthViewModelFactory.swift in Sources */, + 0CB261F32A9E182300287305 /* NominationPoolClaimRewards.swift in Sources */, 3086C94FE01CDFC4F79A9D7F /* DAppAuthConfirmViewController.swift in Sources */, 0F5539A29F404F98DF6B2463 /* DAppAuthConfirmViewLayout.swift in Sources */, 8849C5E8297FBB1F00DE35CC /* InAppUpdatesService.swift in Sources */, @@ -20510,6 +21758,7 @@ 8465DA3D298ECF2500C7CFF1 /* GovernanceAddDelegationTracksProtocols.swift in Sources */, 6797F109D7C270DE4877B435 /* NftListInteractor.swift in Sources */, 845B811528F43C350040CE84 /* Treasury.swift in Sources */, + 0C7C886C2A9622F800DD96A1 /* StakingSelectedEntityViewModel.swift in Sources */, 84C3420B283187D800156569 /* BlockTimeEstimationService.swift in Sources */, EB376E61CD1C39AC148DE80C /* NftListViewController.swift in Sources */, 84BAD213293AFCDA00C55C49 /* TokensManageTableViewCell.swift in Sources */, @@ -20535,6 +21784,7 @@ 84FBED0129277CD700FBEB83 /* EvmAssetBalanceUpdatingService.swift in Sources */, 84ADA61029B9E2E800EB687E /* MultiExtrinsicRetryable.swift in Sources */, 67684F7576ED0252C1050CA5 /* OperationDetailsViewLayout.swift in Sources */, + 77F189442A49974A00E8B933 /* UITextView+bind.swift in Sources */, D840B64C33EF47E723905378 /* OperationDetailsViewFactory.swift in Sources */, 84355CE628B507AD004E5C5E /* LedgerAccountAmount.swift in Sources */, 841E5553282E35D000C8438F /* StakingParachainPresenter.swift in Sources */, @@ -20546,7 +21796,9 @@ 36177C077867DBAEAA2675F7 /* TransferSetupViewController.swift in Sources */, 03348B3D555AA12FF2A95779 /* TransferSetupViewLayout.swift in Sources */, A5880E3789BC9E30835BDCC7 /* TransferSetupViewFactory.swift in Sources */, + 77F189472A49BD6700E8B933 /* StartStakingViewModel.swift in Sources */, 8849AD6629C35B8900F4F7FF /* Web3TransferRecipientRepository.swift in Sources */, + 77F033A62A84EAC3006BC67E /* StakingPoolTableViewCell.swift in Sources */, 8453DE5728FD27390055345C /* Gov2SubscriptionFactory.swift in Sources */, 90ACE8690DA095E4F45494E9 /* TransferConfirmProtocols.swift in Sources */, 4E262D60ACAF44A1FD18FD1D /* TransferConfirmWireframe.swift in Sources */, @@ -20557,12 +21809,14 @@ 8446F5EE2817130600B7A86C /* TitleAmountView+Style.swift in Sources */, 842643BB2878572E0031B5B5 /* TuringStakingRemoteSubscriptionService.swift in Sources */, 0C3205D02A895E5B002EB914 /* EvmConstantGasPriceProvider.swift in Sources */, + 0C59E8ED2AA75C84001E11F3 /* HistoryPoolRewardContext.swift in Sources */, DB37BAF11845A4E5067E07C7 /* TransferConfirmViewController.swift in Sources */, 0D5245ED354CC52A842C85A0 /* TransferConfirmViewLayout.swift in Sources */, 0DF1E0D0CCEDC1340B7A47D7 /* TransferConfirmOnChainViewFactory.swift in Sources */, 779A8F992A04BAC000BE31B3 /* StakingRewardActionControl.swift in Sources */, 8DA9BFE7774B292664FD843F /* DAppPhishingProtocols.swift in Sources */, 84EF8D3E288FDA2100265346 /* WalletListLocalStorageSubscriber.swift in Sources */, + 0C9525E72A7AFA2C00BD724D /* ValueResolver.swift in Sources */, 84DD261429ACA9030032A598 /* VotersInfoOperationFactory.swift in Sources */, 11C6F4CD5B167DE4E9E7F654 /* DAppPhishingWireframe.swift in Sources */, 4014ED301AA53DA0B07B4221 /* DAppPhishingPresenter.swift in Sources */, @@ -20614,6 +21868,7 @@ 84CE22DB29A38ACD00A03156 /* GovernanceDelegateInfoPresenter+Protocol.swift in Sources */, 8217DCBEB74527D57AC82070 /* ParaStkStakeConfirmViewLayout.swift in Sources */, 06FD6F5999D57B27B29C8738 /* ParaStkStakeConfirmViewFactory.swift in Sources */, + 0C9525E32A7AAB2A00BD724D /* StakingTimeModel.swift in Sources */, 8846F73129D6BE5000B8B776 /* Data+base58.swift in Sources */, 2BBA744323AA0BF6FE53C212 /* ParaStkSelectCollatorsProtocols.swift in Sources */, 880059E128EF0A5C00E87B9B /* VotingProgressView.swift in Sources */, @@ -20647,6 +21902,7 @@ 3BFD635E852E4D395025BEE8 /* ParaStkCollatorsSearchViewFactory.swift in Sources */, 88FB7DCB2950712E00784E08 /* AssetDetailsContainerViewController.swift in Sources */, 846AACED28BF94B9009F3D42 /* AccountManagementFilter.swift in Sources */, + 0CE150502A6FAC1200B61CC1 /* NominationPoolsPoolSubscriptionService.swift in Sources */, 8425D0E628FE82CB003B782A /* ReferendumVoteInteractor.swift in Sources */, 766FE2FAB8509BF0F56EA3C0 /* ParaStkCollatorInfoProtocols.swift in Sources */, 4097A50CF5E5794092354758 /* ParaStkCollatorInfoWireframe.swift in Sources */, @@ -20680,6 +21936,7 @@ 77204EA62A1E0EAA00BBDE4A /* WalletConnectionsView.swift in Sources */, F4EA414BCEE9C98FABA9284D /* ParaStkUnstakeConfirmInteractor.swift in Sources */, DECE047E7BBE2B9251A09353 /* ParaStkUnstakeConfirmViewController.swift in Sources */, + 77171CA82A98BBA10032B387 /* NominationPoolErrorPresentable.swift in Sources */, A748D64F6048192E16E5BE44 /* ParaStkUnstakeConfirmViewLayout.swift in Sources */, A14308E2633921838166C843 /* ParaStkUnstakeConfirmViewFactory.swift in Sources */, 841AB78F2993C95200A362E8 /* GovernanceDelegateSetupPresenter+Protocols.swift in Sources */, @@ -20704,6 +21961,7 @@ C32B85D65D0290A577BFC85F /* ParaStkRebondViewFactory.swift in Sources */, 8490110F29E5A4F4005D688B /* URIQRMatcher.swift in Sources */, 912ECC319A48CAD09FB694AC /* AssetsSearchProtocols.swift in Sources */, + 77895CA12A8F7360006870FB /* NominationPoolSearchOperationFactory.swift in Sources */, 8456D2C12993EE9F00D159A7 /* GovernanceDelegateStackCell.swift in Sources */, 8582395FEF296771447439FF /* AssetsSearchWireframe.swift in Sources */, 844304602A28946B00DE36DE /* StakingDashboardParachainMapper.swift in Sources */, @@ -20735,6 +21993,7 @@ 84282288289AC80600163031 /* MultiValueView+Factory.swift in Sources */, 99570B581EF2EE8A1070442F /* CreateWatchOnlyViewFactory.swift in Sources */, 6003DF3EBB77510EFB70B4E4 /* MessageSheetProtocols.swift in Sources */, + 0C13D31D2A82279D0054BB6F /* StartStakingDirectConfirmPresenter.swift in Sources */, 2450083471CD071346371995 /* MessageSheetWireframe.swift in Sources */, 411E74233593A329298C6405 /* MessageSheetPresenter.swift in Sources */, 843461EC290D0D9400379936 /* GovernanceUnlockSchedule.swift in Sources */, @@ -20749,6 +22008,7 @@ A265CC9857E951EB71E5E831 /* WalletsListInteractor.swift in Sources */, 28B4C94DBAF461CBF18B1B63 /* WalletsListViewController.swift in Sources */, 233CB11F486DE1953D977295 /* WalletsListViewLayout.swift in Sources */, + 0C9525E52A7AF82B00BD724D /* DirectStakingMinStakeBuilder.swift in Sources */, FD43B68CFBD5C3497B446F53 /* ChangeWatchOnlyProtocols.swift in Sources */, 887404B62982775F00EE270A /* InAppUpdatesUrlProvider.swift in Sources */, 88B438E728F6C629001FC08A /* StatusTimeViewModel.swift in Sources */, @@ -20783,15 +22043,18 @@ 940DA38E4586A27D7F3E0C67 /* ParitySignerAddressesViewController.swift in Sources */, E8F04B9E557AD6BD0279EA6F /* ParitySignerAddressesViewLayout.swift in Sources */, 84DD49F628EE974B00B804F3 /* ReferendumDecidingFunctionProtocol.swift in Sources */, + 0CF193D32A84FC7C003F12F6 /* SelectedStakingViewModelFactory.swift in Sources */, 01F973625B78736D5EEA86F6 /* ParitySignerAddressesViewFactory.swift in Sources */, ECD4EB7314609007CE35461E /* ParitySignerAddConfirmProtocols.swift in Sources */, 6D622CD4A83EEC1F135B66A8 /* ParitySignerAddConfirmWireframe.swift in Sources */, 41FA237A4AA56AC99322A040 /* ParitySignerAddConfirmPresenter.swift in Sources */, 663DB041307C59E939BF0BE2 /* ParitySignerAddConfirmInteractor.swift in Sources */, 13CF38563E1849EAF1B4E4B6 /* ParitySignerAddConfirmViewFactory.swift in Sources */, + 0C9C64382A8D6949004DC078 /* NPoolsStakingSharedState.swift in Sources */, 84757E17299A2E1200616C6C /* Gov2SubscriptionFactory+Votes.swift in Sources */, 84F76ED629006A0900D7206C /* ReferendumConvictionView.swift in Sources */, 9979A61C1677B7B1D44E58B4 /* ParitySignerTxQrProtocols.swift in Sources */, + 0C13D3212A822B220054BB6F /* StartStakingDirectConfirmWireframe.swift in Sources */, 84CFE448292B9CB100CDDD7C /* OnChainTransferBaseInteractor.swift in Sources */, 535E9CD08FCA2DA52D37A134 /* ParitySignerTxQrWireframe.swift in Sources */, 95AF91994555227D52FCDA24 /* ParitySignerTxQrPresenter.swift in Sources */, @@ -20799,6 +22062,7 @@ 845B0817291902CF005785D3 /* Gov2LockStateFactory.swift in Sources */, 41DE96F778AE909978775438 /* ParitySignerTxQrViewController.swift in Sources */, 844C3E5C2A0615A200C4305F /* SettingsAccessoryTableViewCell.swift in Sources */, + 0C9C643A2A8DF97E004DC078 /* StakingNPoolsError.swift in Sources */, 87F7556E02F6F5BB6F1B1AEA /* ParitySignerTxQrViewLayout.swift in Sources */, 84770F2C291F893200852A33 /* GovernanceUnlockConfirmInitData.swift in Sources */, 84B24FAC2A2F25FF00F9BF59 /* StakingDashboardActiveDetailsView.swift in Sources */, @@ -20814,6 +22078,7 @@ 042799797DF7E6FD02D1D1E6 /* ParitySignerTxScanPresenter.swift in Sources */, 779A8F9B2A050C4400BE31B3 /* StakingRewardDateCell.swift in Sources */, 7580D432F22904C8F71441FE /* ParitySignerTxScanInteractor.swift in Sources */, + 0C12A2472AA1D96000C7FA49 /* SubqueryMultistakingTypeFactory.swift in Sources */, 83DCE6BEEAD957D9C7588DB5 /* ParitySignerTxScanViewController.swift in Sources */, CDED41B125E1D5128736B933 /* ParitySignerTxScanViewLayout.swift in Sources */, 561F5B387B0A1682CE5DE7E4 /* ParitySignerTxScanViewFactory.swift in Sources */, @@ -20832,6 +22097,7 @@ 84FBDBE128C884DA00CC1037 /* ParaStkYieldBoostStorageSubscriber.swift in Sources */, 845B07F929162D24005785D3 /* Gov1LocalMappingFactory.swift in Sources */, 88E74E8A29538C1F008031A3 /* NovaAccountShareFactoryProtocol.swift in Sources */, + 77EFFC932A72A288009E28F8 /* StakingTypeBaseBannerView.swift in Sources */, C102544345E604976BF7AFFC /* LedgerNetworkSelectionWireframe.swift in Sources */, 7796C6FD2A174E6300D56094 /* StakingRewardFiltersPeriod+title.swift in Sources */, D71B2C6056D803C196DF4CDA /* LedgerNetworkSelectionPresenter.swift in Sources */, @@ -20843,6 +22109,7 @@ 33B0D1D29AB3FC3CA23567B6 /* LedgerWalletAccountConfirmationWireframe.swift in Sources */, FF2E6053091276EBBA4D986C /* LedgerAccountConfirmationPresenter.swift in Sources */, 97872BDCD915B56F2F2E7E3B /* LedgerWalletAccountConfirmationInteractor.swift in Sources */, + 0C2F86892A723E5400593C01 /* NominationPoolsOperationFactory.swift in Sources */, E5DC2660D78D3CC9FC48E748 /* LedgerAccountConfirmationViewController.swift in Sources */, 845B823229C8FEC700D187CB /* EtherscanNativeHistoryInfo.swift in Sources */, CDAB179209D12B81430E377C /* LedgerAccountConfirmationViewLayout.swift in Sources */, @@ -20850,6 +22117,7 @@ 99A045F3C6403FB48B39971D /* LedgerWalletConfirmProtocols.swift in Sources */, DCE9FE8A75C2FE7B5CB92CC2 /* LedgerWalletConfirmWireframe.swift in Sources */, 106CC4BFC48B6BFFF31434A9 /* LedgerWalletConfirmPresenter.swift in Sources */, + 77F189492A4A299800E8B933 /* StartStakingViewModelFactory.swift in Sources */, 882808CA29009CDC00AE8089 /* UIView+frame.swift in Sources */, 4541F886953E046C16E42997 /* LedgerWalletConfirmInteractor.swift in Sources */, 2A652719FB31E3C8FF36F46A /* LedgerWalletConfirmViewFactory.swift in Sources */, @@ -20878,6 +22146,7 @@ 9E40464B7687006B1EE75C72 /* LocksProtocols.swift in Sources */, 30413A3C5ADB96B7D663F94D /* LocksWireframe.swift in Sources */, BE301A0F2286CCEF6A02D341 /* LocksPresenter.swift in Sources */, + 0C77B5612A8371AA00B5AE08 /* StaticValidatorListProtocols.swift in Sources */, 0C1BE1A22A46F93B0010933C /* BigUInt+Scientific.swift in Sources */, CE4C1344F03A5132C601A594 /* LocksViewController.swift in Sources */, 8A23DD1F4146639EA2F7AEF6 /* LocksViewFactory.swift in Sources */, @@ -20921,6 +22190,7 @@ 488E4467895040EA85FDCC79 /* ReferendumDetailsViewController.swift in Sources */, 845B811D28F44A700040CE84 /* ReferendumActionLocal.swift in Sources */, 77E255692A16148000B644C3 /* StakingRewardsFilter.swift in Sources */, + 0C13D2F72A7D45F40054BB6F /* RelaychainStakingRestrictions.swift in Sources */, 3403F3DCDE932B9F9C6D32B6 /* ReferendumDetailsViewLayout.swift in Sources */, 1A029717AD309487B70FFD02 /* ReferendumDetailsViewFactory.swift in Sources */, 9DED20EB20A872E682CB402A /* ReferendumFullDetailsProtocols.swift in Sources */, @@ -20943,6 +22213,7 @@ 7E2800371BE3B166F3475E90 /* ReferendumVoteSetupPresenter.swift in Sources */, 04D86D5341406305E60F6D18 /* ReferendumVoteSetupInteractor.swift in Sources */, 2CEFF4C2574F0AABE0E9BF89 /* ReferendumVoteSetupViewController.swift in Sources */, + 0C59E8CF2AA5D744001E11F3 /* PooledBalanceUpdatingState.swift in Sources */, D1C4208A89633395AF2FDB74 /* ReferendumVoteSetupViewLayout.swift in Sources */, 84CE22D929A38AA600A03156 /* GovernanceDelegateInfoPresenter+Update.swift in Sources */, 811096BAAA6BD237DF2769EA /* ReferendumVoteSetupViewFactory.swift in Sources */, @@ -20961,6 +22232,7 @@ 879D493C025963619CFADF4F /* GovernanceUnlockSetupProtocols.swift in Sources */, 46298240F3528B5C62AEC29E /* GovernanceUnlockSetupWireframe.swift in Sources */, 9097EE6D11E2E023D2637BE5 /* GovernanceUnlockSetupPresenter.swift in Sources */, + 0C2F86822A7233DC00593C01 /* EraNominationPoolsService.swift in Sources */, 845938842A03D34F00292BFF /* WCSessionDetailsInteractorError.swift in Sources */, 38D0977931828C7894579968 /* GovernanceUnlockSetupInteractor.swift in Sources */, 16098DABB1C9C058C1965F1D /* GovernanceUnlockSetupViewController.swift in Sources */, @@ -20977,6 +22249,7 @@ F8C0CA3DDBCB5E509295F099 /* MarkdownDescriptionViewFactory.swift in Sources */, 9DE1757D047A4D1E97913774 /* GovernanceUnlockConfirmProtocols.swift in Sources */, 2272FB0A01000A46D097634E /* GovernanceUnlockConfirmWireframe.swift in Sources */, + 0CC2E56A2A6E6EBB004092E7 /* LocalStorageProviderObserving.swift in Sources */, A3BDFA01A32B6C7463E6EFFA /* GovernanceUnlockConfirmPresenter.swift in Sources */, 62649D3FB6AACB508872C67A /* GovernanceUnlockConfirmInteractor.swift in Sources */, 6D603098CCF0B65AA726AD38 /* GovernanceUnlockConfirmViewController.swift in Sources */, @@ -20988,6 +22261,7 @@ B804BDDC5F3960AA59CE3E5B /* AssetDetailsPresenter.swift in Sources */, DCD804D03231F9923FE1624C /* AssetDetailsInteractor.swift in Sources */, 5F107F52BCEF8BA940800F88 /* AssetDetailsViewController.swift in Sources */, + 0C2F86962A72807E00593C01 /* NominationPoolsRewardEngine.swift in Sources */, 84ABB3332A16150400B5E95A /* AccountShareFactory.swift in Sources */, 5B652F1E0040F68F835A2F1D /* AssetDetailsViewLayout.swift in Sources */, E0710E487509797C12110D83 /* AssetDetailsViewFactory.swift in Sources */, @@ -21007,6 +22281,7 @@ 7796C7032A17846B00D56094 /* EmptyCellContentView.swift in Sources */, B8349266F061AEFFA9802237 /* TokenManageSingleViewController.swift in Sources */, D9746967761B1B4772A562B2 /* TokenManageSingleViewLayout.swift in Sources */, + 0C7C88662A95030800DD96A1 /* SubqueryStakingType.swift in Sources */, 0CAC01572A52E1960069413E /* AssetListPresenterHelpers.swift in Sources */, 26533668754DB6C1DF2425AB /* TokenManageSingleViewFactory.swift in Sources */, 3187CF2169E709F25DFB4C0D /* TokensAddSelectNetworkProtocols.swift in Sources */, @@ -21022,6 +22297,8 @@ D3B74ED2525DE12423722DE2 /* AssetReceiveInteractor.swift in Sources */, E6D05825C7512E3CD560B39F /* AssetReceiveViewController.swift in Sources */, 9ACF0A5CCB50CEDD97671EDE /* AssetReceiveViewLayout.swift in Sources */, + 0CAC44AC2A7A7FFD001EDE61 /* RelaychainConsensusStateDepending.swift in Sources */, + 0C59E8E52AA6191E001E11F3 /* ExternalAssetBalanceSubscriber.swift in Sources */, BE966FC878B738FC9DE1D296 /* AssetReceiveViewFactory.swift in Sources */, BDAD1DD6A88406F606E2A70D /* TransactionHistoryProtocols.swift in Sources */, 42AF4E3B592AF7E40CAC13E0 /* TransactionHistoryWireframe.swift in Sources */, @@ -21045,6 +22322,7 @@ F040165DFF9C8D7C5CC47581 /* AddDelegationProtocols.swift in Sources */, FFFA51179A4E22E457BF6F78 /* AddDelegationWireframe.swift in Sources */, 5510625BDA756B939ED7C586 /* AddDelegationPresenter.swift in Sources */, + 0C7C88612A94A54E00DD96A1 /* StakingNPoolsViewModelFactory+Alerts.swift in Sources */, C6E220DA6AD9A938083179CB /* AddDelegationInteractor.swift in Sources */, 8E74A13BA73160F88B2B0948 /* AddDelegationViewController.swift in Sources */, CEED39FF1C586C00B56B1F0C /* AddDelegationViewLayout.swift in Sources */, @@ -21058,6 +22336,7 @@ 507AFE76D2D4EF2F739AE799 /* GovernanceDelegateInfoViewLayout.swift in Sources */, 0B65DAE0327678679CACE0B1 /* GovernanceDelegateInfoViewFactory.swift in Sources */, B776F3F1B81EBA9EED68F605 /* InAppUpdatesProtocols.swift in Sources */, + 0C66102D2A73828800E44634 /* RelaychainStakingSharedState.swift in Sources */, 4CBA0A25DB544214C4A02F0B /* InAppUpdatesWireframe.swift in Sources */, D6A2790F5C9E2AAA137E52CC /* InAppUpdatesPresenter.swift in Sources */, 34D6FF85BEA25EFD1D15D460 /* InAppUpdatesInteractor.swift in Sources */, @@ -21065,6 +22344,7 @@ 841E54A07ACD3AD160A1250A /* InAppUpdatesViewController.swift in Sources */, DDA07514BEF3E2FD6EE1BB4E /* InAppUpdatesViewLayout.swift in Sources */, 32009DBB90D19ACD6D7B7A5C /* InAppUpdatesViewFactory.swift in Sources */, + 77AB555D2AA24BA90058814E /* OperationDetailsPoolRewardView.swift in Sources */, 5D532A958C5C961391177C4A /* DelegationListProtocols.swift in Sources */, 886A277E29E93995003B269F /* EquilibriumAccountInfo.swift in Sources */, F65CC130E018157C0778B074 /* DelegationListWireframe.swift in Sources */, @@ -21087,6 +22367,7 @@ 4B83231E151422897F71408F /* GovernanceSelectTracksInteractor.swift in Sources */, EAAB9E53189BC6394C5900D2 /* GovernanceSelectTracksViewController.swift in Sources */, 347BBBBCC84CA155006FDCDB /* GovernanceSelectTracksViewLayout.swift in Sources */, + 0C59E8CD2AA5D673001E11F3 /* PooledBalanceUpdatingService.swift in Sources */, 60B66AA63089FC2A3A701CF2 /* GovernanceSelectTracksViewFactory.swift in Sources */, 1F45D221E855D5340572C243 /* GovernanceUnavailableTracksProtocols.swift in Sources */, 3EAB85420EDDDE7D5B03A1CF /* GovernanceUnavailableTracksWireframe.swift in Sources */, @@ -21124,6 +22405,7 @@ 1C9EA26D4E4BA6BAE147B374 /* GovernanceDelegateConfirmWireframe.swift in Sources */, 6FE660C98518CEB28AD9CDA3 /* GovernanceDelegateConfirmPresenter.swift in Sources */, 9358E048B1AA0F71F519101E /* GovernanceDelegateConfirmInteractor.swift in Sources */, + 0C79C8A02A7BF80700B171E3 /* RelaychainStakingRecommendationMediator.swift in Sources */, 80175BD9EE66BCE4016E7F28 /* GovernanceDelegateConfirmViewController.swift in Sources */, 5E34CFB3CAE24366E1A24B51 /* GovernanceDelegateConfirmViewLayout.swift in Sources */, 1232A714A96F937330FC0AFA /* GovernanceDelegateConfirmViewFactory.swift in Sources */, @@ -21132,8 +22414,10 @@ 75249684C6F3EE4E553DABA1 /* GovernanceYourDelegationsPresenter.swift in Sources */, EB20C6B406155664B981BA94 /* GovernanceYourDelegationsInteractor.swift in Sources */, 59A0AF440ABAAA459EF7D993 /* GovernanceYourDelegationsViewController.swift in Sources */, + 0C7C886A2A95591900DD96A1 /* StakingTotalRewardView.swift in Sources */, 4FF157CCB838E73449C53831 /* GovernanceYourDelegationsViewLayout.swift in Sources */, D1487642F422F6B96216B0D0 /* GovernanceYourDelegationsViewFactory.swift in Sources */, + 0C13D3262A8275400054BB6F /* StartStakingFeeIdFactory.swift in Sources */, 81ADC94E1CC47A2C6F0F1BEA /* GovernanceEditDelegationTracksProtocols.swift in Sources */, 78E0B6963A8D0A07E742232C /* GovernanceEditDelegationTracksWireframe.swift in Sources */, C7E68F5B6EC7B21B8797F874 /* GovernanceEditDelegationTracksPresenter.swift in Sources */, @@ -21161,10 +22445,12 @@ 1B3E22DE61BAC810106A7D1A /* GovernanceDelegateSearchProtocols.swift in Sources */, 8F97A52ED6D3703A90373387 /* GovernanceDelegateSearchWireframe.swift in Sources */, C6DA3ABD47E72ED1661830A9 /* GovernanceDelegateSearchPresenter.swift in Sources */, + 0CC2E5602A6E44E7004092E7 /* NominationPoolsRemoteSubscriptionService.swift in Sources */, 7C98D0A579FDC879CC5DBF2F /* GovernanceDelegateSearchInteractor.swift in Sources */, 8455F1A02A1E7265003F072D /* Multistaking+Relaychain.swift in Sources */, 84EDF66B29C4BFE9002173E6 /* EvmNativeSubscriptionManager.swift in Sources */, 8C68C4CFAF7CB9312C86D5B8 /* GovernanceDelegateSearchViewController.swift in Sources */, + 77F9FB0B2A9D97A100820625 /* NominationPoolBondMoreSetupWireframe.swift in Sources */, F0C3DCA3CD4F850C16406716 /* GovernanceDelegateSearchViewFactory.swift in Sources */, C98A02D4DEAC6E4CACB9E47E /* StakingRebagConfirmProtocols.swift in Sources */, B09F155D14D146377FB2952A /* StakingRebagConfirmWireframe.swift in Sources */, @@ -21174,6 +22460,7 @@ 37CF597ACB2A32ABCEEFEE67 /* StakingRebagConfirmViewController.swift in Sources */, 09AB6DE2D19F1FA36BF08288 /* StakingRebagConfirmViewLayout.swift in Sources */, 4A24646D497B26E51926BA52 /* StakingRebagConfirmViewFactory.swift in Sources */, + 0C59E8DA2AA6085B001E11F3 /* ExternalAssetBalanceLocalSubscriptionFactory.swift in Sources */, F5AADF461ED043EAF04A50DE /* ReferendumsFiltersProtocols.swift in Sources */, A44CA3E6BB506841948AB2D1 /* ReferendumsFiltersWireframe.swift in Sources */, 32428875321BF68F5DC47D52 /* ReferendumsFiltersPresenter.swift in Sources */, @@ -21187,6 +22474,8 @@ 19055A725FD9C18753B74A52 /* StakingRewardFiltersViewLayout.swift in Sources */, 3983EDE80B9296F3A252BA03 /* StakingRewardFiltersViewFactory.swift in Sources */, CF2F3A0F0999D6D054CD33D2 /* ReferendumSearchProtocols.swift in Sources */, + 0CE629D62AA9B5E200E250BD /* BalanceViewModel.swift in Sources */, + 0C2F86982A728EE900593C01 /* NPoolsRewardEngineFactory.swift in Sources */, C5B07E59C0B00CAD1D0D2DFD /* ReferendumSearchWireframe.swift in Sources */, AB27EE4EE30A06D8E7B8EDB4 /* ReferendumSearchPresenter.swift in Sources */, 2F2ACE609F7423EDD0F06F30 /* ReferendumSearchViewController.swift in Sources */, @@ -21196,6 +22485,7 @@ 2EDE38E0F2E3494D16717A74 /* WalletConnectInteractor.swift in Sources */, EB11BF594D7E16A8885D47DD /* WalletConnectServiceFactory.swift in Sources */, C8171AF2893A4723F4F63E23 /* WalletConnectSessionsProtocols.swift in Sources */, + 0C59E8D52AA5FC96001E11F3 /* ExternalAssetBalanceMapper.swift in Sources */, 842E9EA42A2DC71000759972 /* StakingDashboardModel.swift in Sources */, AB5EA0348C8E8C40FCA9DC86 /* WalletConnectSessionsWireframe.swift in Sources */, B1F2C2241D9020687D469A27 /* WalletConnectSessionsPresenter.swift in Sources */, @@ -21236,6 +22526,93 @@ 25E4B008933E2EF7F2FAAA46 /* StakingMoreOptionsViewController.swift in Sources */, 20B1463FB1F2431A0C913DCE /* StakingMoreOptionsViewLayout.swift in Sources */, FF507CA7B1A33AD160DB59DE /* StakingMoreOptionsViewFactory.swift in Sources */, + 07AB0BC861AA5F134DB9AC26 /* StartStakingInfoProtocols.swift in Sources */, + 77F085A407EDCCF906FD6E22 /* StartStakingInfoWireframe.swift in Sources */, + 52C427F2C572B99D63EB9C21 /* StartStakingInfoBasePresenter.swift in Sources */, + 0CB261EA2A9C940A00287305 /* NominationPoolsUnstakeOperationFactory.swift in Sources */, + 0A6114AACBAB1D55BC03F264 /* StartStakingInfoBaseInteractor.swift in Sources */, + 1A339AB0E007BC2E87B36237 /* StartStakingInfoViewController.swift in Sources */, + A83ED3518F2FD1C7B1C48D9E /* StartStakingInfoViewLayout.swift in Sources */, + DC02C1C18DC5C03F5A006C81 /* StartStakingInfoViewFactory.swift in Sources */, + 62D28F231DED3F39B2C53F1F /* StakingSetupAmountProtocols.swift in Sources */, + 3B1D2A0FCDDF1AAC32BFEE58 /* StakingSetupAmountWireframe.swift in Sources */, + FA0E6F6A12CA290C7079AC6C /* StakingSetupAmountPresenter.swift in Sources */, + 1ECC51FC47422BF1450E0575 /* StakingSetupAmountInteractor.swift in Sources */, + E62D4B6812A8A8518A4B59D9 /* StakingSetupAmountViewController.swift in Sources */, + B5998094F5FFAC894512CD12 /* StakingSetupAmountViewLayout.swift in Sources */, + 5E6F7AC179BE7C1FA1759270 /* StakingSetupAmountViewFactory.swift in Sources */, + 71BA10F56250CF7F7418CDAB /* StakingTypeProtocols.swift in Sources */, + AD3D8EA1D79D3E5E5B625CF7 /* StakingTypeWireframe.swift in Sources */, + 3201805BF8FA78BDF9DA6328 /* StakingTypePresenter.swift in Sources */, + 5D71EC71B3ED00D130C5985F /* StakingTypeInteractor.swift in Sources */, + BE3BCEF256B1B24D702A9869 /* StakingTypeViewController.swift in Sources */, + BB48754C001B791A6ACA16A4 /* StakingTypeViewLayout.swift in Sources */, + 1180349875F35B4D4DD88A4C /* StakingTypeViewFactory.swift in Sources */, + 838D584B803A5A7BCBAD9395 /* StartStakingConfirmProtocols.swift in Sources */, + 08999A79B34D287030887A7C /* StartStakingConfirmWireframe.swift in Sources */, + 60808D290AE02E3A284EC3E9 /* StartStakingConfirmPresenter.swift in Sources */, + F7D3092FDF42D9356654D85A /* StartStakingConfirmInteractor.swift in Sources */, + A99428A540CDA6B359220477 /* StartStakingConfirmViewController.swift in Sources */, + DB8AD60FE397F764623A566F /* StartStakingConfirmViewLayout.swift in Sources */, + A428E022070EF536D4B0B5EC /* StartStakingConfirmViewFactory.swift in Sources */, + AD36A601830C69DA003B3B01 /* StakingSelectPoolProtocols.swift in Sources */, + 141BF00B1B59940711773726 /* StakingSelectPoolWireframe.swift in Sources */, + 37FB290D01ADAA67155C9755 /* StakingSelectPoolPresenter.swift in Sources */, + CC8C6FFB98086AFBE38BDB82 /* StakingSelectPoolInteractor.swift in Sources */, + 621E843DCEA85A00B419926F /* StakingSelectPoolViewController.swift in Sources */, + A3FD763479AAB9290A612A1C /* StakingSelectPoolViewLayout.swift in Sources */, + 2F73FA6B6061F343E2F033F0 /* StakingSelectPoolViewFactory.swift in Sources */, + 6C56D43878A36E0AB7451DF6 /* NominationPoolSearchProtocols.swift in Sources */, + 175CA0D71131FF37CF4A3CB9 /* NominationPoolSearchWireframe.swift in Sources */, + A0EFC9C4C6F0AE9AFDA9A3EA /* NominationPoolSearchPresenter.swift in Sources */, + 5AC2A8AD94278DFA4B68A718 /* NominationPoolSearchInteractor.swift in Sources */, + 0090AF084EEEA26E4018B1B3 /* NominationPoolSearchViewController.swift in Sources */, + 9C223E4BF19F7314A9E6F1CA /* NominationPoolSearchViewLayout.swift in Sources */, + 9D509AD640B01CAB872E0E71 /* NominationPoolSearchViewFactory.swift in Sources */, + D8918818FD97B52EB9DA941E /* NominationPoolBondMoreSetupProtocols.swift in Sources */, + 948FE60822DFC49A0BD5740B /* NominationPoolBondMoreBaseWireframe.swift in Sources */, + 478D954AF83399843A2FAA8A /* NominationPoolBondMoreBasePresenter.swift in Sources */, + C49AC521056CBDB5451B1CDC /* NominationPoolBondMoreBaseInteractor.swift in Sources */, + 873FAB6E5CAD1FD4D02737D0 /* NominationPoolBondMoreSetupViewController.swift in Sources */, + 5FF13D27B596CD7B0CA20671 /* NominationPoolBondMoreSetupViewLayout.swift in Sources */, + BD556407702A75D66B73A55C /* NominationPoolBondMoreSetupViewFactory.swift in Sources */, + 19B35C4E96708405754B8EC5 /* NPoolsUnstakeSetupProtocols.swift in Sources */, + 5103B0A6919721E7E1284829 /* NPoolsUnstakeSetupWireframe.swift in Sources */, + 594EA36463252924AB73475B /* NPoolsUnstakeSetupPresenter.swift in Sources */, + 1E1B60AC1FBF11673A70955C /* NPoolsUnstakeSetupInteractor.swift in Sources */, + BF31A2900BA28194047D2219 /* NPoolsUnstakeSetupViewController.swift in Sources */, + CB2E57FC45AC1E2980E3492E /* NPoolsUnstakeSetupViewLayout.swift in Sources */, + 0C59E8FE2AA773D6001E11F3 /* OperationDetailsDataProviderFactory.swift in Sources */, + 265C6E00915F2F186551A67B /* NPoolsUnstakeSetupViewFactory.swift in Sources */, + F9E6E306BCE32992EA9ABF3E /* NominationPoolBondMoreConfirmProtocols.swift in Sources */, + 3C3C98149DA3BDE3CE692F3C /* NominationPoolBondMoreConfirmWireframe.swift in Sources */, + 9AA6B442A40A0F8C991D0A12 /* NominationPoolBondMoreConfirmPresenter.swift in Sources */, + ACE725ECAB92169CC13E788D /* NominationPoolBondMoreConfirmInteractor.swift in Sources */, + 43914487914F1EAA9800D303 /* NominationPoolBondMoreConfirmViewController.swift in Sources */, + BD02676BEB6F51E2A325EAD9 /* NominationPoolBondMoreConfirmViewLayout.swift in Sources */, + B42ACF24D3F01C44B856C0E7 /* NominationPoolBondMoreConfirmViewFactory.swift in Sources */, + 13DE59F1804CD6761EBC26B9 /* NPoolsUnstakeConfirmProtocols.swift in Sources */, + 39C1255EC6C5C7AC14680608 /* NPoolsUnstakeConfirmWireframe.swift in Sources */, + F7FB7376B1F3918B3751DAA2 /* NPoolsUnstakeConfirmPresenter.swift in Sources */, + 4224A32768A37D48C7E599E7 /* NPoolsUnstakeConfirmInteractor.swift in Sources */, + 1628245885FF82F14BA09E5C /* NPoolsUnstakeConfirmViewController.swift in Sources */, + B33EBF5821B995FE21424705 /* NPoolsUnstakeConfirmViewLayout.swift in Sources */, + 16359021D9683A59F293FA67 /* NPoolsUnstakeConfirmViewFactory.swift in Sources */, + B19BA6D7071BC7BE1EFFDE6D /* NPoolsClaimRewardsProtocols.swift in Sources */, + 0CE629E22AA9BE3000E250BD /* StakingSetupTypeEntityFacade.swift in Sources */, + FCBFD26B960C4837646A0D86 /* NPoolsClaimRewardsWireframe.swift in Sources */, + F690C666F5A95D0C8A48BD45 /* NPoolsClaimRewardsPresenter.swift in Sources */, + 924BADB89E7FA2DC54BF1A02 /* NPoolsClaimRewardsInteractor.swift in Sources */, + 18AB038EB690E4912A003755 /* NPoolsClaimRewardsViewController.swift in Sources */, + 78D63EC7EC5F7427335A025E /* NPoolsClaimRewardsViewLayout.swift in Sources */, + 43D58563868FA362F47B7D92 /* NPoolsClaimRewardsViewFactory.swift in Sources */, + 6E58D665BB280CD332DC9F5E /* NPoolsRedeemProtocols.swift in Sources */, + FD322DC05438902ED369E8FA /* NPoolsRedeemWireframe.swift in Sources */, + 93B64E378DDDCC7F20FF78A2 /* NPoolsRedeemPresenter.swift in Sources */, + B959E423181234E82B0695DF /* NPoolsRedeemInteractor.swift in Sources */, + EB877554208E91A80985F1E5 /* NPoolsRedeemViewController.swift in Sources */, + 59D03D8CB4AE60DE53130729 /* NPoolsRedeemViewLayout.swift in Sources */, + F3719F7C2AD0B75FC271DCE9 /* NPoolsRedeemViewFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -21463,7 +22840,7 @@ /* Begin XCBuildConfiguration section */ 8438E1D724BFAAD2001BDB13 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5EAF3AEE27F7901458B39A7A /* Pods-novawalletAll-novawalletIntegrationTests.debug.xcconfig */; + baseConfigurationReference = 5ACCF5C31EF0E346D0763897 /* Pods-novawalletAll-novawalletIntegrationTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -21485,7 +22862,7 @@ }; 8438E1D824BFAAD2001BDB13 /* Dev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A4865344B432B891D5B48825 /* Pods-novawalletAll-novawalletIntegrationTests.dev.xcconfig */; + baseConfigurationReference = 48E5BB1EB494B5DB92FC3053 /* Pods-novawalletAll-novawalletIntegrationTests.dev.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -21507,7 +22884,7 @@ }; 8438E1D924BFAAD2001BDB13 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F0548D67378CB1EFEC2D5784 /* Pods-novawalletAll-novawalletIntegrationTests.release.xcconfig */; + baseConfigurationReference = A2DB310746B3C2B7DC09389F /* Pods-novawalletAll-novawalletIntegrationTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -21710,7 +23087,7 @@ }; 849013CB24A80986008F705E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 93C4B831433B7A96FF654763 /* Pods-novawalletTests.debug.xcconfig */; + baseConfigurationReference = 60EAEB7059DC148A36865DA8 /* Pods-novawalletTests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -21739,7 +23116,7 @@ }; 849013CC24A80986008F705E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8ADA5C374888879D27DBAA29 /* Pods-novawalletTests.release.xcconfig */; + baseConfigurationReference = 50C441FA177F6838F6902786 /* Pods-novawalletTests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -21856,7 +23233,7 @@ }; 8490140024A92A27008F705E /* Dev */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8F9C67FCF466D9EF48ED35D2 /* Pods-novawalletTests.dev.xcconfig */; + baseConfigurationReference = EFDA1FDA15E7DA2D0952166C /* Pods-novawalletTests.dev.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -21967,7 +23344,7 @@ }; 84AE11CE273C068700B294B7 /* Staging */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 78B08033E84541B42A6EAFEE /* Pods-novawalletTests.staging.xcconfig */; + baseConfigurationReference = D6C738EAB236FB5D854A8D77 /* Pods-novawalletTests.staging.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -21991,7 +23368,7 @@ }; 84AE11CF273C068700B294B7 /* Staging */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A8683EB10308DBF8D445266F /* Pods-novawalletAll-novawalletIntegrationTests.staging.xcconfig */; + baseConfigurationReference = 2E5C5EE99A4B73789BE23039 /* Pods-novawalletAll-novawalletIntegrationTests.staging.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -22064,6 +23441,9 @@ 843910CA253F7E6500E3C217 /* SubstrateDataModel.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 0C59E8C72AA5C3F4001E11F3 /* SubstrateDataModel19.xcdatamodel */, + 0CC2E55C2A6AAFFD004092E7 /* SubstrateDataModel18.xcdatamodel */, + 0C04290B2A67A42A00C3583A /* SubstrateDataModel17.xcdatamodel */, 8473B4742A206E0A003DE213 /* SubstrateDataModel16.xcdatamodel */, 849853502A17449700993977 /* SubstrateDataModel15.xcdatamodel */, 848DCD0A29D71C5200E8A300 /* SubstrateDataModel14.xcdatamodel */, @@ -22081,7 +23461,7 @@ 88787F0328DB3A7B00B115AB /* SubstrateDataModel2.xcdatamodel */, 843910CB253F7E6500E3C217 /* SubstrateDataModel.xcdatamodel */, ); - currentVersion = 8473B4742A206E0A003DE213 /* SubstrateDataModel16.xcdatamodel */; + currentVersion = 0C59E8C72AA5C3F4001E11F3 /* SubstrateDataModel19.xcdatamodel */; path = SubstrateDataModel.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/novawallet/Assets.xcassets/Image.imageset/Coin_4.pdf b/novawallet/Assets.xcassets/Image.imageset/Coin_4.pdf new file mode 100644 index 0000000000..c30eb041a1 Binary files /dev/null and b/novawallet/Assets.xcassets/Image.imageset/Coin_4.pdf differ diff --git a/novawallet/Assets.xcassets/Image.imageset/Contents.json b/novawallet/Assets.xcassets/Image.imageset/Contents.json new file mode 100644 index 0000000000..f882fcd356 --- /dev/null +++ b/novawallet/Assets.xcassets/Image.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Coin_4.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/colors/background/colorBlueStakingCardBackground.colorset/Contents.json b/novawallet/Assets.xcassets/colors/background/colorBlueStakingCardBackground.colorset/Contents.json new file mode 100644 index 0000000000..2fef29c547 --- /dev/null +++ b/novawallet/Assets.xcassets/colors/background/colorBlueStakingCardBackground.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.240", + "blue" : "0xFF", + "green" : "0x8A", + "red" : "0x43" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/colors/background/colorRewardsBackground.colorset/Contents.json b/novawallet/Assets.xcassets/colors/background/colorRewardsBackground.colorset/Contents.json new file mode 100644 index 0000000000..d60ff61c5c --- /dev/null +++ b/novawallet/Assets.xcassets/colors/background/colorRewardsBackground.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x33", + "green" : "0x0A", + "red" : "0x06" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/colors/border/colorRadioBorder.colorset/Contents.json b/novawallet/Assets.xcassets/colors/border/colorRadioBorder.colorset/Contents.json index 733159fa49..d5142e5d14 100644 --- a/novawallet/Assets.xcassets/colors/border/colorRadioBorder.colorset/Contents.json +++ b/novawallet/Assets.xcassets/colors/border/colorRadioBorder.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "0.240", - "blue" : "0.780", - "green" : "0.620", - "red" : "0.600" + "blue" : "0xC6", + "green" : "0x9E", + "red" : "0x99" } }, "idiom" : "universal" diff --git a/novawallet/Assets.xcassets/colors/border/colorStakingTypeCardBorder.colorset/Contents.json b/novawallet/Assets.xcassets/colors/border/colorStakingTypeCardBorder.colorset/Contents.json new file mode 100644 index 0000000000..1c99b26682 --- /dev/null +++ b/novawallet/Assets.xcassets/colors/border/colorStakingTypeCardBorder.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.240", + "blue" : "0xC7", + "green" : "0x9E", + "red" : "0x99" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/colors/icon/colorIconSecondary.colorset/Contents.json b/novawallet/Assets.xcassets/colors/icon/colorIconSecondary.colorset/Contents.json index 1e80062835..b054d632ea 100644 --- a/novawallet/Assets.xcassets/colors/icon/colorIconSecondary.colorset/Contents.json +++ b/novawallet/Assets.xcassets/colors/icon/colorIconSecondary.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "0.320", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" } }, "idiom" : "universal" diff --git a/novawallet/Assets.xcassets/colors/text/colorPolkadotBrand.colorset/Contents.json b/novawallet/Assets.xcassets/colors/text/colorPolkadotBrand.colorset/Contents.json new file mode 100644 index 0000000000..69253c66d8 --- /dev/null +++ b/novawallet/Assets.xcassets/colors/text/colorPolkadotBrand.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x7A", + "green" : "0x00", + "red" : "0xE6" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/colors/text/colorTextSecondary.colorset/Contents.json b/novawallet/Assets.xcassets/colors/text/colorTextSecondary.colorset/Contents.json index f78612a457..61ae07d821 100644 --- a/novawallet/Assets.xcassets/colors/text/colorTextSecondary.colorset/Contents.json +++ b/novawallet/Assets.xcassets/colors/text/colorTextSecondary.colorset/Contents.json @@ -4,10 +4,10 @@ "color" : { "color-space" : "srgb", "components" : { - "alpha" : "0.480", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" + "alpha" : "0.640", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" } }, "idiom" : "universal" diff --git a/novawallet/Assets.xcassets/colors/text/colorTextTertiary.colorset/Contents.json b/novawallet/Assets.xcassets/colors/text/colorTextTertiary.colorset/Contents.json new file mode 100644 index 0000000000..1ce50452e9 --- /dev/null +++ b/novawallet/Assets.xcassets/colors/text/colorTextTertiary.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.480", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconStakingDirect.imageset/Contents.json b/novawallet/Assets.xcassets/iconStakingDirect.imageset/Contents.json new file mode 100644 index 0000000000..d8a52e472b --- /dev/null +++ b/novawallet/Assets.xcassets/iconStakingDirect.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "iconStakingDirect.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconStakingDirect.imageset/iconStakingDirect.pdf b/novawallet/Assets.xcassets/iconStakingDirect.imageset/iconStakingDirect.pdf new file mode 100644 index 0000000000..1ce8a8e147 Binary files /dev/null and b/novawallet/Assets.xcassets/iconStakingDirect.imageset/iconStakingDirect.pdf differ diff --git a/novawallet/Assets.xcassets/iconStakingPool.imageset/Contents.json b/novawallet/Assets.xcassets/iconStakingPool.imageset/Contents.json new file mode 100644 index 0000000000..6aaa7e8e22 --- /dev/null +++ b/novawallet/Assets.xcassets/iconStakingPool.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "iconStakingPool.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconStakingPool.imageset/iconStakingPool.pdf b/novawallet/Assets.xcassets/iconStakingPool.imageset/iconStakingPool.pdf new file mode 100644 index 0000000000..6c68cfc869 Binary files /dev/null and b/novawallet/Assets.xcassets/iconStakingPool.imageset/iconStakingPool.pdf differ diff --git a/novawallet/Assets.xcassets/iconsStaking/Contents.json b/novawallet/Assets.xcassets/iconsStaking/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/novawallet/Assets.xcassets/iconsStaking/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconsStaking/clock.imageset/Contents.json b/novawallet/Assets.xcassets/iconsStaking/clock.imageset/Contents.json new file mode 100644 index 0000000000..9a6fb3fe17 --- /dev/null +++ b/novawallet/Assets.xcassets/iconsStaking/clock.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "unstake-anytime.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconsStaking/clock.imageset/unstake-anytime.pdf b/novawallet/Assets.xcassets/iconsStaking/clock.imageset/unstake-anytime.pdf new file mode 100644 index 0000000000..69cadc80c7 Binary files /dev/null and b/novawallet/Assets.xcassets/iconsStaking/clock.imageset/unstake-anytime.pdf differ diff --git a/novawallet/Assets.xcassets/iconsStaking/coin.imageset/Coin_4.pdf b/novawallet/Assets.xcassets/iconsStaking/coin.imageset/Coin_4.pdf new file mode 100644 index 0000000000..c30eb041a1 Binary files /dev/null and b/novawallet/Assets.xcassets/iconsStaking/coin.imageset/Coin_4.pdf differ diff --git a/novawallet/Assets.xcassets/iconsStaking/coin.imageset/Contents.json b/novawallet/Assets.xcassets/iconsStaking/coin.imageset/Contents.json new file mode 100644 index 0000000000..f882fcd356 --- /dev/null +++ b/novawallet/Assets.xcassets/iconsStaking/coin.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Coin_4.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconsStaking/cup.imageset/Contents.json b/novawallet/Assets.xcassets/iconsStaking/cup.imageset/Contents.json new file mode 100644 index 0000000000..db447d9754 --- /dev/null +++ b/novawallet/Assets.xcassets/iconsStaking/cup.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "rewards.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconsStaking/cup.imageset/rewards.pdf b/novawallet/Assets.xcassets/iconsStaking/cup.imageset/rewards.pdf new file mode 100644 index 0000000000..58afb08173 Binary files /dev/null and b/novawallet/Assets.xcassets/iconsStaking/cup.imageset/rewards.pdf differ diff --git a/novawallet/Assets.xcassets/iconsStaking/ring.imageset/Contents.json b/novawallet/Assets.xcassets/iconsStaking/ring.imageset/Contents.json new file mode 100644 index 0000000000..d4ee577a81 --- /dev/null +++ b/novawallet/Assets.xcassets/iconsStaking/ring.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "monitor-your-stake.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconsStaking/ring.imageset/monitor-your-stake.pdf b/novawallet/Assets.xcassets/iconsStaking/ring.imageset/monitor-your-stake.pdf new file mode 100644 index 0000000000..730c3a71c9 Binary files /dev/null and b/novawallet/Assets.xcassets/iconsStaking/ring.imageset/monitor-your-stake.pdf differ diff --git a/novawallet/Assets.xcassets/iconsStaking/speaker.imageset/Contents.json b/novawallet/Assets.xcassets/iconsStaking/speaker.imageset/Contents.json new file mode 100644 index 0000000000..0b7d8751c2 --- /dev/null +++ b/novawallet/Assets.xcassets/iconsStaking/speaker.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "participate-in-governance.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconsStaking/speaker.imageset/participate-in-governance.pdf b/novawallet/Assets.xcassets/iconsStaking/speaker.imageset/participate-in-governance.pdf new file mode 100644 index 0000000000..b3adc2947c Binary files /dev/null and b/novawallet/Assets.xcassets/iconsStaking/speaker.imageset/participate-in-governance.pdf differ diff --git a/novawallet/Assets.xcassets/iconsStaking/system.imageset/Contents.json b/novawallet/Assets.xcassets/iconsStaking/system.imageset/Contents.json new file mode 100644 index 0000000000..4dc53f42b4 --- /dev/null +++ b/novawallet/Assets.xcassets/iconsStaking/system.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "test-network.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/iconsStaking/system.imageset/test-network.pdf b/novawallet/Assets.xcassets/iconsStaking/system.imageset/test-network.pdf new file mode 100644 index 0000000000..f8bd9179e9 Binary files /dev/null and b/novawallet/Assets.xcassets/iconsStaking/system.imageset/test-network.pdf differ diff --git a/novawallet/Assets.xcassets/imageStakingTypeDirect.imageset/Contents.json b/novawallet/Assets.xcassets/imageStakingTypeDirect.imageset/Contents.json new file mode 100644 index 0000000000..887fc57b14 --- /dev/null +++ b/novawallet/Assets.xcassets/imageStakingTypeDirect.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "imageStakingTypeDirect.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "imageStakingTypeDirect@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "imageStakingTypeDirect@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/imageStakingTypeDirect.imageset/imageStakingTypeDirect.png b/novawallet/Assets.xcassets/imageStakingTypeDirect.imageset/imageStakingTypeDirect.png new file mode 100644 index 0000000000..0d4eeb72d8 Binary files /dev/null and b/novawallet/Assets.xcassets/imageStakingTypeDirect.imageset/imageStakingTypeDirect.png differ diff --git a/novawallet/Assets.xcassets/imageStakingTypeDirect.imageset/imageStakingTypeDirect@2x.png b/novawallet/Assets.xcassets/imageStakingTypeDirect.imageset/imageStakingTypeDirect@2x.png new file mode 100644 index 0000000000..9f50e55966 Binary files /dev/null and b/novawallet/Assets.xcassets/imageStakingTypeDirect.imageset/imageStakingTypeDirect@2x.png differ diff --git a/novawallet/Assets.xcassets/imageStakingTypeDirect.imageset/imageStakingTypeDirect@3x.png b/novawallet/Assets.xcassets/imageStakingTypeDirect.imageset/imageStakingTypeDirect@3x.png new file mode 100644 index 0000000000..a34c688922 Binary files /dev/null and b/novawallet/Assets.xcassets/imageStakingTypeDirect.imageset/imageStakingTypeDirect@3x.png differ diff --git a/novawallet/Assets.xcassets/imageStakingTypePool.imageset/Contents.json b/novawallet/Assets.xcassets/imageStakingTypePool.imageset/Contents.json new file mode 100644 index 0000000000..b6904b2105 --- /dev/null +++ b/novawallet/Assets.xcassets/imageStakingTypePool.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "imageStakingTypePool.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "imageStakingTypePool@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "imageStakingTypePool@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/imageStakingTypePool.imageset/imageStakingTypePool.png b/novawallet/Assets.xcassets/imageStakingTypePool.imageset/imageStakingTypePool.png new file mode 100644 index 0000000000..e3026ebecb Binary files /dev/null and b/novawallet/Assets.xcassets/imageStakingTypePool.imageset/imageStakingTypePool.png differ diff --git a/novawallet/Assets.xcassets/imageStakingTypePool.imageset/imageStakingTypePool@2x.png b/novawallet/Assets.xcassets/imageStakingTypePool.imageset/imageStakingTypePool@2x.png new file mode 100644 index 0000000000..09280bfc34 Binary files /dev/null and b/novawallet/Assets.xcassets/imageStakingTypePool.imageset/imageStakingTypePool@2x.png differ diff --git a/novawallet/Assets.xcassets/imageStakingTypePool.imageset/imageStakingTypePool@3x.png b/novawallet/Assets.xcassets/imageStakingTypePool.imageset/imageStakingTypePool@3x.png new file mode 100644 index 0000000000..e2dd593652 Binary files /dev/null and b/novawallet/Assets.xcassets/imageStakingTypePool.imageset/imageStakingTypePool@3x.png differ diff --git a/novawallet/Common/Configs/ApplicationConfigs.swift b/novawallet/Common/Configs/ApplicationConfigs.swift index acc1a6f3cd..6282f669b3 100644 --- a/novawallet/Common/Configs/ApplicationConfigs.swift +++ b/novawallet/Common/Configs/ApplicationConfigs.swift @@ -129,9 +129,9 @@ extension ApplicationConfig: ApplicationConfigProtocol { var chainListURL: URL { #if F_RELEASE - URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/chains/v13/chains.json")! + URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/chains/v14/chains.json")! #else - URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/chains/v13/chains_dev.json")! + URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/chains/v14/chains_dev.json")! #endif } @@ -157,7 +157,7 @@ extension ApplicationConfig: ApplicationConfigProtocol { #if F_RELEASE URL(string: "https://api.subquery.network/sq/nova-wallet/subquery-staking")! #else - URL(string: "https://api.subquery.network/sq/nova-wallet/subquery-staking")! + URL(string: "https://api.subquery.network/sq/nova-wallet/subquery-staking__bm92Y")! #endif } diff --git a/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift deleted file mode 100644 index 2aa3c9a64f..0000000000 --- a/novawallet/Common/DataProvider/CrowdloanContributionLocalSubscriptionFactory.swift +++ /dev/null @@ -1,222 +0,0 @@ -import SubstrateSdk -import RobinHood - -protocol CrowdloanContributionLocalSubscriptionFactoryProtocol { - func getCrowdloanContributionDataProvider( - for accountId: AccountId, - chain: ChainModel - ) -> StreamableProvider? - - func getAllLocalCrowdloanContributionDataProvider() -> StreamableProvider? -} - -final class CrowdloanContributionLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, - CrowdloanContributionLocalSubscriptionFactoryProtocol { - let operationFactory: CrowdloanOperationFactoryProtocol - let paraIdOperationFactory: ParaIdOperationFactoryProtocol - let eventCenter: EventCenterProtocol - - init( - operationFactory: CrowdloanOperationFactoryProtocol, - operationManager: OperationManagerProtocol, - chainRegistry: ChainRegistryProtocol, - storageFacade: StorageFacadeProtocol, - paraIdOperationFactory: ParaIdOperationFactoryProtocol, - eventCenter: EventCenterProtocol, - logger: LoggerProtocol - ) { - self.operationFactory = operationFactory - self.paraIdOperationFactory = paraIdOperationFactory - self.eventCenter = eventCenter - - 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 = ExternalContributionSourcesFactory.createExternalSources( - for: chain.chainId, - paraIdOperationFactory: paraIdOperationFactory - ) - - 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, - chainId: chain.chainId, - accountId: accountId, - eventCenter: eventCenter - ) - - 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 - ) - } - } - - 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 { - 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, - eventCenter: EventCenter.shared, - logger: Logger.shared - ) -} diff --git a/novawallet/Common/DataProvider/ExternalAssetBalanceFactory/CrowdloanExternalServiceFactory.swift b/novawallet/Common/DataProvider/ExternalAssetBalanceFactory/CrowdloanExternalServiceFactory.swift new file mode 100644 index 0000000000..97cf75f96b --- /dev/null +++ b/novawallet/Common/DataProvider/ExternalAssetBalanceFactory/CrowdloanExternalServiceFactory.swift @@ -0,0 +1,113 @@ +import Foundation +import RobinHood + +final class CrowdloanExternalServiceFactory { + let storageFacade: StorageFacadeProtocol + let chainRegistry: ChainRegistryProtocol + let operationFactory: CrowdloanOperationFactoryProtocol + let paraIdOperationFactory: ParaIdOperationFactoryProtocol + let operationQueue: OperationQueue + let logger: LoggerProtocol + + init( + storageFacade: StorageFacadeProtocol, + chainRegistry: ChainRegistryProtocol, + operationFactory: CrowdloanOperationFactoryProtocol, + paraIdOperationFactory: ParaIdOperationFactoryProtocol, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.storageFacade = storageFacade + self.chainRegistry = chainRegistry + self.operationFactory = operationFactory + self.paraIdOperationFactory = paraIdOperationFactory + self.operationQueue = operationQueue + self.logger = logger + } + + 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(operationQueue: operationQueue), + 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(operationQueue: operationQueue), + repository: AnyDataProviderRepository(serviceRepository), + logger: logger + ) + } + } +} + +extension CrowdloanExternalServiceFactory: ExternalAssetBalanceServiceFactoryProtocol { + func createAutomaticSyncServices(for _: ChainAsset, accountId _: AccountId) -> [SyncServiceProtocol] { + [] + } + + func createPollingSyncServices(for chainAsset: ChainAsset, accountId: AccountId) -> [SyncServiceProtocol] { + guard chainAsset.chain.hasCrowdloans, chainAsset.asset.isUtility else { + return [] + } + + let chainId = chainAsset.chain.chainId + + let onchainSyncService = createOnChainSyncService(chainId: chainId, accountId: accountId) + + let offchainSources = ExternalContributionSourcesFactory.createExternalSources( + for: chainId, + paraIdOperationFactory: paraIdOperationFactory + ) + + let offChainSyncServices = createOffChainSyncServices( + from: offchainSources, + chain: chainAsset.chain, + accountId: accountId + ) + + return [onchainSyncService] + offChainSyncServices + } +} diff --git a/novawallet/Common/DataProvider/ExternalAssetBalanceFactory/ExternalAssetBalanceLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/ExternalAssetBalanceFactory/ExternalAssetBalanceLocalSubscriptionFactory.swift new file mode 100644 index 0000000000..8b0d66a3ad --- /dev/null +++ b/novawallet/Common/DataProvider/ExternalAssetBalanceFactory/ExternalAssetBalanceLocalSubscriptionFactory.swift @@ -0,0 +1,216 @@ +import SubstrateSdk +import RobinHood + +protocol ExternalBalanceLocalSubscriptionFactoryProtocol { + func getExternalAssetBalanceProvider( + for accountId: AccountId, + chainAsset: ChainAsset + ) -> StreamableProvider? + + func getAllExternalAssetBalanceProvider() -> StreamableProvider? +} + +final class ExternalBalanceLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory { + let eventCenter: EventCenterProtocol + let serviceFactories: [ExternalAssetBalanceServiceFactoryProtocol] + + init( + serviceFactories: [ExternalAssetBalanceServiceFactoryProtocol], + chainRegistry: ChainRegistryProtocol, + storageFacade: StorageFacadeProtocol, + eventCenter: EventCenterProtocol, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.eventCenter = eventCenter + self.serviceFactories = serviceFactories + + super.init( + chainRegistry: chainRegistry, + storageFacade: storageFacade, + operationManager: OperationManager(operationQueue: operationQueue), + logger: logger + ) + } + + private func createAutomaticSyncServices( + chainAsset: ChainAsset, + accountId: AccountId + ) -> [SyncServiceProtocol] { + serviceFactories.flatMap { $0.createAutomaticSyncServices(for: chainAsset, accountId: accountId) } + } + + private func createPollingSyncServices( + chainAsset: ChainAsset, + accountId: AccountId + ) -> [SyncServiceProtocol] { + serviceFactories.flatMap { $0.createPollingSyncServices(for: chainAsset, accountId: accountId) } + } +} + +enum ExternalBalanceLocalSubscriptionFacade { + static func createDefaultFactory( + for storageFacade: StorageFacadeProtocol, + chainRegistry: ChainRegistryProtocol + ) -> ExternalBalanceLocalSubscriptionFactory { + let operationQueue = OperationManagerFacade.sharedDefaultQueue + let operationManager = OperationManager(operationQueue: operationQueue) + let workingQueue = DispatchQueue.global(qos: .userInitiated) + let logger = Logger.shared + + let crowdloanServiceFactory = CrowdloanExternalServiceFactory( + storageFacade: storageFacade, + chainRegistry: chainRegistry, + operationFactory: CrowdloanOperationFactory( + requestOperationFactory: StorageRequestFactory( + remoteFactory: StorageKeyFactory(), + operationManager: operationManager + ), + operationManager: operationManager + ), + paraIdOperationFactory: ParaIdOperationFactory.shared, + operationQueue: operationQueue, + logger: logger + ) + + let poolServiceFactory = NominationPoolExternalServiceFactory( + storageFacade: storageFacade, + chainRegistry: chainRegistry, + operationQueue: operationQueue, + workingQueue: workingQueue, + logger: Logger.shared + ) + + return ExternalBalanceLocalSubscriptionFactory( + serviceFactories: [crowdloanServiceFactory, poolServiceFactory], + chainRegistry: chainRegistry, + storageFacade: storageFacade, + eventCenter: EventCenter.shared, + operationQueue: operationQueue, + logger: logger + ) + } +} + +extension ExternalBalanceLocalSubscriptionFactory { + static let shared: ExternalBalanceLocalSubscriptionFactory = { + let storageFacade = SubstrateDataStorageFacade.shared + let chainRegistry = ChainRegistryFacade.sharedRegistry + + return ExternalBalanceLocalSubscriptionFacade.createDefaultFactory( + for: storageFacade, + chainRegistry: chainRegistry + ) + }() +} + +extension ExternalBalanceLocalSubscriptionFactory: ExternalBalanceLocalSubscriptionFactoryProtocol { + func getExternalAssetBalanceProvider( + for accountId: AccountId, + chainAsset: ChainAsset + ) -> StreamableProvider? { + let cacheKey = "externalBalances-\(accountId.toHex())-\(chainAsset.chainAssetId.stringValue)" + + if let provider = getProvider(for: cacheKey) as? StreamableProvider { + return provider + } + + let automaticSyncServices = createAutomaticSyncServices( + chainAsset: chainAsset, + accountId: accountId + ) + + let pollingSyncServices = createPollingSyncServices( + chainAsset: chainAsset, + accountId: accountId + ) + + let source = ExternalAssetBalanceStreambleSource( + automaticSyncServices: automaticSyncServices, + pollingSyncServices: pollingSyncServices, + chainAssetId: chainAsset.chainAssetId, + accountId: accountId, + eventCenter: eventCenter + ) + + let filter = NSPredicate.externalAssetBalance( + for: chainAsset.chainAssetId, + accountId: accountId + ) + + let mapper = ExternalAssetBalanceMapper() + 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 && + chainAsset.chain.chainId == entity.chainId && + chainAsset.asset.assetId == entity.assetId + } + ) + + 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 + } + + func getAllExternalAssetBalanceProvider() -> StreamableProvider? { + let cacheKey = "allExternalBalances" + + if let provider = getProvider(for: cacheKey) as? StreamableProvider { + return provider + } + + let source = EmptyStreamableSource() + let mapper = ExternalAssetBalanceMapper() + 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 + } +} diff --git a/novawallet/Common/DataProvider/ExternalAssetBalanceFactory/ExternalAssetBalanceServiceFactoryProtocol.swift b/novawallet/Common/DataProvider/ExternalAssetBalanceFactory/ExternalAssetBalanceServiceFactoryProtocol.swift new file mode 100644 index 0000000000..8828085e77 --- /dev/null +++ b/novawallet/Common/DataProvider/ExternalAssetBalanceFactory/ExternalAssetBalanceServiceFactoryProtocol.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol ExternalAssetBalanceServiceFactoryProtocol { + func createAutomaticSyncServices(for chainAsset: ChainAsset, accountId: AccountId) -> [SyncServiceProtocol] + func createPollingSyncServices(for chainAsset: ChainAsset, accountId: AccountId) -> [SyncServiceProtocol] +} diff --git a/novawallet/Common/DataProvider/ExternalAssetBalanceFactory/NominationPoolExternalServiceFactory.swift b/novawallet/Common/DataProvider/ExternalAssetBalanceFactory/NominationPoolExternalServiceFactory.swift new file mode 100644 index 0000000000..8efcf3bc3b --- /dev/null +++ b/novawallet/Common/DataProvider/ExternalAssetBalanceFactory/NominationPoolExternalServiceFactory.swift @@ -0,0 +1,67 @@ +import Foundation +import RobinHood + +final class NominationPoolExternalServiceFactory { + let storageFacade: StorageFacadeProtocol + let chainRegistry: ChainRegistryProtocol + let operationQueue: OperationQueue + let workingQueue: DispatchQueue + let logger: LoggerProtocol + + init( + storageFacade: StorageFacadeProtocol, + chainRegistry: ChainRegistryProtocol, + operationQueue: OperationQueue, + workingQueue: DispatchQueue, + logger: LoggerProtocol + ) { + self.storageFacade = storageFacade + self.chainRegistry = chainRegistry + self.operationQueue = operationQueue + self.workingQueue = workingQueue + self.logger = logger + } +} + +extension NominationPoolExternalServiceFactory: ExternalAssetBalanceServiceFactoryProtocol { + func createAutomaticSyncServices( + for chainAsset: ChainAsset, + accountId: AccountId + ) -> [SyncServiceProtocol] { + guard let stakings = chainAsset.asset.stakings, stakings.contains(.nominationPools) else { + return [] + } + + let chainId = chainAsset.chain.chainId + + guard + let connection = chainRegistry.getConnection(for: chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + return [] + } + + let mapper = PooledAssetBalanceMapper() + + let repository = storageFacade.createRepository(mapper: AnyCoreDataMapper(mapper)) + + let service = PooledBalanceUpdatingService( + accountId: accountId, + chainAsset: chainAsset, + repository: AnyDataProviderRepository(repository), + connection: connection, + runtimeService: runtimeService, + operationQueue: operationQueue, + workingQueue: workingQueue, + logger: Logger.shared + ) + + return [service] + } + + func createPollingSyncServices( + for _: ChainAsset, + accountId _: AccountId + ) -> [SyncServiceProtocol] { + [] + } +} diff --git a/novawallet/Common/DataProvider/NPoolsLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/NPoolsLocalSubscriptionFactory.swift new file mode 100644 index 0000000000..35bb8947ca --- /dev/null +++ b/novawallet/Common/DataProvider/NPoolsLocalSubscriptionFactory.swift @@ -0,0 +1,288 @@ +import Foundation +import RobinHood +import SubstrateSdk + +protocol NPoolsLocalSubscriptionFactoryProtocol { + func getMinJoinBondProvider( + for chainId: ChainModel.Id + ) throws -> AnyDataProvider + + func getLastPoolIdProvider( + for chainId: ChainModel.Id + ) throws -> AnyDataProvider + + func getPoolMemberProvider( + for accountId: AccountId, + chainId: ChainModel.Id + ) throws -> AnyDataProvider + + func getBondedPoolProvider( + for poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) throws -> AnyDataProvider + + func getMetadataProvider( + for poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) throws -> AnyDataProvider + + func getRewardPoolProvider( + for poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) throws -> AnyDataProvider + + func getSubPoolsProvider( + for poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) throws -> AnyDataProvider + + func getMaxPoolMembers( + for chainId: ChainModel.Id, + missingEntryStrategy: MissingRuntimeEntryStrategy> + ) throws -> AnyDataProvider + + func getCounterForPoolMembers( + for chainId: ChainModel.Id, + missingEntryStrategy: MissingRuntimeEntryStrategy> + ) throws -> AnyDataProvider + + func getMaxMembersPerPool( + for chainId: ChainModel.Id, + missingEntryStrategy: MissingRuntimeEntryStrategy> + ) throws -> AnyDataProvider + + func getClaimableRewards( + for chainId: ChainModel.Id, + accountId: AccountId + ) throws -> AnySingleValueProvider + + func getTotalReward( + for address: AccountAddress, + startTimestamp: Int64?, + endTimestamp: Int64?, + api: LocalChainExternalApi, + assetPrecision: Int16 + ) throws -> AnySingleValueProvider +} + +final class NPoolsLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory {} + +extension NPoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol { + func getMinJoinBondProvider( + for chainId: ChainModel.Id + ) throws -> AnyDataProvider { + try getNoFallbackPathProvider(for: NominationPools.minJoinBondPath, chainId: chainId) + } + + func getLastPoolIdProvider( + for chainId: ChainModel.Id + ) throws -> AnyDataProvider { + try getNoFallbackPathProvider(for: NominationPools.lastPoolIdPath, chainId: chainId) + } + + func getPoolMemberProvider( + for accountId: AccountId, + chainId: ChainModel.Id + ) throws -> AnyDataProvider { + try getNoFallbackAccountProvider( + for: NominationPools.poolMembersPath, + accountId: accountId, + chainId: chainId + ) + } + + func getBondedPoolProvider( + for poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) throws -> AnyDataProvider { + try getNoFallbackScalingElementProvider( + for: NominationPools.bondedPoolPath, + encodableElement: poolId, + chainId: chainId + ) + } + + func getMetadataProvider( + for poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) throws -> AnyDataProvider { + try getNoFallbackScalingElementProvider( + for: NominationPools.metadataPath, + encodableElement: poolId, + chainId: chainId + ) + } + + func getRewardPoolProvider( + for poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) throws -> AnyDataProvider { + try getNoFallbackScalingElementProvider( + for: NominationPools.rewardPoolsPath, + encodableElement: poolId, + chainId: chainId + ) + } + + func getSubPoolsProvider( + for poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) throws -> AnyDataProvider { + try getNoFallbackScalingElementProvider( + for: NominationPools.subPoolsPath, + encodableElement: poolId, + chainId: chainId + ) + } + + func getMaxPoolMembers( + for chainId: ChainModel.Id, + missingEntryStrategy: MissingRuntimeEntryStrategy> + ) throws -> AnyDataProvider { + let codingPath = NominationPools.maxPoolMembers + let localKey = try LocalStorageKeyFactory().createFromStoragePath(codingPath, chainId: chainId) + + let fallback = StorageProviderSourceFallback( + usesRuntimeFallback: false, + missingEntryStrategy: missingEntryStrategy + ) + + return try getDataProvider( + for: localKey, + chainId: chainId, + storageCodingPath: codingPath, + fallback: fallback + ) + } + + func getCounterForPoolMembers( + for chainId: ChainModel.Id, + missingEntryStrategy: MissingRuntimeEntryStrategy> + ) throws -> AnyDataProvider { + let codingPath = NominationPools.counterForPoolMembers + let localKey = try LocalStorageKeyFactory().createFromStoragePath(codingPath, chainId: chainId) + + let fallback = StorageProviderSourceFallback( + usesRuntimeFallback: false, + missingEntryStrategy: missingEntryStrategy + ) + + return try getDataProvider( + for: localKey, + chainId: chainId, + storageCodingPath: codingPath, + fallback: fallback + ) + } + + func getMaxMembersPerPool( + for chainId: ChainModel.Id, + missingEntryStrategy: MissingRuntimeEntryStrategy> + ) throws -> AnyDataProvider { + let codingPath = NominationPools.maxMembersPerPool + let localKey = try LocalStorageKeyFactory().createFromStoragePath(codingPath, chainId: chainId) + + let fallback = StorageProviderSourceFallback( + usesRuntimeFallback: false, + missingEntryStrategy: missingEntryStrategy + ) + + return try getDataProvider( + for: localKey, + chainId: chainId, + storageCodingPath: codingPath, + fallback: fallback + ) + } + + func getClaimableRewards( + for chainId: ChainModel.Id, + accountId: AccountId + ) throws -> AnySingleValueProvider { + clearIfNeeded() + + let identifier = "poolPending" + chainId + accountId.toHexString() + + if let provider = getProvider(for: identifier) as? SingleValueProvider { + return AnySingleValueProvider(provider) + } + + guard let connection = chainRegistry.getConnection(for: chainId) else { + throw ChainRegistryError.connectionUnavailable + } + + guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + throw ChainRegistryError.runtimeMetadaUnavailable + } + + let repository = SubstrateRepositoryFactory( + storageFacade: storageFacade + ).createSingleValueRepository() + + let source = NPoolsPendingRewardDataSource( + accountId: accountId, + connection: connection, + runtimeService: runtimeService + ) + + let anySource = AnySingleValueProviderSource(source) + + let provider = SingleValueProvider( + targetIdentifier: identifier, + source: anySource, + repository: AnyDataProviderRepository(repository) + ) + + saveProvider(provider, for: identifier) + + return AnySingleValueProvider(provider) + } + + func getTotalReward( + for address: AccountAddress, + startTimestamp: Int64?, + endTimestamp: Int64?, + api: LocalChainExternalApi, + assetPrecision: Int16 + ) throws -> AnySingleValueProvider { + clearIfNeeded() + + let timeIdentifier = [ + startTimestamp.map { "\($0)" } ?? "nil", + endTimestamp.map { "\($0)" } ?? "nil" + ].joined(separator: "-") + + let identifier = ("poolReward" + api.url.absoluteString) + address + timeIdentifier + + if let provider = getProvider(for: identifier) as? SingleValueProvider { + return AnySingleValueProvider(provider) + } + + let repository = SubstrateRepositoryFactory( + storageFacade: storageFacade + ).createSingleValueRepository() + + let operationFactory = SubqueryRewardOperationFactory(url: api.url) + + let source = SubqueryTotalRewardSource( + address: address, + startTimestamp: startTimestamp, + endTimestamp: endTimestamp, + assetPrecision: assetPrecision, + operationFactory: operationFactory, + stakingType: .pools + ) + + let anySource = AnySingleValueProviderSource(source) + + let provider = SingleValueProvider( + targetIdentifier: identifier, + source: anySource, + repository: AnyDataProviderRepository(repository) + ) + + saveProvider(provider, for: identifier) + + return AnySingleValueProvider(provider) + } +} diff --git a/novawallet/Common/DataProvider/ParachainStakingLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/ParachainStakingLocalSubscriptionFactory.swift index dd89dac618..01ee756445 100644 --- a/novawallet/Common/DataProvider/ParachainStakingLocalSubscriptionFactory.swift +++ b/novawallet/Common/DataProvider/ParachainStakingLocalSubscriptionFactory.swift @@ -229,7 +229,8 @@ final class ParachainStakingLocalSubscriptionFactory: SubstrateLocalSubscription startTimestamp: startTimestamp, endTimestamp: endTimestamp, assetPrecision: assetPrecision, - operationFactory: operationFactory + operationFactory: operationFactory, + stakingType: .direct ) let anySource = AnySingleValueProviderSource(source) diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift b/novawallet/Common/DataProvider/Sources/ExternalAssetBalance/ExternalAssetBalanceStreambleSource.swift similarity index 57% rename from novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift rename to novawallet/Common/DataProvider/Sources/ExternalAssetBalance/ExternalAssetBalanceStreambleSource.swift index 91d6c51289..a9172a83de 100644 --- a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionStreamableSource.swift +++ b/novawallet/Common/DataProvider/Sources/ExternalAssetBalance/ExternalAssetBalanceStreambleSource.swift @@ -1,33 +1,42 @@ import Foundation import RobinHood -final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { - typealias Model = CrowdloanContributionData +final class ExternalAssetBalanceStreambleSource: StreamableSourceProtocol { + typealias Model = ExternalAssetBalance typealias CommitNotificationBlock = ((Result?) -> Void) - let syncServices: [SyncServiceProtocol] - let chainId: ChainModel.Id + let automaticSyncServices: [SyncServiceProtocol] + let pollingSyncServices: [SyncServiceProtocol] + let chainAssetId: ChainAssetId let accountId: AccountId let eventCenter: EventCenterProtocol init( - syncServices: [SyncServiceProtocol], - chainId: ChainModel.Id, + automaticSyncServices: [SyncServiceProtocol], + pollingSyncServices: [SyncServiceProtocol], + chainAssetId: ChainAssetId, accountId: AccountId, eventCenter: EventCenterProtocol ) { - self.syncServices = syncServices - self.eventCenter = eventCenter - self.chainId = chainId + self.automaticSyncServices = automaticSyncServices + self.pollingSyncServices = pollingSyncServices + self.chainAssetId = chainAssetId self.accountId = accountId + self.eventCenter = eventCenter - self.eventCenter.add(observer: self) + eventCenter.add(observer: self) - syncServices.forEach { + (automaticSyncServices + pollingSyncServices).forEach { $0.setup() } } + deinit { + (automaticSyncServices + pollingSyncServices).forEach { + $0.stopSyncUp() + } + } + func fetchHistory( runningIn queue: DispatchQueue?, commitNotificationBlock: CommitNotificationBlock? @@ -47,7 +56,7 @@ final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { runningIn queue: DispatchQueue?, commitNotificationBlock: CommitNotificationBlock? ) { - syncServices.forEach { + pollingSyncServices.forEach { $0.syncUp() } @@ -62,11 +71,12 @@ final class CrowdloanContributionStreamableSource: StreamableSourceProtocol { } } -extension CrowdloanContributionStreamableSource: EventVisitorProtocol { +extension ExternalAssetBalanceStreambleSource: EventVisitorProtocol { func processAssetBalanceChanged(event: AssetBalanceChanged) { - guard event.accountId == accountId, event.chainAssetId.chainId == chainId else { + guard event.accountId == accountId, event.chainAssetId == chainAssetId else { return } + refresh(runningIn: nil, commitNotificationBlock: nil) } } diff --git a/novawallet/Common/DataProvider/Sources/NominationPools/NPoolsPendingRewardDataSource.swift b/novawallet/Common/DataProvider/Sources/NominationPools/NPoolsPendingRewardDataSource.swift new file mode 100644 index 0000000000..9372fcfbbb --- /dev/null +++ b/novawallet/Common/DataProvider/Sources/NominationPools/NPoolsPendingRewardDataSource.swift @@ -0,0 +1,97 @@ +import Foundation +import RobinHood +import SubstrateSdk +import BigInt + +final class NPoolsPendingRewardDataSource { + typealias Model = String + + static var rewardsBuiltIn: String { "NominationPoolsApi_pending_rewards" } + + let accountId: AccountId + let connection: JSONRPCEngine + let runtimeService: RuntimeCodingServiceProtocol + + init( + accountId: AccountId, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol + ) { + self.accountId = accountId + self.connection = connection + self.runtimeService = runtimeService + } + + private func createStateCallOperation( + for accountId: AccountId, + builtInFunction: String, + dependingOn codingFactoryOperation: BaseOperation + ) -> BaseOperation { + ClosureOperation { + let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + let context = codingFactory.createRuntimeJsonContext().toRawContext() + + let encoder = codingFactory.createEncoder() + + try encoder.append( + accountId, + ofType: GenericType.accountId.name, + with: context + ) + + let param = try encoder.encode() + + return StateCallRpc.Request(builtInFunction: builtInFunction) { container in + try container.encode(param.toHex(includePrefix: true)) + } + } + } +} + +extension NPoolsPendingRewardDataSource: SingleValueProviderSourceProtocol { + func fetchOperation() -> CompoundOperationWrapper { + let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() + + let requestOperation = createStateCallOperation( + for: accountId, + builtInFunction: Self.rewardsBuiltIn, + dependingOn: codingFactoryOperation + ) + + requestOperation.addDependency(codingFactoryOperation) + + let infoOperation = JSONRPCOperation( + engine: connection, + method: StateCallRpc.method + ) + + infoOperation.configurationBlock = { + do { + infoOperation.parameters = try requestOperation.extractNoCancellableResultData() + } catch { + infoOperation.result = .failure(error) + } + } + + infoOperation.addDependency(requestOperation) + + let mapOperation = ClosureOperation { + let coderFactory = try codingFactoryOperation.extractNoCancellableResultData() + let result = try infoOperation.extractNoCancellableResultData() + let resultData = try Data(hexString: result) + let decoder = try coderFactory.createDecoder(from: resultData) + let remoteModel = try decoder.read(type: KnownType.balance.name).map( + to: StringScaleMapper.self, + with: coderFactory.createRuntimeJsonContext().toRawContext() + ) + + return String(remoteModel.value) + } + + mapOperation.addDependency(infoOperation) + + let dependencies = [codingFactoryOperation, requestOperation, infoOperation] + + return CompoundOperationWrapper(targetOperation: mapOperation, dependencies: dependencies) + } +} diff --git a/novawallet/Common/DataProvider/Sources/Rewards/SubqueryTotalRewardSource.swift b/novawallet/Common/DataProvider/Sources/Rewards/SubqueryTotalRewardSource.swift index 599166d2dd..f29864fc42 100644 --- a/novawallet/Common/DataProvider/Sources/Rewards/SubqueryTotalRewardSource.swift +++ b/novawallet/Common/DataProvider/Sources/Rewards/SubqueryTotalRewardSource.swift @@ -10,19 +10,22 @@ final class SubqueryTotalRewardSource { let endTimestamp: Int64? let assetPrecision: Int16 let operationFactory: SubqueryRewardOperationFactoryProtocol + let stakingType: SubqueryStakingType init( address: AccountAddress, startTimestamp: Int64?, endTimestamp: Int64?, assetPrecision: Int16, - operationFactory: SubqueryRewardOperationFactoryProtocol + operationFactory: SubqueryRewardOperationFactoryProtocol, + stakingType: SubqueryStakingType ) { self.address = address self.startTimestamp = startTimestamp self.endTimestamp = endTimestamp self.assetPrecision = assetPrecision self.operationFactory = operationFactory + self.stakingType = stakingType } private func createMapOperation( @@ -43,7 +46,8 @@ extension SubqueryTotalRewardSource: SingleValueProviderSourceProtocol { let rewardOperation = operationFactory.createTotalRewardOperation( for: address, startTimestamp: startTimestamp, - endTimestamp: endTimestamp + endTimestamp: endTimestamp, + stakingType: stakingType ) let mapOperation = createMapOperation( diff --git a/novawallet/Common/DataProvider/StakingLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/StakingLocalSubscriptionFactory.swift index 8eab3e2d63..c4fde3a095 100644 --- a/novawallet/Common/DataProvider/StakingLocalSubscriptionFactory.swift +++ b/novawallet/Common/DataProvider/StakingLocalSubscriptionFactory.swift @@ -53,7 +53,8 @@ protocol StakingLocalSubscriptionFactoryProtocol { ) throws -> AnySingleValueProvider func getStashItemProvider( - for address: AccountAddress + for address: AccountAddress, + chainId: ChainModel.Id ) -> StreamableProvider func getBagListNodeProvider( @@ -315,7 +316,8 @@ final class StakingLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, startTimestamp: startTimestamp, endTimestamp: endTimestamp, assetPrecision: assetPrecision, - operationFactory: operationFactory + operationFactory: operationFactory, + stakingType: .direct ) let anySource = AnySingleValueProviderSource(source) @@ -332,11 +334,12 @@ final class StakingLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, } func getStashItemProvider( - for address: AccountAddress + for address: AccountAddress, + chainId: ChainModel.Id ) -> StreamableProvider { clearIfNeeded() - let identifier = "stashItem" + address + let identifier = "stashItem" + address + chainId if let provider = getProvider(for: identifier) as? StreamableProvider { return provider @@ -346,7 +349,7 @@ final class StakingLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, facade: storageFacade, operationManager: operationManager, logger: logger - ).createStashItemProvider(for: address) + ).createStashItemProvider(for: address, chainId: chainId) saveProvider(provider, for: identifier) diff --git a/novawallet/Common/DataProvider/Subscription/CrowdloansLocalStorageSubscriber.swift b/novawallet/Common/DataProvider/Subscription/CrowdloansLocalStorageSubscriber.swift deleted file mode 100644 index e2fcf6197a..0000000000 --- a/novawallet/Common/DataProvider/Subscription/CrowdloansLocalStorageSubscriber.swift +++ /dev/null @@ -1,120 +0,0 @@ -import Foundation -import RobinHood - -protocol CrowdloanContributionLocalSubscriptionHandler: AnyObject { - func handleCrowdloans( - result: Result<[DataProviderChange], Error>, - accountId: AccountId, - chain: ChainModel - ) - - func handleAllCrowdloans(result: Result<[DataProviderChange], Error>) -} - -protocol CrowdloansLocalStorageSubscriber: AnyObject { - var crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol { get } - var crowdloansLocalSubscriptionHandler: CrowdloanContributionLocalSubscriptionHandler { get } - - func subscribeToCrowdloansProvider( - for account: AccountId, - chain: ChainModel - ) -> StreamableProvider? - - func subscribeToAllCrowdloansProvider() -> 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 - } - - 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/Common/DataProvider/Subscription/DecodedProviderTypes.swift b/novawallet/Common/DataProvider/Subscription/DecodedProviderTypes.swift index a796df1888..84fa10d1b9 100644 --- a/novawallet/Common/DataProvider/Subscription/DecodedProviderTypes.swift +++ b/novawallet/Common/DataProvider/Subscription/DecodedProviderTypes.swift @@ -4,6 +4,7 @@ import BigInt typealias DecodedBigUInt = ChainStorageDecodedItem> typealias DecodedU32 = ChainStorageDecodedItem> +typealias DecodedBytes = ChainStorageDecodedItem typealias DecodedNomination = ChainStorageDecodedItem typealias DecodedValidator = ChainStorageDecodedItem typealias DecodedLedgerInfo = ChainStorageDecodedItem @@ -13,3 +14,8 @@ typealias DecodedPayee = ChainStorageDecodedItem typealias DecodedBlockNumber = ChainStorageDecodedItem> typealias DecodedCrowdloanFunds = ChainStorageDecodedItem typealias DecodedBagListNode = ChainStorageDecodedItem +typealias DecodedPoolMember = ChainStorageDecodedItem +typealias DecodedBondedPool = ChainStorageDecodedItem +typealias DecodedRewardPool = ChainStorageDecodedItem +typealias DecodedSubPools = ChainStorageDecodedItem +typealias DecodedPoolId = ChainStorageDecodedItem> diff --git a/novawallet/Common/DataProvider/Subscription/ExternalAssetBalanceSubscriber.swift b/novawallet/Common/DataProvider/Subscription/ExternalAssetBalanceSubscriber.swift new file mode 100644 index 0000000000..badff11a29 --- /dev/null +++ b/novawallet/Common/DataProvider/Subscription/ExternalAssetBalanceSubscriber.swift @@ -0,0 +1,101 @@ +import Foundation +import RobinHood + +protocol ExternalAssetBalanceSubscriber: AnyObject { + var externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactoryProtocol { get } + var externalBalancesSubscriptionHandler: ExternalAssetBalanceSubscriptionHandler { get } + + func subscribeToExternalAssetBalancesProvider( + for accountId: AccountId, + chainAsset: ChainAsset + ) -> StreamableProvider? + + func subscribeToAllExternalAssetBalancesProvider() -> StreamableProvider? +} + +extension ExternalAssetBalanceSubscriber { + func subscribeToExternalAssetBalancesProvider( + for accountId: AccountId, + chainAsset: ChainAsset + ) -> StreamableProvider? { + guard + let provider = externalBalancesSubscriptionFactory.getExternalAssetBalanceProvider( + for: accountId, + chainAsset: chainAsset + ) else { + return nil + } + + let updateClosure = { [weak self] (changes: [DataProviderChange]) in + self?.externalBalancesSubscriptionHandler.handleExternalAssetBalances( + result: .success(changes), + accountId: accountId, + chainAsset: chainAsset + ) + return + } + + let failureClosure = { [weak self] (error: Error) in + self?.externalBalancesSubscriptionHandler.handleExternalAssetBalances( + result: .failure(error), + accountId: accountId, + chainAsset: chainAsset + ) + 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 + } + + func subscribeToAllExternalAssetBalancesProvider() -> StreamableProvider? { + guard let provider = externalBalancesSubscriptionFactory.getAllExternalAssetBalanceProvider() else { + return nil + } + + let updateClosure = { [weak self] (changes: [DataProviderChange]) in + self?.externalBalancesSubscriptionHandler.handleAllExternalAssetBalances(result: .success(changes)) + return + } + + let failureClosure = { [weak self] (error: Error) in + self?.externalBalancesSubscriptionHandler.handleAllExternalAssetBalances(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 ExternalAssetBalanceSubscriber where Self: ExternalAssetBalanceSubscriptionHandler { + var externalBalancesSubscriptionHandler: ExternalAssetBalanceSubscriptionHandler { self } +} diff --git a/novawallet/Common/DataProvider/Subscription/ExternalAssetBalanceSubscriptionHandler.swift b/novawallet/Common/DataProvider/Subscription/ExternalAssetBalanceSubscriptionHandler.swift new file mode 100644 index 0000000000..5660f037ca --- /dev/null +++ b/novawallet/Common/DataProvider/Subscription/ExternalAssetBalanceSubscriptionHandler.swift @@ -0,0 +1,26 @@ +import Foundation +import RobinHood + +protocol ExternalAssetBalanceSubscriptionHandler: AnyObject { + func handleExternalAssetBalances( + result: Result<[DataProviderChange], Error>, + accountId: AccountId, + chainAsset: ChainAsset + ) + + func handleAllExternalAssetBalances( + result: Result<[DataProviderChange], Error> + ) +} + +extension ExternalAssetBalanceSubscriptionHandler { + func handleExternalAssetBalances( + result _: Result<[DataProviderChange], Error>, + accountId _: AccountId, + chainAsset _: ChainAsset + ) {} + + func handleAllExternalAssetBalances( + result _: Result<[DataProviderChange], Error> + ) {} +} diff --git a/novawallet/Common/DataProvider/Subscription/NPoolsLocalStorageHandler.swift b/novawallet/Common/DataProvider/Subscription/NPoolsLocalStorageHandler.swift new file mode 100644 index 0000000000..3e3ad0a972 --- /dev/null +++ b/novawallet/Common/DataProvider/Subscription/NPoolsLocalStorageHandler.swift @@ -0,0 +1,116 @@ +import Foundation +import BigInt + +protocol NPoolsLocalSubscriptionHandler { + func handlePoolMember( + result: Result, + accountId: AccountId, + chainId: ChainModel.Id + ) + + func handleBondedPool( + result: Result, + poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) + + func handlePoolMetadata( + result: Result, + poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) + + func handleRewardPool( + result: Result, + poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) + + func handleSubPools( + result: Result, + poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) + + func handleMinJoinBond(result: Result, chainId: ChainModel.Id) + + func handleLastPoolId(result: Result, chainId: ChainModel.Id) + + func handleMaxPoolMembers(result _: Result, chainId _: ChainModel.Id) + + func handleCounterForPoolMembers(result _: Result, chainId _: ChainModel.Id) + + func handleMaxPoolMembersPerPool(result _: Result, chainId _: ChainModel.Id) + + func handleClaimableRewards( + result: Result, + chainId: ChainModel.Id, + poolId: NominationPools.PoolId, + accountId: AccountId + ) + + func handlePoolTotalReward( + result _: Result, + for _: AccountAddress, + startTimestamp: Int64?, + endTimestamp: Int64?, + api _: LocalChainExternalApi + ) +} + +extension NPoolsLocalSubscriptionHandler { + func handlePoolMember( + result _: Result, + accountId _: AccountId, + chainId _: ChainModel.Id + ) {} + + func handleBondedPool( + result _: Result, + poolId _: NominationPools.PoolId, + chainId _: ChainModel.Id + ) {} + + func handlePoolMetadata( + result _: Result, + poolId _: NominationPools.PoolId, + chainId _: ChainModel.Id + ) {} + + func handleRewardPool( + result _: Result, + poolId _: NominationPools.PoolId, + chainId _: ChainModel.Id + ) {} + + func handleSubPools( + result _: Result, + poolId _: NominationPools.PoolId, + chainId _: ChainModel.Id + ) {} + + func handleMinJoinBond(result _: Result, chainId _: ChainModel.Id) {} + + func handleLastPoolId(result _: Result, chainId _: ChainModel.Id) {} + + func handleMaxPoolMembers(result _: Result, chainId _: ChainModel.Id) {} + + func handleCounterForPoolMembers(result _: Result, chainId _: ChainModel.Id) {} + + func handleMaxPoolMembersPerPool(result _: Result, chainId _: ChainModel.Id) {} + + func handleClaimableRewards( + result _: Result, + chainId _: ChainModel.Id, + poolId _: NominationPools.PoolId, + accountId _: AccountId + ) {} + + func handlePoolTotalReward( + result _: Result, + for _: AccountAddress, + startTimestamp _: Int64?, + endTimestamp _: Int64?, + api _: LocalChainExternalApi + ) {} +} diff --git a/novawallet/Common/DataProvider/Subscription/NPoolsLocalStorageSubscriber.swift b/novawallet/Common/DataProvider/Subscription/NPoolsLocalStorageSubscriber.swift new file mode 100644 index 0000000000..7850cf8e72 --- /dev/null +++ b/novawallet/Common/DataProvider/Subscription/NPoolsLocalStorageSubscriber.swift @@ -0,0 +1,453 @@ +import Foundation +import RobinHood +import SubstrateSdk +import BigInt + +protocol NPoolsLocalStorageSubscriber: LocalStorageProviderObserving where Self: AnyObject { + var npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol { get } + + var npoolsLocalSubscriptionHandler: NPoolsLocalSubscriptionHandler { get } + + func subscribePoolMember( + for accountId: AccountId, + chainId: ChainModel.Id, + callbackQueue: DispatchQueue + ) -> AnyDataProvider? + + func subscribeBondedPool( + for poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) -> AnyDataProvider? + + func subscribePoolMetadata( + for poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) -> AnyDataProvider? + + func subscribeRewardPool( + for poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) -> AnyDataProvider? + + func subscribeSubPools( + for poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) -> AnyDataProvider? + + func subscribeMinJoinBond(for chainId: ChainModel.Id) -> AnyDataProvider? + + func subscribeLastPoolId( + for chainId: ChainModel.Id, + callbackQueue: DispatchQueue + ) -> AnyDataProvider? + + func subscribeMaxPoolMembers(for chainId: ChainModel.Id) + -> AnyDataProvider? + + func subscribeCounterForPoolMembers(for chainId: ChainModel.Id) + -> AnyDataProvider? + + func subscribeMaxPoolMembersPerPool(for chainId: ChainModel.Id) + -> AnyDataProvider? + + func subscribeClaimableRewards( + for chainId: ChainModel.Id, + poolId: NominationPools.PoolId, + accountId: AccountId + ) -> AnySingleValueProvider? + + func subscribePoolTotalReward( + for address: AccountAddress, + startTimestamp: Int64?, + endTimestamp: Int64?, + api: LocalChainExternalApi, + assetPrecision: Int16 + ) -> AnySingleValueProvider? +} + +extension NPoolsLocalStorageSubscriber where Self: NPoolsLocalSubscriptionHandler { + var npoolsLocalSubscriptionHandler: NPoolsLocalSubscriptionHandler { self } + + func subscribePoolMember( + for accountId: AccountId, + chainId: ChainModel.Id + ) -> AnyDataProvider? { + subscribePoolMember(for: accountId, chainId: chainId, callbackQueue: .main) + } + + func subscribePoolMember( + for accountId: AccountId, + chainId: ChainModel.Id, + callbackQueue: DispatchQueue + ) -> AnyDataProvider? { + guard + let provider = try? npoolsLocalSubscriptionFactory.getPoolMemberProvider( + for: accountId, + chainId: chainId + ) else { + return nil + } + + addDataProviderObserver( + for: provider, + updateClosure: { [weak self] value in + self?.npoolsLocalSubscriptionHandler.handlePoolMember( + result: .success(value), + accountId: accountId, + chainId: chainId + ) + }, + failureClosure: { [weak self] error in + self?.npoolsLocalSubscriptionHandler.handlePoolMember( + result: .failure(error), + accountId: accountId, + chainId: chainId + ) + }, + callbackQueue: callbackQueue, + options: .init(alwaysNotifyOnRefresh: false, waitsInProgressSyncOnAdd: false) + ) + + return provider + } + + func subscribeBondedPool( + for poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) -> AnyDataProvider? { + guard + let provider = try? npoolsLocalSubscriptionFactory.getBondedPoolProvider( + for: poolId, + chainId: chainId + ) else { + return nil + } + + addDataProviderObserver( + for: provider, + updateClosure: { [weak self] value in + self?.npoolsLocalSubscriptionHandler.handleBondedPool( + result: .success(value), + poolId: poolId, + chainId: chainId + ) + }, failureClosure: { [weak self] error in + self?.npoolsLocalSubscriptionHandler.handleBondedPool( + result: .failure(error), + poolId: poolId, + chainId: chainId + ) + } + ) + + return provider + } + + func subscribePoolMetadata( + for poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) -> AnyDataProvider? { + guard + let provider = try? npoolsLocalSubscriptionFactory.getMetadataProvider( + for: poolId, + chainId: chainId + ) else { + return nil + } + + addDataProviderObserver( + for: provider, + updateClosure: { [weak self] value in + self?.npoolsLocalSubscriptionHandler.handlePoolMetadata( + result: .success(value?.wrappedValue), + poolId: poolId, + chainId: chainId + ) + }, failureClosure: { [weak self] error in + self?.npoolsLocalSubscriptionHandler.handlePoolMetadata( + result: .failure(error), + poolId: poolId, + chainId: chainId + ) + } + ) + + return provider + } + + func subscribeRewardPool( + for poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) -> AnyDataProvider? { + guard + let provider = try? npoolsLocalSubscriptionFactory.getRewardPoolProvider( + for: poolId, + chainId: chainId + ) else { + return nil + } + + addDataProviderObserver( + for: provider, + updateClosure: { [weak self] value in + self?.npoolsLocalSubscriptionHandler.handleRewardPool( + result: .success(value), + poolId: poolId, + chainId: chainId + ) + }, failureClosure: { [weak self] error in + self?.npoolsLocalSubscriptionHandler.handleRewardPool( + result: .failure(error), + poolId: poolId, + chainId: chainId + ) + } + ) + + return provider + } + + func subscribeSubPools( + for poolId: NominationPools.PoolId, + chainId: ChainModel.Id + ) -> AnyDataProvider? { + guard + let provider = try? npoolsLocalSubscriptionFactory.getSubPoolsProvider( + for: poolId, + chainId: chainId + ) else { + return nil + } + + addDataProviderObserver( + for: provider, + updateClosure: { [weak self] value in + self?.npoolsLocalSubscriptionHandler.handleSubPools( + result: .success(value), + poolId: poolId, + chainId: chainId + ) + }, failureClosure: { [weak self] error in + self?.npoolsLocalSubscriptionHandler.handleSubPools( + result: .failure(error), + poolId: poolId, + chainId: chainId + ) + } + ) + + return provider + } + + func subscribeMinJoinBond(for chainId: ChainModel.Id) -> AnyDataProvider? { + guard let provider = try? npoolsLocalSubscriptionFactory.getMinJoinBondProvider(for: chainId) else { + return nil + } + + addDataProviderObserver( + for: provider, + updateClosure: { [weak self] value in + self?.npoolsLocalSubscriptionHandler.handleMinJoinBond( + result: .success(value?.value), + chainId: chainId + ) + }, + failureClosure: { [weak self] error in + self?.npoolsLocalSubscriptionHandler.handleMinJoinBond( + result: .failure(error), + chainId: chainId + ) + } + ) + + return provider + } + + func subscribeLastPoolId(for chainId: ChainModel.Id) -> AnyDataProvider? { + subscribeLastPoolId(for: chainId, callbackQueue: .main) + } + + func subscribeLastPoolId( + for chainId: ChainModel.Id, + callbackQueue: DispatchQueue + ) -> AnyDataProvider? { + guard let provider = try? npoolsLocalSubscriptionFactory.getLastPoolIdProvider(for: chainId) else { + return nil + } + + addDataProviderObserver( + for: provider, + updateClosure: { [weak self] value in + self?.npoolsLocalSubscriptionHandler.handleLastPoolId( + result: .success(value?.value), + chainId: chainId + ) + }, failureClosure: { [weak self] error in + self?.npoolsLocalSubscriptionHandler.handleLastPoolId( + result: .failure(error), + chainId: chainId + ) + }, + callbackQueue: callbackQueue, + options: .init(alwaysNotifyOnRefresh: false, waitsInProgressSyncOnAdd: false) + ) + + return provider + } + + func subscribeMaxPoolMembers(for chainId: ChainModel.Id) -> AnyDataProvider? { + guard let provider = try? npoolsLocalSubscriptionFactory.getMaxPoolMembers( + for: chainId, + missingEntryStrategy: .defaultValue(StringScaleMapper(value: UInt32.max)) + ) else { + return nil + } + + addDataProviderObserver( + for: provider, + updateClosure: { [weak self] value in + self?.npoolsLocalSubscriptionHandler.handleMaxPoolMembers( + result: .success(value?.value), + chainId: chainId + ) + }, failureClosure: { [weak self] error in + self?.npoolsLocalSubscriptionHandler.handleMaxPoolMembers( + result: .failure(error), + chainId: chainId + ) + } + ) + + return provider + } + + func subscribeCounterForPoolMembers(for chainId: ChainModel.Id) -> AnyDataProvider? { + guard let provider = try? npoolsLocalSubscriptionFactory.getCounterForPoolMembers( + for: chainId, + missingEntryStrategy: .defaultValue(StringScaleMapper(value: 0)) + ) else { + return nil + } + + addDataProviderObserver( + for: provider, + updateClosure: { [weak self] value in + self?.npoolsLocalSubscriptionHandler.handleCounterForPoolMembers( + result: .success(value?.value), + chainId: chainId + ) + }, failureClosure: { [weak self] error in + self?.npoolsLocalSubscriptionHandler.handleCounterForPoolMembers( + result: .failure(error), + chainId: chainId + ) + } + ) + + return provider + } + + func subscribeMaxPoolMembersPerPool(for chainId: ChainModel.Id) -> AnyDataProvider? { + guard let provider = try? npoolsLocalSubscriptionFactory.getMaxMembersPerPool( + for: chainId, + missingEntryStrategy: .defaultValue(StringScaleMapper(value: UInt32.max)) + ) else { + return nil + } + + addDataProviderObserver( + for: provider, + updateClosure: { [weak self] value in + self?.npoolsLocalSubscriptionHandler.handleMaxPoolMembersPerPool( + result: .success(value?.value), + chainId: chainId + ) + }, failureClosure: { [weak self] error in + self?.npoolsLocalSubscriptionHandler.handleMaxPoolMembersPerPool( + result: .failure(error), + chainId: chainId + ) + } + ) + + return provider + } + + func subscribeClaimableRewards( + for chainId: ChainModel.Id, + poolId: NominationPools.PoolId, + accountId: AccountId + ) -> AnySingleValueProvider? { + guard + let provider = try? npoolsLocalSubscriptionFactory.getClaimableRewards( + for: chainId, + accountId: accountId + ) else { + return nil + } + + addSingleValueProviderObserver( + for: provider, + updateClosure: { [weak self] value in + let amount = value.flatMap { BigUInt($0) } + + self?.npoolsLocalSubscriptionHandler.handleClaimableRewards( + result: .success(amount), + chainId: chainId, + poolId: poolId, + accountId: accountId + ) + }, failureClosure: { [weak self] error in + self?.npoolsLocalSubscriptionHandler.handleClaimableRewards( + result: .failure(error), + chainId: chainId, + poolId: poolId, + accountId: accountId + ) + } + ) + + return provider + } + + func subscribePoolTotalReward( + for address: AccountAddress, + startTimestamp: Int64?, + endTimestamp: Int64?, + api: LocalChainExternalApi, + assetPrecision: Int16 + ) -> AnySingleValueProvider? { + guard let provider = try? npoolsLocalSubscriptionFactory.getTotalReward( + for: address, + startTimestamp: startTimestamp, + endTimestamp: endTimestamp, + api: api, + assetPrecision: assetPrecision + ) else { + return nil + } + + addSingleValueProviderObserver( + for: provider, + updateClosure: { [weak self] value in + self?.handlePoolTotalReward( + result: .success(value), + for: address, + startTimestamp: startTimestamp, + endTimestamp: endTimestamp, + api: api + ) + }, failureClosure: { [weak self] error in + self?.handlePoolTotalReward( + result: .failure(error), + for: address, + startTimestamp: startTimestamp, + endTimestamp: endTimestamp, + api: api + ) + } + ) + + return provider + } +} diff --git a/novawallet/Common/DataProvider/Subscription/StakingLocalStorageSubscriber.swift b/novawallet/Common/DataProvider/Subscription/StakingLocalStorageSubscriber.swift index 3b18db8442..64eefef189 100644 --- a/novawallet/Common/DataProvider/Subscription/StakingLocalStorageSubscriber.swift +++ b/novawallet/Common/DataProvider/Subscription/StakingLocalStorageSubscriber.swift @@ -52,7 +52,8 @@ protocol StakingLocalStorageSubscriber where Self: AnyObject { ) -> AnySingleValueProvider? func subscribeStashItemProvider( - for address: AccountAddress + for address: AccountAddress, + chainId: ChainModel.Id ) -> StreamableProvider? } @@ -621,9 +622,10 @@ extension StakingLocalStorageSubscriber { } func subscribeStashItemProvider( - for address: AccountAddress + for address: AccountAddress, + chainId: ChainModel.Id ) -> StreamableProvider? { - let provider = stakingLocalSubscriptionFactory.getStashItemProvider(for: address) + let provider = stakingLocalSubscriptionFactory.getStashItemProvider(for: address, chainId: chainId) let changesClosure: ([DataProviderChange]) -> Void = { [weak self] changes in let stashItem = changes.reduceToLastChange() diff --git a/novawallet/Common/DataProvider/SubstrateDataProviderFactory.swift b/novawallet/Common/DataProvider/SubstrateDataProviderFactory.swift index 41c4ad30f7..ab2126e97e 100644 --- a/novawallet/Common/DataProvider/SubstrateDataProviderFactory.swift +++ b/novawallet/Common/DataProvider/SubstrateDataProviderFactory.swift @@ -2,7 +2,7 @@ import Foundation import RobinHood protocol SubstrateDataProviderFactoryProtocol { - func createStashItemProvider(for address: String) -> StreamableProvider + func createStashItemProvider(for address: String, chainId: ChainModel.Id) -> StreamableProvider func createStorageProvider(for key: String) -> StreamableProvider } @@ -21,22 +21,16 @@ final class SubstrateDataProviderFactory: SubstrateDataProviderFactoryProtocol { self.logger = logger } - func createStashItemProvider(for address: String) -> StreamableProvider { - let mapper: CodableCoreDataMapper = - CodableCoreDataMapper(entityIdentifierFieldName: #keyPath(CDStashItem.stash)) + func createStashItemProvider(for address: String, chainId: ChainModel.Id) -> StreamableProvider { + let mapper = StashItemMapper() - let filter = NSPredicate.filterByStashOrController(address) - let repository: CoreDataRepository = facade - .createRepository( - filter: filter, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) + let repository = SubstrateRepositoryFactory(storageFacade: facade) + .createStashItemRepository(for: address, chainId: chainId) let observable = CoreDataContextObservable( service: facade.databaseService, mapper: AnyCoreDataMapper(mapper), - predicate: { $0.stash == address || $0.controller == address } + predicate: { ($0.stash == address || $0.controller == address) && $0.chainId == chainId } ) observable.start { [weak self] error in @@ -47,7 +41,7 @@ final class SubstrateDataProviderFactory: SubstrateDataProviderFactoryProtocol { return StreamableProvider( source: AnyStreamableSource(EmptyStreamableSource()), - repository: AnyDataProviderRepository(repository), + repository: repository, observable: AnyDataProviderRepositoryObservable(observable), operationManager: operationManager ) diff --git a/novawallet/Common/DataProvider/SubstrateLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/SubstrateLocalSubscriptionFactory.swift index 59da8e8248..638a850177 100644 --- a/novawallet/Common/DataProvider/SubstrateLocalSubscriptionFactory.swift +++ b/novawallet/Common/DataProvider/SubstrateLocalSubscriptionFactory.swift @@ -1,5 +1,6 @@ import Foundation import RobinHood +import SubstrateSdk class SubstrateLocalSubscriptionFactory { private var providers: [String: WeakWrapper] = [:] @@ -147,4 +148,56 @@ class SubstrateLocalSubscriptionFactory { return AnyDataProvider(dataProvider) } + + func getNoFallbackPathProvider( + for storagePath: StorageCodingPath, + chainId: ChainModel.Id + ) throws -> AnyDataProvider> { + let localKey = try LocalStorageKeyFactory().createFromStoragePath(storagePath, chainId: chainId) + + return try getDataProvider( + for: localKey, + chainId: chainId, + storageCodingPath: storagePath, + shouldUseFallback: false + ) + } + + func getNoFallbackAccountProvider( + for storagePath: StorageCodingPath, + accountId: AccountId, + chainId: ChainModel.Id + ) throws -> AnyDataProvider> { + let localKey = try LocalStorageKeyFactory().createFromStoragePath( + storagePath, + accountId: accountId, + chainId: chainId + ) + + return try getDataProvider( + for: localKey, + chainId: chainId, + storageCodingPath: storagePath, + shouldUseFallback: false + ) + } + + func getNoFallbackScalingElementProvider( + for storagePath: StorageCodingPath, + encodableElement: E, + chainId: ChainModel.Id + ) throws -> AnyDataProvider> { + let localKey = try LocalStorageKeyFactory().createFromStoragePath( + storagePath, + encodableElement: encodableElement, + chainId: chainId + ) + + return try getDataProvider( + for: localKey, + chainId: chainId, + storageCodingPath: storagePath, + shouldUseFallback: false + ) + } } diff --git a/novawallet/Common/EventCenter/EventVisitor.swift b/novawallet/Common/EventCenter/EventVisitor.swift index aceec32a63..93a7cef17f 100644 --- a/novawallet/Common/EventCenter/EventVisitor.swift +++ b/novawallet/Common/EventCenter/EventVisitor.swift @@ -9,6 +9,7 @@ protocol EventVisitorProtocol: AnyObject { func processPurchaseCompletion(event: PurchaseCompleted) func processTypeRegistryPrepared(event: TypeRegistryPrepared) func processEraStakersInfoChanged(event: EraStakersInfoChanged) + func processEraNominationPoolsChanged(event: EraNominationPoolsChanged) func processChainSyncDidStart(event: ChainSyncDidStart) func processChainSyncDidComplete(event: ChainSyncDidComplete) @@ -37,6 +38,7 @@ extension EventVisitorProtocol { func processPurchaseCompletion(event _: PurchaseCompleted) {} func processTypeRegistryPrepared(event _: TypeRegistryPrepared) {} func processEraStakersInfoChanged(event _: EraStakersInfoChanged) {} + func processEraNominationPoolsChanged(event _: EraNominationPoolsChanged) {} func processChainSyncDidStart(event _: ChainSyncDidStart) {} func processChainSyncDidComplete(event _: ChainSyncDidComplete) {} diff --git a/novawallet/Common/EventCenter/Events/EraNominationPoolsChanged.swift b/novawallet/Common/EventCenter/Events/EraNominationPoolsChanged.swift new file mode 100644 index 0000000000..eff7d7f530 --- /dev/null +++ b/novawallet/Common/EventCenter/Events/EraNominationPoolsChanged.swift @@ -0,0 +1,7 @@ +import Foundation + +struct EraNominationPoolsChanged: EventProtocol { + func accept(visitor: EventVisitorProtocol) { + visitor.processEraNominationPoolsChanged(event: self) + } +} diff --git a/novawallet/Common/Extension/Foundation/NSPredicate+ExternalAssetBalance.swift b/novawallet/Common/Extension/Foundation/NSPredicate+ExternalAssetBalance.swift new file mode 100644 index 0000000000..3a38099002 --- /dev/null +++ b/novawallet/Common/Extension/Foundation/NSPredicate+ExternalAssetBalance.swift @@ -0,0 +1,28 @@ +import Foundation + +extension NSPredicate { + static func externalAssetBalance( + for chainAssetId: ChainAssetId, + accountId: AccountId + ) -> NSPredicate { + let chainPredicate = NSPredicate( + format: "%K == %@", + #keyPath(CDExternalBalance.chainId), + chainAssetId.chainId + ) + + let assetPredicate = NSPredicate( + format: "%K == %d", + #keyPath(CDExternalBalance.assetId), + chainAssetId.assetId + ) + + let accountPredicate = NSPredicate( + format: "%K == %@", + #keyPath(CDExternalBalance.chainAccountId), + accountId.toHex() + ) + + return NSCompoundPredicate(andPredicateWithSubpredicates: [chainPredicate, assetPredicate, accountPredicate]) + } +} diff --git a/novawallet/Common/Extension/Foundation/NSPredicate+Filter.swift b/novawallet/Common/Extension/Foundation/NSPredicate+Filter.swift index f59ba72e33..1d37b1c665 100644 --- a/novawallet/Common/Extension/Foundation/NSPredicate+Filter.swift +++ b/novawallet/Common/Extension/Foundation/NSPredicate+Filter.swift @@ -18,15 +18,16 @@ extension NSPredicate { NSPredicate(format: "%K BEGINSWITH %@", #keyPath(CDChainStorageItem.identifier), prefix) } - static func filterByStash(_ address: String) -> NSPredicate { - NSPredicate(format: "%K == %@", #keyPath(CDStashItem.stash), address) - } + static func filterByStashOrController(_ address: String, chainId: ChainModel.Id) -> NSPredicate { + let stashFilter = NSPredicate(format: "%K == %@", #keyPath(CDStashItem.stash), address) + let controllerFiter = NSPredicate(format: "%K == %@", #keyPath(CDStashItem.controller), address) + let chainIdFilter = NSPredicate(format: "%K == %@", #keyPath(CDStashItem.chainId), chainId) - static func filterByStashOrController(_ address: String) -> NSPredicate { - let stash = filterByStash(address) - let controller = NSPredicate(format: "%K == %@", #keyPath(CDStashItem.controller), address) + let stashOrControllerFilter = NSCompoundPredicate( + orPredicateWithSubpredicates: [stashFilter, controllerFiter] + ) - return NSCompoundPredicate(orPredicateWithSubpredicates: [stash, controller]) + return NSCompoundPredicate(andPredicateWithSubpredicates: [stashOrControllerFilter, chainIdFilter]) } static func filterMetaAccountByAccountId(_ accountId: AccountId) -> NSPredicate { @@ -265,10 +266,19 @@ extension NSPredicate { } static func crowdloanContribution(chainIds: Set) -> NSPredicate { - let predicates = chainIds.map { - NSPredicate(format: "%K == %@", #keyPath(CDCrowdloanContribution.chainId), $0) + let crowdloanTypePredicate = NSPredicate( + format: "%K == %@", + #keyPath(CDExternalBalance.type), + ExternalAssetBalance.BalanceType.crowdloan.rawValue + ) + + let chainIdPredicates = chainIds.map { + NSPredicate(format: "%K == %@", #keyPath(CDExternalBalance.chainId), $0) } - return NSCompoundPredicate(orPredicateWithSubpredicates: Array(predicates)) + + let chainIdPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: Array(chainIdPredicates)) + + return NSCompoundPredicate(andPredicateWithSubpredicates: [crowdloanTypePredicate, chainIdPredicate]) } static func crowdloanContribution( @@ -278,10 +288,11 @@ extension NSPredicate { ) -> 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)) + NSPredicate(format: "%K == %@", #keyPath(CDExternalBalance.subtype), $0) + } ?? NSPredicate(format: "%K = nil", #keyPath(CDExternalBalance.subtype)) - return NSCompoundPredicate(andPredicateWithSubpredicates: [accountChainPredicate, sourcePredicate]) + let predicates = [accountChainPredicate, sourcePredicate] + return NSCompoundPredicate(andPredicateWithSubpredicates: predicates) } static func crowdloanContribution( @@ -290,16 +301,22 @@ extension NSPredicate { ) -> NSPredicate { let chainPredicate = NSPredicate( format: "%K == %@", - #keyPath(CDCrowdloanContribution.chainId), + #keyPath(CDExternalBalance.chainId), chainId ) let accountPredicate = NSPredicate( format: "%K == %@", - #keyPath(CDCrowdloanContribution.chainAccountId), + #keyPath(CDExternalBalance.chainAccountId), accountId.toHex() ) - return NSCompoundPredicate(andPredicateWithSubpredicates: [chainPredicate, accountPredicate]) + let typePredicate = NSPredicate( + format: "%K == %@", + #keyPath(CDExternalBalance.type), + ExternalAssetBalance.BalanceType.crowdloan.rawValue + ) + + return NSCompoundPredicate(andPredicateWithSubpredicates: [typePredicate, chainPredicate, accountPredicate]) } static func referendums(for chainId: ChainModel.Id) -> NSPredicate { diff --git a/novawallet/Common/Extension/Foundation/String+Helpers.swift b/novawallet/Common/Extension/Foundation/String+Helpers.swift index c95ef2c953..de8355e029 100644 --- a/novawallet/Common/Extension/Foundation/String+Helpers.swift +++ b/novawallet/Common/Extension/Foundation/String+Helpers.swift @@ -3,6 +3,7 @@ import Foundation extension String { static var returnKey: String { "\n" } static var readMore: String { "..." } + static var empty: String = "" func firstLetterCapitalized() -> String { prefix(1).capitalized + dropFirst() diff --git a/novawallet/Common/Extension/Foundation/TimeInterval+Localization.swift b/novawallet/Common/Extension/Foundation/TimeInterval+Localization.swift index a87b82af41..e2e3dd7598 100644 --- a/novawallet/Common/Extension/Foundation/TimeInterval+Localization.swift +++ b/novawallet/Common/Extension/Foundation/TimeInterval+Localization.swift @@ -2,9 +2,60 @@ import Foundation import SoraFoundation extension TimeInterval { - func localizedDaysHours(for locale: Locale) -> String { + func localizedDaysHours( + for locale: Locale, + preposition: String? = nil, + separator: String = " ", + shortcutHandler: PredefinedTimeShortcutProtocol? = nil, + roundsDown: Bool = true + ) -> String { + if + let shortcut = shortcutHandler?.getShortcut( + for: self, + roundsDown: roundsDown, + locale: locale + ) { + return shortcut + } + + let (days, hours) = getDaysAndHours(roundingDown: roundsDown) + + var components: [String] = [] + + if days > 0 { + let daysString = R.string.localizable.commonDaysFormat( + format: days, preferredLanguages: locale.rLanguages + ) + + components.append(daysString) + } + + if hours > 0 { + let hoursString = R.string.localizable.commonHoursFormat( + format: hours, preferredLanguages: locale.rLanguages + ) + + components.append(hoursString) + } + + let timeString = components.joined(separator: separator) + + if let preposition = preposition, !preposition.isEmpty { + return preposition + " " + timeString + } else { + return timeString + } + } + + func localizedDaysHoursMinutes( + for locale: Locale, + preposition: String = "", + separator: String = " " + ) -> String { let days = daysFromSeconds let hours = (self - TimeInterval(days).secondsFromDays).hoursFromSeconds + let minutes = (self - TimeInterval(days).secondsFromDays - + TimeInterval(hours).secondsFromHours).minutesFromSeconds var components: [String] = [] @@ -24,7 +75,18 @@ extension TimeInterval { components.append(hoursString) } - return components.joined(separator: " ") + if minutes > 0, components.count < 2 { + let minutesString = R.string.localizable.commonMinutesFormat( + format: minutes, preferredLanguages: locale.rLanguages + ) + + components.append(minutesString) + } + + return [ + preposition, + components.joined(separator: separator) + ].joined(with: .space) } func localizedDaysHoursIncludingZero(for locale: Locale) -> String { diff --git a/novawallet/Common/Extension/Foundation/TimeInterval+Time.swift b/novawallet/Common/Extension/Foundation/TimeInterval+Time.swift index efb4045b0f..23dae6bd2a 100644 --- a/novawallet/Common/Extension/Foundation/TimeInterval+Time.swift +++ b/novawallet/Common/Extension/Foundation/TimeInterval+Time.swift @@ -35,4 +35,30 @@ extension TimeInterval { return UInt64(nextHour).timeInterval } + + func getDaysAndHours(roundingDown: Bool) -> (Int, Int) { + let days = daysFromSeconds + let hours = (self - TimeInterval(days).secondsFromDays).hoursFromSeconds + + if !roundingDown { + return roundUp(days: days, hours: hours) + } else { + return (days, hours) + } + } + + private func roundUp(days: Int, hours: Int) -> (Int, Int) { + let diff = self - TimeInterval(days).secondsFromDays - TimeInterval(hours).secondsFromHours + let remainedMinutes = diff.minutesFromSeconds + + guard remainedMinutes > TimeInterval.secondsInHour.minutesFromSeconds / 2 else { + return (days, hours) + } + + guard TimeInterval(hours + 1).secondsFromHours.daysFromSeconds == 0 else { + return (days + 1, 0) + } + + return (days, hours + 1) + } } diff --git a/novawallet/Common/Extension/Storage/CDStashItem+CoreDataCodable.swift b/novawallet/Common/Extension/Storage/CDStashItem+CoreDataCodable.swift deleted file mode 100644 index 56f7e66474..0000000000 --- a/novawallet/Common/Extension/Storage/CDStashItem+CoreDataCodable.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation -import CoreData -import RobinHood - -extension CDStashItem: CoreDataCodable { - public func populate(from decoder: Decoder, using _: NSManagedObjectContext) throws { - let stashItem = try StashItem(from: decoder) - - stash = stashItem.stash - controller = stashItem.controller - } - - public func encode(to encoder: Encoder) throws { - guard let stash = stash, let controller = controller else { - return - } - - try StashItem(stash: stash, controller: controller).encode(to: encoder) - } -} diff --git a/novawallet/Common/Extension/UIKit/RoundedView+Styles.swift b/novawallet/Common/Extension/UIKit/RoundedView+Styles.swift index b97c0eef50..14084b477e 100644 --- a/novawallet/Common/Extension/UIKit/RoundedView+Styles.swift +++ b/novawallet/Common/Extension/UIKit/RoundedView+Styles.swift @@ -45,4 +45,10 @@ extension RoundedView { shadowOpacity = 0.0 strokeWidth = 0.0 } + + func applyStrokedBackgroundStyle() { + shadowOpacity = 0.0 + fillColor = .clear + highlightedFillColor = .clear + } } diff --git a/novawallet/Common/Extension/UIKit/Style/UILabel+Style.swift b/novawallet/Common/Extension/UIKit/Style/UILabel+Style.swift index 3333896e14..0de735147e 100644 --- a/novawallet/Common/Extension/UIKit/Style/UILabel+Style.swift +++ b/novawallet/Common/Extension/UIKit/Style/UILabel+Style.swift @@ -56,6 +56,16 @@ extension UILabel.Style { font: .semiBoldFootnote ) + static let semiboldFootnoteButtonInactive = UILabel.Style( + textColor: R.color.colorButtonTextInactive(), + font: .semiBoldFootnote + ) + + static let semiboldFootnoteButtonText = UILabel.Style( + textColor: R.color.colorButtonText(), + font: .semiBoldFootnote + ) + static let semiboldCalloutPositive = UILabel.Style( textColor: R.color.colorTextPositive(), font: .semiBoldCallout @@ -76,6 +86,16 @@ extension UILabel.Style { font: .caption1 ) + static let caption1Tertiary = UILabel.Style( + textColor: R.color.colorTextTertiary(), + font: .caption1 + ) + + static let caption1Positive = UILabel.Style( + textColor: R.color.colorTextPositive(), + font: .caption1 + ) + static let caption2Secondary = UILabel.Style( textColor: R.color.colorTextSecondary(), font: .caption2 diff --git a/novawallet/Common/Extension/UIKit/TriangularedButton+Style.swift b/novawallet/Common/Extension/UIKit/TriangularedButton+Style.swift index bd9daeb0c7..e593c4bdca 100644 --- a/novawallet/Common/Extension/UIKit/TriangularedButton+Style.swift +++ b/novawallet/Common/Extension/UIKit/TriangularedButton+Style.swift @@ -31,10 +31,10 @@ extension TriangularedButton { changesContentOpacityWhenHighlighted = true } - func applyEnabledStyle() { + func applyEnabledStyle(colored color: UIColor = R.color.colorButtonBackgroundPrimary()!) { triangularedView?.shadowOpacity = 0.0 - triangularedView?.fillColor = R.color.colorButtonBackgroundPrimary()! - triangularedView?.highlightedFillColor = R.color.colorButtonBackgroundPrimary()! + triangularedView?.fillColor = color + triangularedView?.highlightedFillColor = color triangularedView?.strokeColor = .clear triangularedView?.highlightedStrokeColor = .clear diff --git a/novawallet/Common/Extension/UIKit/UIFont+Style.swift b/novawallet/Common/Extension/UIKit/UIFont+Style.swift index e3782d855f..8e1a01fa72 100644 --- a/novawallet/Common/Extension/UIKit/UIFont+Style.swift +++ b/novawallet/Common/Extension/UIKit/UIFont+Style.swift @@ -53,6 +53,7 @@ extension UIFont { static var semiBoldBody: UIFont { R.font.publicSansSemiBold(size: 17)! } static var semiBoldFootnote: UIFont { R.font.publicSansSemiBold(size: 13)! } static var semiBoldCallout: UIFont { R.font.publicSansSemiBold(size: 16)! } + static var regularCallout: UIFont { R.font.publicSansRegular(size: 16)! } static var semiBoldCaps2: UIFont { R.font.publicSansSemiBold(size: 10)! } static var semiBoldCaps1: UIFont { R.font.publicSansSemiBold(size: 11)! } static var semiBoldCaption1: UIFont { R.font.publicSansSemiBold(size: 12)! } diff --git a/novawallet/Common/Extension/UIKit/UINavigaionController+Pop.swift b/novawallet/Common/Extension/UIKit/UINavigaionController+Pop.swift new file mode 100644 index 0000000000..f0fbbe1ca9 --- /dev/null +++ b/novawallet/Common/Extension/UIKit/UINavigaionController+Pop.swift @@ -0,0 +1,7 @@ +import UIKit + +extension UINavigationController { + func findTopView() -> T? { + viewControllers.last(where: { $0 is T }) as? T + } +} diff --git a/novawallet/Common/Helpers/AttributedStringDecorator.swift b/novawallet/Common/Helpers/AttributedStringDecorator.swift index d2f3be1cbc..ca2211bc29 100644 --- a/novawallet/Common/Helpers/AttributedStringDecorator.swift +++ b/novawallet/Common/Helpers/AttributedStringDecorator.swift @@ -7,10 +7,12 @@ protocol AttributedStringDecoratorProtocol: AnyObject { final class HighlightingAttributedStringDecorator: AttributedStringDecoratorProtocol { let pattern: String let attributes: [NSAttributedString.Key: Any] + let includeSeparator: Bool - init(pattern: String, attributes: [NSAttributedString.Key: Any]) { + init(pattern: String, attributes: [NSAttributedString.Key: Any], includeSeparator: Bool = false) { self.pattern = pattern self.attributes = attributes + self.includeSeparator = includeSeparator } func decorate(attributedString: NSAttributedString) -> NSAttributedString { @@ -26,6 +28,19 @@ final class HighlightingAttributedStringDecorator: AttributedStringDecoratorProt resultAttributedString.addAttributes(attributes, range: range) + if includeSeparator, range.upperBound < string.length { + let punctuationSet = CharacterSet.punctuationCharacters + let remainingRange = NSRange(location: range.upperBound, length: string.length - range.upperBound) + let rangeOfPunctuation = string.rangeOfCharacter( + from: punctuationSet, + options: [], + range: remainingRange + ) + if rangeOfPunctuation.location != NSNotFound { + resultAttributedString.addAttributes(attributes, range: rangeOfPunctuation) + } + } + return resultAttributedString } } diff --git a/novawallet/Common/Helpers/BalanceCalculator.swift b/novawallet/Common/Helpers/BalanceCalculator.swift index ed1ce93b2b..9872ffa75e 100644 --- a/novawallet/Common/Helpers/BalanceCalculator.swift +++ b/novawallet/Common/Helpers/BalanceCalculator.swift @@ -9,8 +9,8 @@ protocol BalancesCalculating: AnyObject { final class BalancesCalculator { private var identifierMapping: [String: AssetBalanceId] = [:] private var balances: [AccountId: [ChainAssetId: BigUInt]] = [:] - private var crowdloanContributions: [AccountId: [ChainModel.Id: BigUInt]] = [:] - private var crowdloanContributionsMapping: [String: CrowdloanContributionId] = [:] + private var externalBalances: [AccountId: [ChainAssetId: BigUInt]] = [:] + private var externalBalancesMapping: [String: ExternalBalanceContribution] = [:] private var prices: [ChainAssetId: PriceData] = [:] private var chains: [ChainModel.Id: ChainModel] = [:] @@ -49,34 +49,34 @@ final class BalancesCalculator { } } - func didReceiveCrowdloanContributionChanges(_ changes: [DataProviderChange]) { + func didReceiveExternalBalanceChanges(_ changes: [DataProviderChange]) { 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 - previousAmount + item.amount - crowdloanContributions[item.accountId] = accountCrowdloan - crowdloanContributionsMapping[item.identifier] = CrowdloanContributionId( - chainId: item.chainId, + let previousAmount = externalBalancesMapping[item.identifier]?.amount ?? 0 + var accountBalances = externalBalances[item.accountId] ?? [:] + let value: BigUInt = accountBalances[item.chainAssetId] ?? 0 + accountBalances[item.chainAssetId] = value - previousAmount + item.amount + externalBalances[item.accountId] = accountBalances + externalBalancesMapping[item.identifier] = .init( + chainAssetId: item.chainAssetId, accountId: item.accountId, amount: item.amount ) case let .delete(deletedIdentifier): - if let accountContributionId = crowdloanContributionsMapping[deletedIdentifier] { - var accountContributions = crowdloanContributions[accountContributionId.accountId] - if let contribution = accountContributions?[accountContributionId.chainId], + if let accountContributionId = externalBalancesMapping[deletedIdentifier] { + var accountContributions = externalBalances[accountContributionId.accountId] + if let contribution = accountContributions?[accountContributionId.chainAssetId], contribution > accountContributionId.amount { let newAmount = contribution - accountContributionId.amount - accountContributions?[accountContributionId.chainId] = newAmount + accountContributions?[accountContributionId.chainAssetId] = newAmount } else { - accountContributions?[accountContributionId.chainId] = nil + accountContributions?[accountContributionId.chainAssetId] = nil } - crowdloanContributions[accountContributionId.accountId] = accountContributions + externalBalances[accountContributionId.accountId] = accountContributions } - crowdloanContributionsMapping[deletedIdentifier] = nil + externalBalancesMapping[deletedIdentifier] = nil } } } @@ -108,19 +108,19 @@ final class BalancesCalculator { } } - private func calculateCrowdloanContribution( - _ contributions: [ChainModel.Id: BigUInt], + private func calculateExternalBalances( + _ externalBalances: [ChainAssetId: 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)], + externalBalances.reduce(0) { result, externalBalance in + guard let asset = chains[externalBalance.key.chainId]?.asset(for: externalBalance.key.assetId), + let priceData = prices[externalBalance.key], let price = Decimal(string: priceData.price) else { return result } guard let decimalAmount = Decimal.fromSubstrateAmount( - contribution.value, + externalBalance.value, precision: Int16(bitPattern: asset.precision) ) else { return result @@ -139,11 +139,11 @@ final class BalancesCalculator { excludingChainIds: excludingChainIds ) - let contributions = crowdloanContributions[accountId]? - .filter { !excludingChainIds.contains($0.key) } ?? [:] + let externalBalances = externalBalances[accountId]? + .filter { !excludingChainIds.contains($0.key.chainId) } ?? [:] - let crowdloans = calculateCrowdloanContribution( - contributions, + let crowdloans = calculateExternalBalances( + externalBalances, chains: chains, prices: prices ) @@ -180,9 +180,9 @@ extension BalancesCalculator: BalancesCalculating { includingChainIds: [chainAccount.chainId], excludingChainIds: Set() ) - let contributions = crowdloanContributions[chainAccount.accountId] ?? [:] - totalValue += calculateCrowdloanContribution( - contributions, + let externalBalances = externalBalances[chainAccount.accountId] ?? [:] + totalValue += calculateExternalBalances( + externalBalances, chains: chains, prices: prices ) diff --git a/novawallet/Common/Helpers/BalancesStore+Default.swift b/novawallet/Common/Helpers/BalancesStore+Default.swift index e667c99db3..a077de8823 100644 --- a/novawallet/Common/Helpers/BalancesStore+Default.swift +++ b/novawallet/Common/Helpers/BalancesStore+Default.swift @@ -11,7 +11,7 @@ extension BalancesStore { walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, currencyManager: currencyManager, - crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactory.shared + externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactory.shared ) } } diff --git a/novawallet/Common/Helpers/BalancesStore.swift b/novawallet/Common/Helpers/BalancesStore.swift index ca760259d5..f18988de73 100644 --- a/novawallet/Common/Helpers/BalancesStore.swift +++ b/novawallet/Common/Helpers/BalancesStore.swift @@ -4,7 +4,7 @@ import RobinHood enum BalancesStoreError: Error { case priceFailed(Error) case balancesFailed(Error) - case crowdloansFailed(Error) + case externalBalancesFailed(Error) } protocol BalancesStoreDelegate: AnyObject { @@ -22,14 +22,14 @@ final class BalancesStore { let chainRegistry: ChainRegistryProtocol let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol - let crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol + let externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactoryProtocol weak var delegate: BalancesStoreDelegate? private(set) var priceSubscription: StreamableProvider? private(set) var assetsSubscription: StreamableProvider? private(set) var walletsSubscription: StreamableProvider? - private(set) var crowdloansSubscription: StreamableProvider? + private(set) var externalBalancesSubscription: StreamableProvider? private(set) var availableTokenPrice: [ChainAssetId: AssetModel.PriceId] = [:] private var calculator: BalancesCalculator? @@ -39,12 +39,12 @@ final class BalancesStore { walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, currencyManager: CurrencyManagerProtocol, - crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol + externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactoryProtocol ) { self.chainRegistry = chainRegistry self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory - self.crowdloansLocalSubscriptionFactory = crowdloansLocalSubscriptionFactory + self.externalBalancesSubscriptionFactory = externalBalancesSubscriptionFactory self.currencyManager = currencyManager } @@ -52,8 +52,8 @@ final class BalancesStore { assetsSubscription = subscribeAllBalancesProvider() } - private func subscribeToCrowdloans() { - crowdloansSubscription = subscribeToAllCrowdloansProvider() + private func subscribeToExternalBalances() { + externalBalancesSubscription = subscribeToAllExternalAssetBalancesProvider() } private func subscribeChains() { @@ -166,7 +166,7 @@ extension BalancesStore: BalancesStoreProtocol { subscribeChains() subscribeAssets() - subscribeToCrowdloans() + subscribeToExternalBalances() } } @@ -182,14 +182,16 @@ extension BalancesStore: WalletLocalStorageSubscriber, WalletLocalSubscriptionHa } } -extension BalancesStore: CrowdloanContributionLocalSubscriptionHandler, CrowdloansLocalStorageSubscriber { - func handleAllCrowdloans(result: Result<[DataProviderChange], Error>) { +extension BalancesStore: ExternalAssetBalanceSubscriptionHandler, ExternalAssetBalanceSubscriber { + func handleAllExternalAssetBalances( + result: Result<[DataProviderChange], Error> + ) { switch result { case let .success(changes): - calculator?.didReceiveCrowdloanContributionChanges(changes) + calculator?.didReceiveExternalBalanceChanges(changes) notifyCalculatorChanges() case let .failure(error): - notify(error: .crowdloansFailed(error)) + notify(error: .externalBalancesFailed(error)) } } } diff --git a/novawallet/Common/Helpers/KeyboardAppearanceState.swift b/novawallet/Common/Helpers/KeyboardAppearanceState.swift index b00d0b74d8..3368731283 100644 --- a/novawallet/Common/Helpers/KeyboardAppearanceState.swift +++ b/novawallet/Common/Helpers/KeyboardAppearanceState.swift @@ -12,20 +12,30 @@ final class EventDrivenKeyboardStrategy: KeyboardAppearanceStrategyProtocol { } let events: Set + let triggersOnes: Bool - init(events: Set) { + private var triggered: Bool = false + + private var canTrigger: Bool { + !(triggersOnes && triggered) + } + + init(events: Set, triggersOnes: Bool = false) { self.events = events + self.triggersOnes = triggersOnes } func onViewWillAppear(for target: UIView) { - if events.contains(.viewWillAppear) { + if canTrigger, events.contains(.viewWillAppear) { target.becomeFirstResponder() + triggered = true } } func onViewDidAppear(for target: UIView) { - if events.contains(.viewDidAppear) { + if canTrigger, events.contains(.viewDidAppear) { target.becomeFirstResponder() + triggered = true } } } diff --git a/novawallet/Common/Helpers/PredefinedTimeShortcut.swift b/novawallet/Common/Helpers/PredefinedTimeShortcut.swift new file mode 100644 index 0000000000..9e906a22eb --- /dev/null +++ b/novawallet/Common/Helpers/PredefinedTimeShortcut.swift @@ -0,0 +1,17 @@ +import Foundation + +protocol PredefinedTimeShortcutProtocol { + func getShortcut(for timeInterval: TimeInterval, roundsDown: Bool, locale: Locale) -> String? +} + +final class EverydayShortcut: PredefinedTimeShortcutProtocol { + func getShortcut(for timeInterval: TimeInterval, roundsDown: Bool, locale: Locale) -> String? { + let (days, hours) = timeInterval.getDaysAndHours(roundingDown: roundsDown) + + guard days == 1, hours == 0 else { + return nil + } + + return R.string.localizable.commonEveryDay(preferredLanguages: locale.rLanguages) + } +} diff --git a/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift b/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift index 3731d35796..7a0ebabf6d 100644 --- a/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift +++ b/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift @@ -7,7 +7,12 @@ protocol SubstrateRepositoryFactoryProtocol { func createAssetBalanceRepository() -> AnyDataProviderRepository func createAssetBalanceRepository(for chainAssetIds: Set) -> AnyDataProviderRepository - func createStashItemRepository() -> AnyDataProviderRepository + + func createStashItemRepository( + for address: AccountAddress, + chainId: ChainModel.Id + ) -> AnyDataProviderRepository + func createSingleValueRepository() -> AnyDataProviderRepository func createChainRepository() -> AnyDataProviderRepository @@ -85,20 +90,6 @@ final class SubstrateRepositoryFactory: SubstrateRepositoryFactoryProtocol { return AnyDataProviderRepository(repository) } - func createStashItemRepository() -> AnyDataProviderRepository { - let mapper: CodableCoreDataMapper = - CodableCoreDataMapper(entityIdentifierFieldName: #keyPath(CDStashItem.stash)) - - let repository: CoreDataRepository = - storageFacade.createRepository( - filter: nil, - sortDescriptors: [], - mapper: AnyCoreDataMapper(mapper) - ) - - return AnyDataProviderRepository(repository) - } - func createSingleValueRepository() -> AnyDataProviderRepository { let repository: CoreDataRepository = storageFacade.createRepository() @@ -291,4 +282,21 @@ final class SubstrateRepositoryFactory: SubstrateRepositoryFactoryProtocol { ) return AnyDataProviderRepository(repository) } + + func createStashItemRepository( + for address: AccountAddress, + chainId: ChainModel.Id + ) -> AnyDataProviderRepository { + let filter = NSPredicate.filterByStashOrController(address, chainId: chainId) + + let mapper = StashItemMapper() + + let repository = storageFacade.createRepository( + filter: filter, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + + return AnyDataProviderRepository(repository) + } } diff --git a/novawallet/Common/Helpers/ValueResolver.swift b/novawallet/Common/Helpers/ValueResolver.swift new file mode 100644 index 0000000000..7fc76f561f --- /dev/null +++ b/novawallet/Common/Helpers/ValueResolver.swift @@ -0,0 +1,77 @@ +import Foundation + +class ValueResolver { + let resultClosure: (V) -> Void + let resolver: (P1, P2, P3, P4, P5) -> V + + private var p1Store: UncertainStorage + private var p2Store: UncertainStorage + private var p3Store: UncertainStorage + private var p4Store: UncertainStorage + private var p5Store: UncertainStorage + + init( + p1Store: UncertainStorage = .undefined, + p2Store: UncertainStorage = .undefined, + p3Store: UncertainStorage = .undefined, + p4Store: UncertainStorage = .undefined, + p5Store: UncertainStorage = .undefined, + resolver: @escaping (P1, P2, P3, P4, P5) -> V, + resultClosure: @escaping (V) -> Void + ) { + self.p1Store = p1Store + self.p2Store = p2Store + self.p3Store = p3Store + self.p4Store = p4Store + self.p5Store = p5Store + self.resolver = resolver + self.resultClosure = resultClosure + } + + private func resolve() { + guard + case let .defined(param1) = p1Store, + case let .defined(param2) = p2Store, + case let .defined(param3) = p3Store, + case let .defined(param4) = p4Store, + case let .defined(param5) = p5Store else { + return + } + + let value = resolver(param1, param2, param3, param4, param5) + + resultClosure(value) + } +} + +extension ValueResolver { + func apply(param1: P1) { + p1Store = .defined(param1) + + resolve() + } + + func apply(param2: P2) { + p2Store = .defined(param2) + + resolve() + } + + func apply(param3: P3) { + p3Store = .defined(param3) + + resolve() + } + + func apply(param4: P4) { + p4Store = .defined(param4) + + resolve() + } + + func apply(param5: P5) { + p5Store = .defined(param5) + + resolve() + } +} diff --git a/novawallet/Common/Migration/SubstrateStorageVersion.swift b/novawallet/Common/Migration/SubstrateStorageVersion.swift index 66166e426a..0e5c3f3de6 100644 --- a/novawallet/Common/Migration/SubstrateStorageVersion.swift +++ b/novawallet/Common/Migration/SubstrateStorageVersion.swift @@ -15,6 +15,9 @@ enum SubstrateStorageVersion: String, CaseIterable { case version14 = "SubstrateDataModel14" case version15 = "SubstrateDataModel15" case version16 = "SubstrateDataModel16" + case version17 = "SubstrateDataModel17" + case version18 = "SubstrateDataModel18" + case version19 = "SubstrateDataModel19" static var current: SubstrateStorageVersion { allCases.last! @@ -53,6 +56,12 @@ enum SubstrateStorageVersion: String, CaseIterable { case .version15: return .version16 case .version16: + return .version17 + case .version17: + return .version18 + case .version18: + return .version19 + case .version19: return nil } } diff --git a/novawallet/Common/Model/ChainRegistry/LocalChain/AssetModel+Staking.swift b/novawallet/Common/Model/ChainRegistry/LocalChain/AssetModel+Staking.swift new file mode 100644 index 0000000000..d11108295d --- /dev/null +++ b/novawallet/Common/Model/ChainRegistry/LocalChain/AssetModel+Staking.swift @@ -0,0 +1,15 @@ +import Foundation + +extension AssetModel { + var supportsNominationPoolsStaking: Bool { + guard let stakings = stakings else { + return false + } + + return stakings.contains(.nominationPools) + } + + var hasMultipleStakingOptions: Bool { + (stakings ?? []).count > 1 + } +} diff --git a/novawallet/Common/Model/ChainRegistry/LocalChain/AssetModel.swift b/novawallet/Common/Model/ChainRegistry/LocalChain/AssetModel.swift index 09a56765b1..f50e1493c8 100644 --- a/novawallet/Common/Model/ChainRegistry/LocalChain/AssetModel.swift +++ b/novawallet/Common/Model/ChainRegistry/LocalChain/AssetModel.swift @@ -34,6 +34,10 @@ struct AssetModel: Equatable, Codable, Hashable { stakings?.contains { $0 != .unsupported } ?? false } + var hasPoolStaking: Bool { + stakings?.contains { $0 == .nominationPools } ?? false + } + init( assetId: Id, icon: URL?, @@ -76,6 +80,10 @@ struct AssetModel: Equatable, Codable, Hashable { self.enabled = enabled source = .remote } + + var hasPrice: Bool { + priceId != nil + } } extension AssetModel { diff --git a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel+Additional.swift b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel+Additional.swift new file mode 100644 index 0000000000..7f83f6f732 --- /dev/null +++ b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel+Additional.swift @@ -0,0 +1,20 @@ +import UIKit +import SubstrateSdk + +extension ChainModel { + var themeColor: UIColor? { + guard let hexColor = additional?.themeColor?.stringValue else { + return nil + } + + return UIColor(hex: hexColor) + } + + var stakingWiki: URL? { + guard let wiki = additional?.stakingWiki?.stringValue else { + return nil + } + + return URL(string: wiki) + } +} diff --git a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift index 58189ddd28..7a2696c05e 100644 --- a/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift +++ b/novawallet/Common/Model/ChainRegistry/LocalChain/ChainModel.swift @@ -146,6 +146,22 @@ struct ChainModel: Equatable, Codable, Hashable { !noSubstrateRuntime } + func chainAssetsWithExternalBalances() -> [ChainAsset] { + assets.compactMap { asset in + guard asset.hasPoolStaking || asset.isUtility && hasCrowdloans else { + return nil + } + + return ChainAsset(chain: self, asset: asset) + } + } + + func chainAssetIdsWithExternalBalances() -> Set { + let chainAssets = chainAssetsWithExternalBalances() + + return Set(chainAssets.map(\.chainAssetId)) + } + var isRelaychain: Bool { parentId == nil } func utilityAssets() -> Set { @@ -183,6 +199,10 @@ struct ChainModel: Equatable, Codable, Hashable { return nil } } + + var defaultBlockTimeMillis: BlockTime? { + additional?.defaultBlockTime?.unsignedIntValue + } } extension ChainModel: Identifiable { diff --git a/novawallet/Common/Model/ExternalAssetBalance.swift b/novawallet/Common/Model/ExternalAssetBalance.swift new file mode 100644 index 0000000000..fd441c783e --- /dev/null +++ b/novawallet/Common/Model/ExternalAssetBalance.swift @@ -0,0 +1,61 @@ +import Foundation +import BigInt +import RobinHood +import SoraFoundation + +struct ExternalAssetBalance: Equatable, Identifiable { + enum BalanceType: String { + case crowdloan + case nominationPools + case unknown + + init(rawType: String) { + if let knownType = Self(rawValue: rawType) { + self = knownType + } else { + self = .unknown + } + } + } + + let identifier: String + let chainAssetId: ChainAssetId + let accountId: AccountId + let amount: BigUInt + let type: BalanceType + let subtype: String? + let param: String? +} + +struct ExternalBalanceAssetGroupId: Equatable, Hashable { + let chainAssetId: ChainAssetId + let type: ExternalAssetBalance.BalanceType + + var stringValue: String { + chainAssetId.stringValue + "-" + type.rawValue + } +} + +extension Array where Element == ExternalAssetBalance { + func groupByAssetType() -> [ExternalBalanceAssetGroupId: BigUInt] { + reduce(into: [ExternalBalanceAssetGroupId: BigUInt]()) { accum, balance in + let group = ExternalBalanceAssetGroupId(chainAssetId: balance.chainAssetId, type: balance.type) + + let previousAmount = accum[group] ?? 0 + accum[group] = previousAmount + balance.amount + } + } +} + +extension ExternalAssetBalance.BalanceType { + var lockTitle: LocalizableResource { + switch self { + case .crowdloan: + return LocalizableResource { R.string.localizable.tabbarCrowdloanTitle(preferredLanguages: $0.rLanguages) } + case .nominationPools: + return LocalizableResource { R.string.localizable.stakingPoolStaking(preferredLanguages: $0.rLanguages) } + case .unknown: + return LocalizableResource { R.string.localizable.commonUnknown(preferredLanguages: $0.rLanguages) } + } + } +} diff --git a/novawallet/Common/Model/OnchainStorage.swift b/novawallet/Common/Model/OnchainStorage.swift index 5c9e2df806..4434c9e8a0 100644 --- a/novawallet/Common/Model/OnchainStorage.swift +++ b/novawallet/Common/Model/OnchainStorage.swift @@ -4,6 +4,15 @@ enum UncertainStorage { case undefined case defined(T) + var isDefined: Bool { + switch self { + case .defined: + return true + case .undefined: + return false + } + } + var value: T? { switch self { case let .defined(value): @@ -36,10 +45,10 @@ enum UncertainStorage { extension UncertainStorage where T: Decodable { init( values: [BatchStorageSubscriptionResultValue], - localKey: String, + mappingKey: String, context: [CodingUserInfoKey: Any]? ) throws { - if let wrappedValue = values.first(where: { $0.localKey == localKey }) { + if let wrappedValue = values.first(where: { $0.mappingKey == mappingKey }) { let value = try wrappedValue.value.map(to: T.self, with: context) self = .defined(value) } else { diff --git a/novawallet/Common/Model/PooledAssetBalance.swift b/novawallet/Common/Model/PooledAssetBalance.swift new file mode 100644 index 0000000000..86635f2033 --- /dev/null +++ b/novawallet/Common/Model/PooledAssetBalance.swift @@ -0,0 +1,22 @@ +import Foundation +import BigInt +import SubstrateSdk +import RobinHood + +struct PooledAssetBalance: Equatable { + let chainAssetId: ChainAssetId + let accountId: AccountId + let amount: BigUInt + let poolId: NominationPools.PoolId +} + +extension PooledAssetBalance: Identifiable { + var identifier: String { + Self.createIdentifier(from: chainAssetId, accountId: accountId) + } + + static func createIdentifier(from chainAssetId: ChainAssetId, accountId: AccountId) -> String { + ExternalAssetBalance.BalanceType.nominationPools.rawValue + "-" + + chainAssetId.stringValue + "-" + accountId.toHex() + } +} diff --git a/novawallet/Common/Model/StashItem.swift b/novawallet/Common/Model/StashItem.swift index f244e20e49..1f1e68c14f 100644 --- a/novawallet/Common/Model/StashItem.swift +++ b/novawallet/Common/Model/StashItem.swift @@ -4,8 +4,15 @@ import RobinHood struct StashItem: Codable, Equatable { let stash: String let controller: String + let chainId: String } extension StashItem: Identifiable { - var identifier: String { stash } + var identifier: String { + Self.createIdentifier(from: stash, chainId: chainId) + } + + static func createIdentifier(from stash: String, chainId: ChainModel.Id) -> String { + stash + "-" + chainId + } } diff --git a/novawallet/Common/Model/TransactionType.swift b/novawallet/Common/Model/TransactionType.swift index 0ba5110bf5..936318b1ac 100644 --- a/novawallet/Common/Model/TransactionType.swift +++ b/novawallet/Common/Model/TransactionType.swift @@ -6,4 +6,6 @@ enum TransactionType: String, CaseIterable, Equatable { case reward = "REWARD" case slash = "SLASH" case extrinsic = "EXTRINSIC" + case poolReward = "POOL REWARD" + case poolSlash = "POOL SLASH" } diff --git a/novawallet/Common/Network/Etherscan/Native/EtherscanNativeOperationFactory.swift b/novawallet/Common/Network/Etherscan/Native/EtherscanNativeOperationFactory.swift index 439b5b48df..26bca6ece1 100644 --- a/novawallet/Common/Network/Etherscan/Native/EtherscanNativeOperationFactory.swift +++ b/novawallet/Common/Network/Etherscan/Native/EtherscanNativeOperationFactory.swift @@ -61,7 +61,7 @@ final class EtherscanNativeOperationFactory: EtherscanBaseOperationFactory { return filter.contains(.transfers) case .extrinsics: return filter.contains(.extrinsics) - case .rewards: + case .rewards, .poolRewards: return filter.contains(.rewardsAndSlashes) } } diff --git a/novawallet/Common/Network/Subquery/Filter/SubqueryFilter.swift b/novawallet/Common/Network/Subquery/Filter/SubqueryFilter.swift index 1fa89eb00a..417936a64c 100644 --- a/novawallet/Common/Network/Subquery/Filter/SubqueryFilter.swift +++ b/novawallet/Common/Network/Subquery/Filter/SubqueryFilter.swift @@ -35,6 +35,14 @@ struct SubqueryLessThanOrEqualToFilter: SubqueryFilter { } } +struct SubqueryIsNotNullFilter: SubqueryFilter { + let fieldName: String + + func rawSubqueryFilter() -> String { + "\(fieldName): { isNull: false }" + } +} + extension String: SubqueryFilterValue { func rawSubqueryFilter() -> String { "\"\(self)\"" diff --git a/novawallet/Common/Network/Subquery/Models/SubqueryHistory.swift b/novawallet/Common/Network/Subquery/Models/SubqueryHistory.swift index 5dcd164137..f8dfb3fa4d 100644 --- a/novawallet/Common/Network/Subquery/Models/SubqueryHistory.swift +++ b/novawallet/Common/Network/Subquery/Models/SubqueryHistory.swift @@ -25,6 +25,7 @@ struct SubqueryTransfer: Codable { } struct SubqueryRewardOrSlash: Codable { + let eventIdx: Int let amount: String let isReward: Bool let era: Int? @@ -40,6 +41,13 @@ struct SubqueryExtrinsic: Decodable { let success: Bool } +struct SubqueryPoolRewardOrSlash: Codable { + let eventIdx: Int + let amount: String + let isReward: Bool + let poolId: Int +} + struct SubqueryHistoryElement: Decodable { enum CodingKeys: String, CodingKey { case identifier = "id" @@ -52,6 +60,7 @@ struct SubqueryHistoryElement: Decodable { case extrinsic case transfer case assetTransfer + case poolReward } let identifier: String @@ -64,6 +73,7 @@ struct SubqueryHistoryElement: Decodable { let extrinsic: SubqueryExtrinsic? let transfer: SubqueryTransfer? let assetTransfer: SubqueryTransfer? + let poolReward: SubqueryPoolRewardOrSlash? } struct SubqueryHistoryData: Decodable { diff --git a/novawallet/Common/Network/Subquery/Models/SubqueryMultistaking.swift b/novawallet/Common/Network/Subquery/Models/SubqueryMultistaking.swift index f882fd1e24..99ebbc1bd0 100644 --- a/novawallet/Common/Network/Subquery/Models/SubqueryMultistaking.swift +++ b/novawallet/Common/Network/Subquery/Models/SubqueryMultistaking.swift @@ -35,6 +35,12 @@ enum SubqueryMultistaking { let stakingType: String } + struct NetworkAccountStaking: Hashable { + let networkId: String + let accountId: AccountId + let stakingType: String + } + struct StatsResponse: Decodable { let activeStakers: SubqueryNodes let stakingApies: SubqueryNodes diff --git a/novawallet/Common/Network/Subquery/Models/SubqueryMultistakingTypeFactory.swift b/novawallet/Common/Network/Subquery/Models/SubqueryMultistakingTypeFactory.swift new file mode 100644 index 0000000000..1ce3d56e85 --- /dev/null +++ b/novawallet/Common/Network/Subquery/Models/SubqueryMultistakingTypeFactory.swift @@ -0,0 +1,33 @@ +import Foundation + +enum SubqueryMultistakingTypeFactory { + static let nominationPoolsKey = "nomination-pool" + + static func activeStakersTypeKey(for stakingType: StakingType, allTypes: [StakingType]) -> String { + switch stakingType { + case .nominationPools: + let relaychainStaking = allTypes.first { StakingClass(stakingType: $0) == .relaychain } + + return relaychainStaking?.rawValue ?? Self.nominationPoolsKey + default: + return stakingType.rawValue + } + } + + static func rewardsTypeKey(for stakingType: StakingType) -> String { + switch stakingType { + case .nominationPools: + return Self.nominationPoolsKey + default: + return stakingType.rawValue + } + } + + static func stakingType(from key: String) -> StakingType? { + if key == Self.nominationPoolsKey { + return .nominationPools + } else { + return StakingType(rawType: key) + } + } +} diff --git a/novawallet/Common/Network/Subquery/Models/SubqueryStakingType.swift b/novawallet/Common/Network/Subquery/Models/SubqueryStakingType.swift new file mode 100644 index 0000000000..5142c02ef0 --- /dev/null +++ b/novawallet/Common/Network/Subquery/Models/SubqueryStakingType.swift @@ -0,0 +1,6 @@ +import Foundation + +enum SubqueryStakingType { + case direct + case pools +} diff --git a/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift b/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift index 8995131147..50577fe735 100644 --- a/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift +++ b/novawallet/Common/Network/Subquery/SubqueryHistory+Wallet.swift @@ -29,6 +29,8 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { return .transfers } else if reward != nil { return .rewards + } else if poolReward != nil { + return .poolRewards } else { return .extrinsics } @@ -49,6 +51,12 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { chainAssetId: chainAsset.chainAssetId, chainFormat: chainAsset.chain.chainFormat ) + } else if let poolReward = poolReward { + return createTransactionFromPoolReward( + poolReward, + chainAssetId: chainAsset.chainAssetId, + chainFormat: chainAsset.chain.chainFormat + ) } else if let extrinsic = extrinsic { return createTransactionFromExtrinsic( extrinsic, @@ -100,7 +108,7 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { let context = HistoryRewardContext( validator: reward.validator, era: reward.era, - eventId: identifier + eventId: createEventId(from: reward.eventIdx) ) let source = TransactionHistoryItemSource.substrate @@ -125,6 +133,38 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { ) } + private func createTransactionFromPoolReward( + _ reward: SubqueryPoolRewardOrSlash, + chainAssetId: ChainAssetId, + chainFormat: ChainFormat + ) -> TransactionHistoryItem { + let context = HistoryPoolRewardContext( + poolId: NominationPools.PoolId(reward.poolId), + eventId: createEventId(from: reward.eventIdx) + ) + + let source = TransactionHistoryItemSource.substrate + let remoteIdentifier = TransactionHistoryItem.createIdentifier(from: identifier, source: source) + + return .init( + identifier: remoteIdentifier, + source: source, + chainId: chainAssetId.chainId, + assetId: chainAssetId.assetId, + sender: "\(reward.poolId)", + receiver: address.normalize(for: chainFormat) ?? address, + amountInPlank: reward.amount, + status: .success, + txHash: extrinsicHash ?? identifier, + timestamp: itemTimestamp, + fee: nil, + blockNumber: blockNumber, + txIndex: nil, + callPath: reward.isReward ? .poolReward : .poolSlash, + call: try? JSONEncoder().encode(context) + ) + } + private func createTransactionFromExtrinsic( _ extrinsic: SubqueryExtrinsic, chainAssetId: ChainAssetId, @@ -151,4 +191,8 @@ extension SubqueryHistoryElement: WalletRemoteHistoryItemProtocol { call: extrinsic.call.data(using: .utf8) ) } + + private func createEventId(from remoteId: Int) -> String { + String(blockNumber) + "-" + String(remoteId) + } } diff --git a/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift b/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift index 29c3d342f9..6382121d2a 100644 --- a/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift +++ b/novawallet/Common/Network/Subquery/SubqueryHistoryOperationFactory.swift @@ -14,11 +14,13 @@ final class SubqueryHistoryOperationFactory { let url: URL let filter: WalletHistoryFilter let assetId: String? + let hasPoolStaking: Bool - init(url: URL, filter: WalletHistoryFilter, assetId: String?) { + init(url: URL, filter: WalletHistoryFilter, assetId: String?, hasPoolStaking: Bool) { self.url = url self.filter = filter self.assetId = assetId + self.hasPoolStaking = hasPoolStaking } private func prepareExtrinsicInclusionFilter() -> String { @@ -65,7 +67,16 @@ final class SubqueryHistoryOperationFactory { } if filter.contains(.rewardsAndSlashes) { - filterStrings.append("{ reward: { isNull: false } }") + var childFilters: [SubqueryFilter] = [SubqueryIsNotNullFilter(fieldName: "reward")] + + if hasPoolStaking { + childFilters.append(SubqueryIsNotNullFilter(fieldName: "poolReward")) + } + + let filter = SubqueryInnerFilter(inner: + SubqueryCompoundFilter.or(childFilters) + ) + filterStrings.append(filter.rawSubqueryFilter()) } if filter.contains(.transfers) { @@ -87,6 +98,7 @@ final class SubqueryHistoryOperationFactory { let after = cursor.map { "\"\($0)\"" } ?? "null" let transferField = assetId != nil ? "assetTransfer" : "transfer" let filterString = prepareFilter() + let poolRewardField = hasPoolStaking ? "poolReward" : "" return """ { historyElements( @@ -114,6 +126,7 @@ final class SubqueryHistoryOperationFactory { reward extrinsic \(transferField) + \(poolRewardField) } } } diff --git a/novawallet/Common/Network/Subquery/SubqueryMultistakingOperationFactory.swift b/novawallet/Common/Network/Subquery/SubqueryMultistakingOperationFactory.swift index 49e7c0380d..ccbda3b075 100644 --- a/novawallet/Common/Network/Subquery/SubqueryMultistakingOperationFactory.swift +++ b/novawallet/Common/Network/Subquery/SubqueryMultistakingOperationFactory.swift @@ -3,8 +3,11 @@ import RobinHood import BigInt final class SubqueryMultistakingOperationFactory: SubqueryBaseOperationFactory { - private func buildAccountFilter(for request: Multistaking.OffchainRequest) throws -> SubqueryFilter { - let filterItems: [SubqueryFilter] = try request.filters.map { nextFilter in + private func buildAccountFilter( + for offchainFilters: Set, + stakingTypeMapping: (StakingType, ChainAsset) -> String + ) throws -> SubqueryFilter { + let filterItems: [SubqueryFilter] = try offchainFilters.map { nextFilter in let chain = nextFilter.chainAsset.chain let address: AccountAddress @@ -29,7 +32,9 @@ final class SubqueryMultistakingOperationFactory: SubqueryBaseOperationFactory { ) let typeFilterItems = nextFilter.stakingTypes.map { stakingType in - SubqueryEqualToFilter(fieldName: "stakingType", value: stakingType.rawValue) + let stakingTypeKey = stakingTypeMapping(stakingType, nextFilter.chainAsset) + + return SubqueryEqualToFilter(fieldName: "stakingType", value: stakingTypeKey) } let typeFilter = SubqueryCompoundFilter.or(typeFilterItems) @@ -40,21 +45,15 @@ final class SubqueryMultistakingOperationFactory: SubqueryBaseOperationFactory { return SubqueryCompoundFilter.or(filterItems) } - private func buildQuery(for request: Multistaking.OffchainRequest) throws -> String { - let accountFilter = try buildAccountFilter(for: request) - - let accountQueryFilter = SubqueryFilterBuilder.buildBlock(accountFilter) - - let rewardFilter = SubqueryEqualToFilter(fieldName: "type", value: SubqueryRewardType.reward) - let rewardsQueryFilter = SubqueryFilterBuilder.buildBlock(SubqueryCompoundFilter.and([accountFilter, rewardFilter])) - - let slashFilter = SubqueryEqualToFilter(fieldName: "type", value: SubqueryRewardType.slash) - let slashesQueryFilter = SubqueryFilterBuilder.buildBlock(SubqueryCompoundFilter.and([accountFilter, slashFilter])) - - return """ + private func buildQuery( + activeStakerQueryFilter: String, + rewardsQueryFilter: String, + slashesQueryFilter: String + ) -> String { + """ { activeStakers( - \(accountQueryFilter) + \(activeStakerQueryFilter) ) { nodes { networkId @@ -97,6 +96,39 @@ final class SubqueryMultistakingOperationFactory: SubqueryBaseOperationFactory { } """ } + + private func buildQuery(for request: Multistaking.OffchainRequest) throws -> String { + let activeStakersAccountFilter = try buildAccountFilter( + for: request.stateFilters + ) { stakingType, chainAsset in + SubqueryMultistakingTypeFactory.activeStakersTypeKey( + for: stakingType, + allTypes: chainAsset.asset.stakings ?? [] + ) + } + + let rewardsAccountFilter = try buildAccountFilter(for: request.rewardFilters) { stakingType, _ in + SubqueryMultistakingTypeFactory.rewardsTypeKey(for: stakingType) + } + + let activeStakerQueryFilter = SubqueryFilterBuilder.buildBlock(activeStakersAccountFilter) + + let rewardFilter = SubqueryEqualToFilter(fieldName: "type", value: SubqueryRewardType.reward) + let rewardsQueryFilter = SubqueryFilterBuilder.buildBlock( + SubqueryCompoundFilter.and([rewardsAccountFilter, rewardFilter]) + ) + + let slashFilter = SubqueryEqualToFilter(fieldName: "type", value: SubqueryRewardType.slash) + let slashesQueryFilter = SubqueryFilterBuilder.buildBlock( + SubqueryCompoundFilter.and([rewardsAccountFilter, slashFilter]) + ) + + return buildQuery( + activeStakerQueryFilter: activeStakerQueryFilter, + rewardsQueryFilter: rewardsQueryFilter, + slashesQueryFilter: slashesQueryFilter + ) + } } extension SubqueryMultistakingOperationFactory: MultistakingOffchainOperationFactoryProtocol { @@ -106,23 +138,40 @@ extension SubqueryMultistakingOperationFactory: MultistakingOffchainOperationFac do { let query = try buildQuery(for: request) let operation = createOperation(for: query) { (result: SubqueryMultistaking.StatsResponse) in - let activeStakers: [SubqueryMultistaking.NetworkStaking: AccountAddress] - activeStakers = result.activeStakers.nodes.reduce(into: [:]) { - $0[.init(networkId: $1.networkId, stakingType: $1.stakingType)] = $1.address - } - + let activeStakers = result.activeStakers.groupByNetworkAccountStaking() let rewards = result.rewards.groupByNetworkStaking() let slashes = result.slashes.groupByNetworkStaking() - let stakings = result.stakingApies.nodes.map { node in + let stateFilterByNetworkStaking = request.stateFilters.groupByNetworkStaking() + + let stakings: [Multistaking.OffchainStaking] = result.stakingApies.nodes.compactMap { node in + guard let stakingType = SubqueryMultistakingTypeFactory.stakingType(from: node.stakingType) else { + return nil + } + let state: Multistaking.OffchainStakingState - let networkStaking = SubqueryMultistaking.NetworkStaking( - networkId: node.networkId, - stakingType: node.stakingType + // it is currently save to assume staking is enabled only for utility assets + let filterKey = Multistaking.Option( + chainAssetId: .init( + chainId: node.networkId.withoutHexPrefix(), + assetId: AssetModel.utilityAssetId + ), + type: stakingType ) - if activeStakers[networkStaking] != nil { + let optStakersKey = stateFilterByNetworkStaking[filterKey].map { filter in + SubqueryMultistaking.NetworkAccountStaking( + networkId: node.networkId, + accountId: filter.accountId, + stakingType: SubqueryMultistakingTypeFactory.activeStakersTypeKey( + for: stakingType, + allTypes: filter.chainAsset.asset.stakings ?? [] + ) + ) + } + + if let stakersKey = optStakersKey, activeStakers[stakersKey] != nil { state = .active } else { state = .inactive @@ -130,6 +179,11 @@ extension SubqueryMultistakingOperationFactory: MultistakingOffchainOperationFac let totalRewards: BigUInt? + let networkStaking = SubqueryMultistaking.NetworkStaking( + networkId: node.networkId, + stakingType: node.stakingType + ) + if let reward = rewards[networkStaking] { let slash = slashes[networkStaking] ?? 0 @@ -140,7 +194,7 @@ extension SubqueryMultistakingOperationFactory: MultistakingOffchainOperationFac return Multistaking.OffchainStaking( chainId: node.networkId.withoutHexPrefix(), - stakingType: StakingType(rawType: node.stakingType), + stakingType: stakingType, maxApy: node.maxApy, state: state, totalRewards: totalRewards @@ -168,3 +222,21 @@ extension SubqueryAggregates where T == SubqueryMultistaking.AccumulatedReward { } } } + +extension SubqueryNodes where T == SubqueryMultistaking.ActiveStaker { + func groupByNetworkAccountStaking() -> [SubqueryMultistaking.NetworkAccountStaking: AccountAddress] { + nodes.reduce(into: [:]) { + guard let accountId = try? $1.address.toAccountId() else { + return + } + + let key = SubqueryMultistaking.NetworkAccountStaking( + networkId: $1.networkId, + accountId: accountId, + stakingType: $1.stakingType + ) + + return $0[key] = $1.address + } + } +} diff --git a/novawallet/Common/Network/Subquery/SubqueryRewardOperationFactory.swift b/novawallet/Common/Network/Subquery/SubqueryRewardOperationFactory.swift index e008f20341..bfe1de7712 100644 --- a/novawallet/Common/Network/Subquery/SubqueryRewardOperationFactory.swift +++ b/novawallet/Common/Network/Subquery/SubqueryRewardOperationFactory.swift @@ -13,7 +13,8 @@ protocol SubqueryRewardOperationFactoryProtocol { func createTotalRewardOperation( for address: AccountAddress, startTimestamp: Int64?, - endTimestamp: Int64? + endTimestamp: Int64?, + stakingType: SubqueryStakingType ) -> BaseOperation } @@ -75,20 +76,23 @@ final class SubqueryRewardOperationFactory { private func prepareTotalRewardQuery( for address: AccountAddress, startTimestamp: Int64?, - endTimestamp: Int64? + endTimestamp: Int64?, + stakingType: SubqueryStakingType ) -> String { let rewardsQuery = accountRewardsQuery( address: address, startTimestamp: startTimestamp, endTimestamp: endTimestamp, - type: .reward + rewardType: .reward, + stakingType: stakingType ) let slashQuery = accountRewardsQuery( address: address, startTimestamp: startTimestamp, endTimestamp: endTimestamp, - type: .slash + rewardType: .slash, + stakingType: stakingType ) return """ @@ -103,11 +107,12 @@ final class SubqueryRewardOperationFactory { address: AccountAddress, startTimestamp: Int64?, endTimestamp: Int64?, - type: SubqueryRewardType + rewardType: SubqueryRewardType, + stakingType: SubqueryStakingType ) -> String { var commonFilters: [SubqueryFilter] = [ SubqueryEqualToFilter(fieldName: "address", value: address), - SubqueryEqualToFilter(fieldName: "type", value: type) + SubqueryEqualToFilter(fieldName: "type", value: rewardType) ] if let startTimestamp = startTimestamp { @@ -122,8 +127,17 @@ final class SubqueryRewardOperationFactory { let queryFilter = SubqueryFilterBuilder.buildBlock(SubqueryCompoundFilter.and(commonFilters)) + let entityName: String + + switch stakingType { + case .direct: + entityName = "accountRewards" + case .pools: + entityName = "accountPoolRewards" + } + return """ - accountRewards( + \(entityName)( \(queryFilter) ) { groupedAggregates(groupBy: [ADDRESS]) { @@ -184,12 +198,14 @@ extension SubqueryRewardOperationFactory: SubqueryRewardOperationFactoryProtocol func createTotalRewardOperation( for address: AccountAddress, startTimestamp: Int64?, - endTimestamp: Int64? + endTimestamp: Int64?, + stakingType: SubqueryStakingType ) -> BaseOperation { let queryString = prepareTotalRewardQuery( for: address, startTimestamp: startTimestamp, - endTimestamp: endTimestamp + endTimestamp: endTimestamp, + stakingType: stakingType ) let requestFactory = BlockNetworkRequestFactory { diff --git a/novawallet/Common/Operation/StorageDecodingOperation+Init.swift b/novawallet/Common/Operation/StorageDecodingOperation+Init.swift index 9594c6130e..9cdc65d26e 100644 --- a/novawallet/Common/Operation/StorageDecodingOperation+Init.swift +++ b/novawallet/Common/Operation/StorageDecodingOperation+Init.swift @@ -19,4 +19,26 @@ extension PrimitiveConstantOperation { return operation } + + static func wrapper( + for path: ConstantCodingPath, + runtimeService: RuntimeCodingServiceProtocol, + fallbackValue: T? = nil + ) -> CompoundOperationWrapper { + let factoryOperation = runtimeService.fetchCoderFactoryOperation() + + let operation = PrimitiveConstantOperation(path: path, fallbackValue: fallbackValue) + + operation.configurationBlock = { + do { + operation.codingFactory = try factoryOperation.extractNoCancellableResultData() + } catch { + operation.result = .failure(error) + } + } + + operation.addDependency(factoryOperation) + + return CompoundOperationWrapper(targetOperation: operation, dependencies: [factoryOperation]) + } } diff --git a/novawallet/Common/Protocols/ExtrinsicSubmissionPresenting.swift b/novawallet/Common/Protocols/ExtrinsicSubmissionPresenting.swift index d071b814f8..2ff58f5a02 100644 --- a/novawallet/Common/Protocols/ExtrinsicSubmissionPresenting.swift +++ b/novawallet/Common/Protocols/ExtrinsicSubmissionPresenting.swift @@ -1,9 +1,10 @@ -import Foundation +import UIKit enum ExtrinsicSubmissionPresentingAction { case dismiss case pop case popBack + case popBaseAndDismiss } protocol ExtrinsicSubmissionPresenting: AnyObject { @@ -40,6 +41,22 @@ extension ExtrinsicSubmissionPresenting where Self: ModalAlertPresenting { view?.controller.navigationController?.popViewController(animated: true) presentSuccessNotification(title, from: presenter, completion: nil) + case .popBaseAndDismiss: + var rootNavigationController: UINavigationController? + + let presenter = view?.controller.navigationController?.presentingViewController + + if let tabBar = presenter as? UITabBarController { + rootNavigationController = tabBar.selectedViewController as? UINavigationController + } else { + rootNavigationController = presenter as? UINavigationController + } + + rootNavigationController?.popToRootViewController(animated: false) + + presenter?.dismiss(animated: true) { [weak self] in + self?.presentSuccessNotification(title, from: presenter, completion: nil) + } } } } diff --git a/novawallet/Common/Protocols/LocalStorageProviderObserving.swift b/novawallet/Common/Protocols/LocalStorageProviderObserving.swift new file mode 100644 index 0000000000..1013d27da4 --- /dev/null +++ b/novawallet/Common/Protocols/LocalStorageProviderObserving.swift @@ -0,0 +1,102 @@ +import Foundation +import RobinHood + +protocol LocalStorageProviderObserving where Self: AnyObject { + func addDataProviderObserver( + for provider: AnyDataProvider>, + updateClosure: @escaping (T?) -> Void, + failureClosure: @escaping (Error) -> Void, + callbackQueue: DispatchQueue, + options: DataProviderObserverOptions + ) + + func addSingleValueProviderObserver( + for provider: AnySingleValueProvider, + updateClosure: @escaping (T?) -> Void, + failureClosure: @escaping (Error) -> Void, + callbackQueue: DispatchQueue, + options: DataProviderObserverOptions + ) +} + +extension LocalStorageProviderObserving { + func addDataProviderObserver( + for provider: AnyDataProvider>, + updateClosure: @escaping (T?) -> Void, + failureClosure: @escaping (Error) -> Void, + options: DataProviderObserverOptions = .init(alwaysNotifyOnRefresh: false, waitsInProgressSyncOnAdd: false) + ) { + addDataProviderObserver( + for: provider, + updateClosure: updateClosure, + failureClosure: failureClosure, + callbackQueue: .main, + options: options + ) + } + + func addDataProviderObserver( + for provider: AnyDataProvider>, + updateClosure: @escaping (T?) -> Void, + failureClosure: @escaping (Error) -> Void, + callbackQueue: DispatchQueue, + options: DataProviderObserverOptions + ) { + let update = { (changes: [DataProviderChange>]) in + let value = changes.reduceToLastChange() + updateClosure(value?.item) + } + + let failure = { error in + failureClosure(error) + } + + provider.addObserver( + self, + deliverOn: callbackQueue, + executing: update, + failing: failure, + options: options + ) + } + + func addSingleValueProviderObserver( + for provider: AnySingleValueProvider, + updateClosure: @escaping (T?) -> Void, + failureClosure: @escaping (Error) -> Void, + options: DataProviderObserverOptions = .init(alwaysNotifyOnRefresh: false, waitsInProgressSyncOnAdd: false) + ) { + addSingleValueProviderObserver( + for: provider, + updateClosure: updateClosure, + failureClosure: failureClosure, + callbackQueue: .main, + options: options + ) + } + + func addSingleValueProviderObserver( + for provider: AnySingleValueProvider, + updateClosure: @escaping (T?) -> Void, + failureClosure: @escaping (Error) -> Void, + callbackQueue: DispatchQueue, + options: DataProviderObserverOptions + ) { + let update = { (changes: [DataProviderChange]) in + let value = changes.reduceToLastChange() + updateClosure(value) + } + + let failure = { error in + failureClosure(error) + } + + provider.addObserver( + self, + deliverOn: callbackQueue, + executing: update, + failing: failure, + options: options + ) + } +} diff --git a/novawallet/Common/Protocols/RuntimeConstantFetching.swift b/novawallet/Common/Protocols/RuntimeConstantFetching.swift index f51f32954d..2228b0d068 100644 --- a/novawallet/Common/Protocols/RuntimeConstantFetching.swift +++ b/novawallet/Common/Protocols/RuntimeConstantFetching.swift @@ -7,6 +7,7 @@ protocol RuntimeConstantFetching { runtimeCodingService: RuntimeCodingServiceProtocol, operationManager: OperationManagerProtocol, fallbackValue: T?, + callbackQueue: DispatchQueue, closure: @escaping (Result) -> Void ) -> CancellableCall @@ -15,11 +16,48 @@ protocol RuntimeConstantFetching { runtimeCodingService: RuntimeCodingServiceProtocol, operationManager: OperationManagerProtocol, fallbackValue: T?, + callbackQueue: DispatchQueue, closure: @escaping (Result) -> Void ) -> CancellableCall } extension RuntimeConstantFetching { + @discardableResult + func fetchConstant( + for path: ConstantCodingPath, + runtimeCodingService: RuntimeCodingServiceProtocol, + operationManager: OperationManagerProtocol, + fallbackValue: T?, + closure: @escaping (Result) -> Void + ) -> CancellableCall { + fetchConstant( + for: path, + runtimeCodingService: runtimeCodingService, + operationManager: operationManager, + fallbackValue: fallbackValue, + callbackQueue: .main, + closure: closure + ) + } + + @discardableResult + func fetchCompoundConstant( + for path: ConstantCodingPath, + runtimeCodingService: RuntimeCodingServiceProtocol, + operationManager: OperationManagerProtocol, + fallbackValue: T?, + closure: @escaping (Result) -> Void + ) -> CancellableCall { + fetchCompoundConstant( + for: path, + runtimeCodingService: runtimeCodingService, + operationManager: operationManager, + fallbackValue: fallbackValue, + callbackQueue: .main, + closure: closure + ) + } + @discardableResult func fetchConstant( for path: ConstantCodingPath, @@ -32,6 +70,7 @@ extension RuntimeConstantFetching { runtimeCodingService: runtimeCodingService, operationManager: operationManager, fallbackValue: nil, + callbackQueue: .main, closure: closure ) } @@ -48,6 +87,7 @@ extension RuntimeConstantFetching { runtimeCodingService: runtimeCodingService, operationManager: operationManager, fallbackValue: nil, + callbackQueue: .main, closure: closure ) } @@ -58,6 +98,7 @@ extension RuntimeConstantFetching { runtimeCodingService: RuntimeCodingServiceProtocol, operationManager: OperationManagerProtocol, fallbackValue: T?, + callbackQueue: DispatchQueue, closure: @escaping (Result) -> Void ) -> CancellableCall { let codingFactoryOperation = runtimeCodingService.fetchCoderFactoryOperation() @@ -73,7 +114,7 @@ extension RuntimeConstantFetching { constOperation.addDependency(codingFactoryOperation) constOperation.completionBlock = { - DispatchQueue.main.async { + callbackQueue.async { if let result = constOperation.result { closure(result) } else { @@ -93,6 +134,7 @@ extension RuntimeConstantFetching { runtimeCodingService: RuntimeCodingServiceProtocol, operationManager: OperationManagerProtocol, fallbackValue: T?, + callbackQueue: DispatchQueue, closure: @escaping (Result) -> Void ) -> CancellableCall { let codingFactoryOperation = runtimeCodingService.fetchCoderFactoryOperation() @@ -108,7 +150,7 @@ extension RuntimeConstantFetching { constOperation.addDependency(codingFactoryOperation) constOperation.completionBlock = { - DispatchQueue.main.async { + callbackQueue.async { if let result = constOperation.result { closure(result) } else { diff --git a/novawallet/Common/Services/BaseSyncService.swift b/novawallet/Common/Services/BaseSyncService.swift index 6d46585450..84f185323c 100644 --- a/novawallet/Common/Services/BaseSyncService.swift +++ b/novawallet/Common/Services/BaseSyncService.swift @@ -49,6 +49,8 @@ class BaseSyncService { fatalError("Method must be overriden by child class") } + func deactivate() {} + func markSyncingImmediate() { isSyncing = true } @@ -131,6 +133,8 @@ extension BaseSyncService: ApplicationServiceProtocol { isSyncing = false retryAttempt = 0 + + deactivate() } } diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionData.swift b/novawallet/Common/Services/ExternalBalanceUpdater/CrowdloanService/CrowdloanContributionData.swift similarity index 75% rename from novawallet/Common/Services/CrowdloanService/CrowdloanContributionData.swift rename to novawallet/Common/Services/ExternalBalanceUpdater/CrowdloanService/CrowdloanContributionData.swift index 099cf586ac..23df820b3b 100644 --- a/novawallet/Common/Services/CrowdloanService/CrowdloanContributionData.swift +++ b/novawallet/Common/Services/ExternalBalanceUpdater/CrowdloanService/CrowdloanContributionData.swift @@ -3,11 +3,15 @@ import RobinHood struct CrowdloanContributionData: Equatable { let accountId: AccountId - let chainId: ChainModel.Id + let chainAssetId: ChainAssetId let paraId: ParaId let source: String? let amount: BigUInt + var chainId: ChainModel.Id { + chainAssetId.chainId + } + var type: SourceType { if let source = source, !source.isEmpty { return .offChain @@ -24,17 +28,17 @@ struct CrowdloanContributionData: Equatable { extension CrowdloanContributionData: Identifiable { var identifier: String { - Self.createIdentifier(for: chainId, accountId: accountId, paraId: paraId, source: source) + Self.createIdentifier(for: chainAssetId, accountId: accountId, paraId: paraId, source: source) } static func createIdentifier( - for chainId: ChainModel.Id, + for chainAssetId: ChainAssetId, accountId: AccountId, paraId: ParaId, source: String? ) -> String { let data = [ - chainId, + chainAssetId.stringValue, accountId.toHex(), paraId.toHex(), source diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanOffChainSyncService.swift b/novawallet/Common/Services/ExternalBalanceUpdater/CrowdloanService/CrowdloanOffChainSyncService.swift similarity index 97% rename from novawallet/Common/Services/CrowdloanService/CrowdloanOffChainSyncService.swift rename to novawallet/Common/Services/ExternalBalanceUpdater/CrowdloanService/CrowdloanOffChainSyncService.swift index 1bfced4173..c6dcba78de 100644 --- a/novawallet/Common/Services/CrowdloanService/CrowdloanOffChainSyncService.swift +++ b/novawallet/Common/Services/ExternalBalanceUpdater/CrowdloanService/CrowdloanOffChainSyncService.swift @@ -43,7 +43,7 @@ final class CrowdloanOffChainSyncService: BaseSyncService { let remoteModels: [CrowdloanContributionData] = contributions.compactMap { CrowdloanContributionData( accountId: accountId, - chainId: chainId, + chainAssetId: ChainAssetId(chainId: chainId, assetId: AssetModel.utilityAssetId), paraId: $0.paraId, source: $0.source, amount: $0.amount diff --git a/novawallet/Common/Services/CrowdloanService/CrowdloanOnChainSyncService.swift b/novawallet/Common/Services/ExternalBalanceUpdater/CrowdloanService/CrowdloanOnChainSyncService.swift similarity index 98% rename from novawallet/Common/Services/CrowdloanService/CrowdloanOnChainSyncService.swift rename to novawallet/Common/Services/ExternalBalanceUpdater/CrowdloanService/CrowdloanOnChainSyncService.swift index adabcf3781..0bd451171d 100644 --- a/novawallet/Common/Services/CrowdloanService/CrowdloanOnChainSyncService.swift +++ b/novawallet/Common/Services/ExternalBalanceUpdater/CrowdloanService/CrowdloanOnChainSyncService.swift @@ -89,7 +89,7 @@ final class CrowdloanOnChainSyncService: BaseSyncService { } return CrowdloanContributionData( accountId: accountId, - chainId: chainId, + chainAssetId: ChainAssetId(chainId: chainId, assetId: AssetModel.utilityAssetId), paraId: $0.crowdloan.paraId, source: nil, amount: contribution.balance diff --git a/novawallet/Common/Services/CrowdloanService/RemoteCrowdloanContribution.swift b/novawallet/Common/Services/ExternalBalanceUpdater/CrowdloanService/RemoteCrowdloanContribution.swift similarity index 100% rename from novawallet/Common/Services/CrowdloanService/RemoteCrowdloanContribution.swift rename to novawallet/Common/Services/ExternalBalanceUpdater/CrowdloanService/RemoteCrowdloanContribution.swift diff --git a/novawallet/Common/Services/ExternalBalanceUpdater/PooledBalanceService/PooledBalanceUpdatingService.swift b/novawallet/Common/Services/ExternalBalanceUpdater/PooledBalanceService/PooledBalanceUpdatingService.swift new file mode 100644 index 0000000000..82d7c766fb --- /dev/null +++ b/novawallet/Common/Services/ExternalBalanceUpdater/PooledBalanceService/PooledBalanceUpdatingService.swift @@ -0,0 +1,275 @@ +import Foundation +import SubstrateSdk +import RobinHood + +final class PooledBalanceUpdatingService: BaseSyncService, RuntimeConstantFetching { + let accountId: AccountId + let chainAsset: ChainAsset + let connection: JSONRPCEngine + let runtimeService: RuntimeCodingServiceProtocol + let repository: AnyDataProviderRepository + let workingQueue: DispatchQueue + let operationQueue: OperationQueue + + private var poolMemberSubscription: CallbackStorageSubscription? + + private var stateSubscription: CallbackBatchStorageSubscription? + + private var state: PooledBalanceState? + + init( + accountId: AccountId, + chainAsset: ChainAsset, + repository: AnyDataProviderRepository, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue, + workingQueue: DispatchQueue, + logger: LoggerProtocol + ) { + self.accountId = accountId + self.chainAsset = chainAsset + self.repository = repository + self.connection = connection + self.runtimeService = runtimeService + self.workingQueue = workingQueue + self.operationQueue = operationQueue + + super.init(logger: logger) + } + + override func performSyncUp() { + clearSubscriptions() + + subscribePoolResolution(for: accountId) + } + + override func stopSyncUp() { + state = nil + clearSubscriptions() + + logger?.debug("Stop pool external sync for: \(chainAsset.chain.name) \(accountId.toHexString())") + } + + private func clearSubscriptions() { + clearPoolMemberSubscription() + clearStateSubscription() + } + + private func clearPoolMemberSubscription() { + poolMemberSubscription = nil + } + + private func clearStateSubscription() { + stateSubscription?.unsubscribe() + stateSubscription = nil + } + + private func subscribePoolResolution(for accountId: AccountId) { + let request = MapSubscriptionRequest(storagePath: NominationPools.poolMembersPath, localKey: .empty) { + BytesCodable(wrappedValue: accountId) + } + + poolMemberSubscription = CallbackStorageSubscription( + request: request, + connection: connection, + runtimeService: runtimeService, + repository: nil, + operationQueue: operationQueue, + callbackQueue: workingQueue + ) { [weak self] result in + self?.mutex.lock() + + self?.handlePoolMember(result: result, accountId: accountId) + + self?.mutex.unlock() + } + } + + private func handlePoolMember(result: Result, accountId: AccountId) { + switch result { + case let .success(optPoolMember): + markSyncingImmediate() + + if let poolMember = optPoolMember { + if let state = state, poolMember.poolId == state.poolId { + let newState = state.applying(newPoolMember: poolMember) + self.state = newState + saveState(newState) + return + } + + state = .init( + poolMember: poolMember, + ledger: nil, + bondedPool: nil, + subPools: nil + ) + + resolvePalletIdAndSubscribeState(for: poolMember, accountId: accountId) + } else { + state = nil + + saveState(nil) + } + case let .failure(error): + completeImmediate(error) + } + } + + private func resolvePalletIdAndSubscribeState(for poolMember: NominationPools.PoolMember, accountId _: AccountId) { + let currentPoolSubscription = poolMemberSubscription + + fetchCompoundConstant( + for: NominationPools.palletIdPath, + runtimeCodingService: runtimeService, + operationManager: OperationManager(operationQueue: operationQueue), + fallbackValue: nil, + callbackQueue: workingQueue + ) { [weak self] (result: Result) in + self?.mutex.lock() + + defer { + self?.mutex.unlock() + } + + guard self?.poolMemberSubscription === currentPoolSubscription else { + self?.logger?.warning("Tried to query pallet id but subscription changed") + return + } + + switch result { + case let .success(palletId): + if + let poolAccountId = try? NominationPools.derivedAccount( + for: poolMember.poolId, + accountType: .bonded, + palletId: palletId.wrappedValue + ) { + self?.logger?.debug("Derived pool account id: \(poolAccountId.toHex())") + + self?.subscribeState(for: poolAccountId, poolId: poolMember.poolId) + } else { + self?.logger?.error("Can't derive pool account id") + self?.completeImmediate(CommonError.dataCorruption) + } + case let .failure(error): + self?.logger?.error("Can't get pallet id \(error)") + self?.completeImmediate(error) + } + } + } + + private func subscribeState(for poolAccountId: AccountId, poolId: NominationPools.PoolId) { + clearStateSubscription() + + let ledgerRequest = BatchStorageSubscriptionRequest( + innerRequest: MapSubscriptionRequest( + storagePath: .stakingLedger, + localKey: .empty, + keyParamClosure: { + BytesCodable(wrappedValue: poolAccountId) + } + ), + mappingKey: PooledBalanceStateChange.Key.ledger.rawValue + ) + + let subPoolsRequest = BatchStorageSubscriptionRequest( + innerRequest: MapSubscriptionRequest( + storagePath: NominationPools.subPoolsPath, + localKey: .empty, + keyParamClosure: { + StringScaleMapper(value: poolId) + } + ), + mappingKey: PooledBalanceStateChange.Key.subpools.rawValue + ) + + let bondedPoolRequest = BatchStorageSubscriptionRequest( + innerRequest: MapSubscriptionRequest( + storagePath: NominationPools.bondedPoolPath, + localKey: .empty, + keyParamClosure: { + StringScaleMapper(value: poolId) + } + ), + mappingKey: PooledBalanceStateChange.Key.bonded.rawValue + ) + + stateSubscription = CallbackBatchStorageSubscription( + requests: [ledgerRequest, bondedPoolRequest, subPoolsRequest], + connection: connection, + runtimeService: runtimeService, + repository: nil, + operationQueue: operationQueue, + callbackQueue: workingQueue + ) { [weak self] result in + self?.mutex.lock() + + self?.handleStateSubscription(result: result) + + self?.mutex.unlock() + } + + stateSubscription?.subscribe() + } + + private func handleStateSubscription(result: Result) { + switch result { + case let .success(stateChange): + guard let state = state else { + completeImmediate(CommonError.dataCorruption) + logger?.error("Expected state but not found") + return + } + + let newState = state.applying(change: stateChange) + self.state = newState + + logger?.debug("Pool new state: \(newState)") + + saveState(newState) + case let .failure(error): + completeImmediate(error) + } + } + + private func saveState(_ state: PooledBalanceState?) { + let optItem: PooledAssetBalance? + let removeIdentifier: String? + + if let poolId = state?.poolId, let totalStake = state?.totalStake { + optItem = PooledAssetBalance( + chainAssetId: chainAsset.chainAssetId, + accountId: accountId, + amount: totalStake, + poolId: poolId + ) + + removeIdentifier = nil + } else { + optItem = nil + + removeIdentifier = PooledAssetBalance.createIdentifier(from: chainAsset.chainAssetId, accountId: accountId) + } + + let saveOperation = repository.saveOperation({ + optItem.map { [$0] } ?? [] + }, { + removeIdentifier.map { [$0] } ?? [] + }) + + saveOperation.completionBlock = { [weak self] in + self?.workingQueue.async { + do { + _ = try saveOperation.extractNoCancellableResultData() + self?.complete(nil) + } catch { + self?.complete(error) + } + } + } + + operationQueue.addOperation(saveOperation) + } +} diff --git a/novawallet/Common/Services/ExternalBalanceUpdater/PooledBalanceService/PooledBalanceUpdatingState.swift b/novawallet/Common/Services/ExternalBalanceUpdater/PooledBalanceService/PooledBalanceUpdatingState.swift new file mode 100644 index 0000000000..b852c6e0a9 --- /dev/null +++ b/novawallet/Common/Services/ExternalBalanceUpdater/PooledBalanceService/PooledBalanceUpdatingState.swift @@ -0,0 +1,88 @@ +import Foundation +import BigInt +import SubstrateSdk + +struct PooledBalanceStateChange: BatchStorageSubscriptionResult { + enum Key: String { + case ledger + case bonded + case subpools + } + + let ledger: UncertainStorage + let bondedPool: UncertainStorage + let subPools: UncertainStorage + + init( + values: [BatchStorageSubscriptionResultValue], + blockHashJson _: JSON, + context: [CodingUserInfoKey: Any]? + ) throws { + ledger = try UncertainStorage( + values: values, + mappingKey: Key.ledger.rawValue, + context: context + ) + + bondedPool = try UncertainStorage( + values: values, + mappingKey: Key.bonded.rawValue, + context: context + ) + + subPools = try UncertainStorage( + values: values, + mappingKey: Key.subpools.rawValue, + context: context + ) + } +} + +struct PooledBalanceState { + let poolMember: NominationPools.PoolMember + let ledger: StakingLedger? + let bondedPool: NominationPools.BondedPool? + let subPools: NominationPools.SubPools? + + var poolId: NominationPools.PoolId { + poolMember.poolId + } + + var totalStake: BigUInt? { + guard let bondedPool = bondedPool, let ledger = ledger else { + return nil + } + + let activeStake = NominationPools.pointsToBalance( + for: poolMember.points, + totalPoints: bondedPool.points, + poolBalance: ledger.active + ) + + let unbondingStake = subPools?.unbondingBalance(for: poolMember) ?? 0 + + return activeStake + unbondingStake + } + + func applying(change: PooledBalanceStateChange) -> PooledBalanceState { + let newLedger = change.ledger.valueWhenDefined(else: ledger) + let newBondedPool = change.bondedPool.valueWhenDefined(else: bondedPool) + let newSubPools = change.subPools.valueWhenDefined(else: subPools) + + return .init( + poolMember: poolMember, + ledger: newLedger, + bondedPool: newBondedPool, + subPools: newSubPools + ) + } + + func applying(newPoolMember: NominationPools.PoolMember) -> PooledBalanceState { + .init( + poolMember: newPoolMember, + ledger: ledger, + bondedPool: bondedPool, + subPools: subPools + ) + } +} diff --git a/novawallet/Common/Services/Multistaking/Multistaking+Model.swift b/novawallet/Common/Services/Multistaking/Multistaking+Model.swift index 6050a0fd1b..aecb4ffcd9 100644 --- a/novawallet/Common/Services/Multistaking/Multistaking+Model.swift +++ b/novawallet/Common/Services/Multistaking/Multistaking+Model.swift @@ -42,6 +42,11 @@ extension Multistaking { let state: Multistaking.ParachainState } + struct DashboardItemNominationPoolPart { + let stakingOption: OptionWithWallet + let state: Multistaking.NominationPoolState? + } + struct DashboardItemOffchainPart { let stakingOption: OptionWithWallet let maxApy: Decimal @@ -79,6 +84,22 @@ extension Multistaking { return .bonded } } + + static func from(nominationPoolState: Multistaking.NominationPoolState) -> DashboardItemOnchainState? { + guard nominationPoolState.bondedPool?.state == .open else { + return nil + } + + guard nominationPoolState.ledger != nil else { + return nil + } + + if let nomination = nominationPoolState.nomination, let stateEra = nominationPoolState.era { + return nomination.submittedIn >= stateEra.index ? .waiting : .active + } else { + return .bonded + } + } } struct DashboardItem: Equatable { @@ -95,6 +116,14 @@ extension Multistaking { let totalRewards: BigUInt? let maxApy: Decimal? + var hasStaking: Bool { + stake != nil + } + + var stakeOrZero: BigUInt { + stake ?? 0 + } + var state: State? { switch onchainState { case .none: @@ -102,12 +131,20 @@ extension Multistaking { case .bonded: return .inactive case .waiting: + guard stakeOrZero > 0 else { + return .inactive + } + if hasAssignedStake { return .active } else { return .waiting } case .active: + guard stakeOrZero > 0 else { + return .inactive + } + if hasAssignedStake { return .active } else { @@ -121,6 +158,7 @@ extension Multistaking { let stakingOption: Option let walletAccountId: AccountId let resolvedAccountId: AccountId + let rewardsAccountId: AccountId? } } diff --git a/novawallet/Common/Services/Multistaking/Multistaking+NominationPools.swift b/novawallet/Common/Services/Multistaking/Multistaking+NominationPools.swift new file mode 100644 index 0000000000..b5bba4625f --- /dev/null +++ b/novawallet/Common/Services/Multistaking/Multistaking+NominationPools.swift @@ -0,0 +1,105 @@ +import Foundation +import SubstrateSdk +import BigInt + +extension Multistaking { + struct NominationPoolStateChange: BatchStorageSubscriptionResult { + enum Key: String { + case era + case ledger + case nomination + case bonded + } + + let ledger: UncertainStorage + let bondedPool: UncertainStorage + let era: UncertainStorage + let nomination: UncertainStorage + + init( + values: [BatchStorageSubscriptionResultValue], + blockHashJson _: JSON, + context: [CodingUserInfoKey: Any]? + ) throws { + ledger = try UncertainStorage( + values: values, + mappingKey: Key.ledger.rawValue, + context: context + ) + + nomination = try UncertainStorage( + values: values, + mappingKey: Key.nomination.rawValue, + context: context + ) + + era = try UncertainStorage( + values: values, + mappingKey: Key.era.rawValue, + context: context + ) + + bondedPool = try UncertainStorage( + values: values, + mappingKey: Key.bonded.rawValue, + context: context + ) + } + } + + struct NominationPoolState { + let poolMember: NominationPools.PoolMember + let era: ActiveEraInfo? + let ledger: StakingLedger? + let nomination: Nomination? + let bondedPool: NominationPools.BondedPool? + + var poolId: NominationPools.PoolId { + poolMember.poolId + } + + var poolMemberStake: BigUInt? { + guard let bondedPool = bondedPool, let ledger = ledger else { + return nil + } + + return NominationPools.pointsToBalance( + for: poolMember.points, + totalPoints: bondedPool.points, + poolBalance: ledger.active + ) + } + + func applying(change: NominationPoolStateChange) -> NominationPoolState { + let newEra: ActiveEraInfo? + + if case let .defined(activeEra) = change.era { + newEra = activeEra + } else { + newEra = era + } + + let newLedger = change.ledger.valueWhenDefined(else: ledger) + let newNomination = change.nomination.valueWhenDefined(else: nomination) + let newBondedPool = change.bondedPool.valueWhenDefined(else: bondedPool) + + return .init( + poolMember: poolMember, + era: newEra, + ledger: newLedger, + nomination: newNomination, + bondedPool: newBondedPool + ) + } + + func applying(newPoolMember: NominationPools.PoolMember) -> NominationPoolState { + .init( + poolMember: newPoolMember, + era: era, + ledger: ledger, + nomination: nomination, + bondedPool: bondedPool + ) + } + } +} diff --git a/novawallet/Common/Services/Multistaking/Multistaking+Relaychain.swift b/novawallet/Common/Services/Multistaking/Multistaking+Relaychain.swift index 3bb21e61e9..6401a5588a 100644 --- a/novawallet/Common/Services/Multistaking/Multistaking+Relaychain.swift +++ b/novawallet/Common/Services/Multistaking/Multistaking+Relaychain.swift @@ -18,13 +18,13 @@ extension Multistaking { ) throws { stash = try UncertainStorage( values: values, - localKey: Key.stash.rawValue, + mappingKey: Key.stash.rawValue, context: context ).map { $0?.stash } controller = try UncertainStorage( values: values, - localKey: Key.controller.rawValue, + mappingKey: Key.controller.rawValue, context: context ).map { $0?.wrappedValue } } @@ -71,25 +71,25 @@ extension Multistaking { ) throws { ledger = try UncertainStorage( values: values, - localKey: Key.ledger.rawValue, + mappingKey: Key.ledger.rawValue, context: context ) nomination = try UncertainStorage( values: values, - localKey: Key.nomination.rawValue, + mappingKey: Key.nomination.rawValue, context: context ) validatorPrefs = try UncertainStorage( values: values, - localKey: Key.validatorPrefs.rawValue, + mappingKey: Key.validatorPrefs.rawValue, context: context ) era = try UncertainStorage( values: values, - localKey: Key.era.rawValue, + mappingKey: Key.era.rawValue, context: context ) } diff --git a/novawallet/Common/Services/Multistaking/Multistaking.swift b/novawallet/Common/Services/Multistaking/Multistaking.swift index f330bd8e3c..8839a1cf2b 100644 --- a/novawallet/Common/Services/Multistaking/Multistaking.swift +++ b/novawallet/Common/Services/Multistaking/Multistaking.swift @@ -17,8 +17,21 @@ enum Multistaking { } } + struct OffchainFilters: Hashable { + let stateFilters: [OffchainFilter] + let rewardFilters: [OffchainFilter] + + func adding(newStateFilters: [OffchainFilter], newRewardFilters: [OffchainFilter]) -> OffchainFilters { + .init( + stateFilters: stateFilters + newStateFilters, + rewardFilters: rewardFilters + newRewardFilters + ) + } + } + struct OffchainRequest { - let filters: Set + let stateFilters: Set + let rewardFilters: Set } enum OffchainStakingState: Hashable { @@ -36,3 +49,14 @@ enum Multistaking { typealias OffchainResponse = Set } + +extension Set where Element == Multistaking.OffchainFilter { + func groupByNetworkStaking() -> [Multistaking.Option: Multistaking.OffchainFilter] { + reduce(into: [:]) { accumulator, filter in + filter.stakingTypes.forEach { stakingType in + let key = Multistaking.Option(chainAssetId: filter.chainAsset.chainAssetId, type: stakingType) + accumulator[key] = filter + } + } + } +} diff --git a/novawallet/Common/Services/Multistaking/MultistakingRepositoryFactory.swift b/novawallet/Common/Services/Multistaking/MultistakingRepositoryFactory.swift index 7746fb94d8..ef5f41b71a 100644 --- a/novawallet/Common/Services/Multistaking/MultistakingRepositoryFactory.swift +++ b/novawallet/Common/Services/Multistaking/MultistakingRepositoryFactory.swift @@ -4,7 +4,9 @@ import RobinHood protocol MultistakingRepositoryFactoryProtocol { var storageFacade: StorageFacadeProtocol { get } - func createDashboardRepository(for walletId: MetaAccountModel.Id) -> AnyDataProviderRepository + func createDashboardRepository( + for walletId: MetaAccountModel.Id + ) -> AnyDataProviderRepository func createResolvedAccountRepository( ) -> AnyDataProviderRepository @@ -17,6 +19,9 @@ protocol MultistakingRepositoryFactoryProtocol { func createParachainRepository( ) -> AnyDataProviderRepository + + func createNominationPoolsRepository( + ) -> AnyDataProviderRepository } final class MultistakingRepositoryFactory { @@ -49,6 +54,13 @@ extension MultistakingRepositoryFactory: MultistakingRepositoryFactoryProtocol { return AnyDataProviderRepository(repository) } + func createNominationPoolsRepository( + ) -> AnyDataProviderRepository { + let mapper = StakingDashboardNominationPoolMapper() + let repository = storageFacade.createRepository(mapper: AnyCoreDataMapper(mapper)) + return AnyDataProviderRepository(repository) + } + func createParachainRepository( ) -> AnyDataProviderRepository { let mapper = StakingDashboardParachainMapper() diff --git a/novawallet/Common/Services/Multistaking/MultistakingServices.swift b/novawallet/Common/Services/Multistaking/MultistakingServices.swift index 2c31155895..f2c22ec43b 100644 --- a/novawallet/Common/Services/Multistaking/MultistakingServices.swift +++ b/novawallet/Common/Services/Multistaking/MultistakingServices.swift @@ -8,45 +8,87 @@ protocol MultistakingOffchainOperationFactoryProtocol { } extension MultistakingOffchainOperationFactoryProtocol { + private func resolveAccountId( + from accounts: [Multistaking.Option: AccountId], + option: Multistaking.Option, + defaultAccountResponse: ChainAccountResponse + ) -> AccountId? { + switch option.type { + case .relaychain, .auraRelaychain, .azero, .parachain, .turing, .unsupported: + return accounts[option] ?? defaultAccountResponse.accountId + case .nominationPools: + // we don't want to use default account as it might be connected to direct staking + return accounts[option] + } + } + + private func createWrapperFilters( + for accounts: [Multistaking.Option: AccountId], + chainAsset: ChainAsset, + defaultAccountResponse: ChainAccountResponse + ) -> [Multistaking.OffchainFilter] { + let stakingTypes = chainAsset.asset.supportedStakings ?? [] + + let filters = stakingTypes.reduce(into: [AccountId: Multistaking.OffchainFilter]()) { result, stakingType in + let stakingOption = Multistaking.Option(chainAssetId: chainAsset.chainAssetId, type: stakingType) + + guard + let accountId = resolveAccountId( + from: accounts, + option: stakingOption, + defaultAccountResponse: defaultAccountResponse + ) else { + return + } + + if let existingFilter = result[accountId] { + result[accountId] = existingFilter.adding(newStakingTypes: [stakingType]) + } else { + result[accountId] = Multistaking.OffchainFilter( + chainAsset: chainAsset, + stakingTypes: [stakingType], + accountId: accountId + ) + } + } + + return Array(filters.values) + } + func createWrapper( from wallet: MetaAccountModel, - resolvedAccounts: [Multistaking.Option: AccountId], + bondedAccounts: [Multistaking.Option: AccountId], + rewardAccounts: [Multistaking.Option: AccountId], chainAssets: Set ) -> CompoundOperationWrapper { - let filters: [Multistaking.OffchainFilter] = chainAssets.flatMap { chainAsset in + let filters: Multistaking.OffchainFilters = chainAssets.reduce( + Multistaking.OffchainFilters(stateFilters: [], rewardFilters: []) + ) { result, chainAsset in guard chainAsset.asset.hasStaking, let account = wallet.fetch(for: chainAsset.chain.accountRequest()) else { - return [Multistaking.OffchainFilter]() + return result } - let stakingTypes = chainAsset.asset.supportedStakings ?? [] - - let accountIds = stakingTypes.reduce( - into: [AccountId: Multistaking.OffchainFilter]() - ) { result, stakingType in - let stakingOption = Multistaking.Option( - chainAssetId: chainAsset.chainAssetId, - type: stakingType - ) + let stateFilters = createWrapperFilters( + for: bondedAccounts, + chainAsset: chainAsset, + defaultAccountResponse: account + ) - let accountId = resolvedAccounts[stakingOption] ?? account.accountId - - if let existingFilter = result[accountId] { - result[accountId] = existingFilter.adding(newStakingTypes: [stakingType]) - } else { - result[accountId] = Multistaking.OffchainFilter( - chainAsset: chainAsset, - stakingTypes: [stakingType], - accountId: accountId - ) - } - } + let rewardFilters = createWrapperFilters( + for: rewardAccounts, + chainAsset: chainAsset, + defaultAccountResponse: account + ) - return Array(accountIds.values) + return result.adding(newStateFilters: stateFilters, newRewardFilters: rewardFilters) } - let request = Multistaking.OffchainRequest(filters: Set(filters)) + let request = Multistaking.OffchainRequest( + stateFilters: Set(filters.stateFilters), + rewardFilters: Set(filters.rewardFilters) + ) return createWrapper(for: request) } diff --git a/novawallet/Common/Services/Multistaking/MultistakingSyncService.swift b/novawallet/Common/Services/Multistaking/MultistakingSyncService.swift index f65bea351c..cc73b717ca 100644 --- a/novawallet/Common/Services/Multistaking/MultistakingSyncService.swift +++ b/novawallet/Common/Services/Multistaking/MultistakingSyncService.swift @@ -20,7 +20,8 @@ final class MultistakingSyncService { typealias OnchainSyncServiceProtocol = ObservableSyncServiceProtocol & ApplicationServiceProtocol let chainRegistry: ChainRegistryProtocol - let repositoryFactory: MultistakingRepositoryFactoryProtocol + let multistakingRepositoryFactory: MultistakingRepositoryFactoryProtocol + let substrateRepositoryFactory: SubstrateRepositoryFactoryProtocol let providerFactory: MultistakingProviderFactoryProtocol let offchainOperationFactory: MultistakingOffchainOperationFactoryProtocol let operationQueue: OperationQueue @@ -43,7 +44,8 @@ final class MultistakingSyncService { wallet: MetaAccountModel, chainRegistry: ChainRegistryProtocol, providerFactory: MultistakingProviderFactoryProtocol, - repositoryFactory: MultistakingRepositoryFactoryProtocol, + multistakingRepositoryFactory: MultistakingRepositoryFactoryProtocol, + substrateRepositoryFactory: SubstrateRepositoryFactoryProtocol, offchainOperationFactory: MultistakingOffchainOperationFactoryProtocol, operationQueue: OperationQueue = OperationManagerFacade.assetsRepositoryQueue, workingQueue: DispatchQueue = DispatchQueue( @@ -56,7 +58,8 @@ final class MultistakingSyncService { self.wallet = wallet self.chainRegistry = chainRegistry self.providerFactory = providerFactory - self.repositoryFactory = repositoryFactory + self.multistakingRepositoryFactory = multistakingRepositoryFactory + self.substrateRepositoryFactory = substrateRepositoryFactory self.offchainOperationFactory = offchainOperationFactory self.workingQueue = workingQueue self.operationQueue = operationQueue @@ -110,7 +113,7 @@ final class MultistakingSyncService { private func setupOffchainService() { let accountProvider = providerFactory.createResolvedAccountsProvider() - let dashboardRepository = repositoryFactory.createOffchainRepository() + let dashboardRepository = multistakingRepositoryFactory.createOffchainRepository() offchainUpdater = OffchainMultistakingUpdateService( wallet: wallet, @@ -206,6 +209,19 @@ final class MultistakingSyncService { addSyncHandler(for: service, stakingOption: stakingOption) + if isActive { + service.setup() + } + } + case .nominationPools: + if let service = createPoolsStaking( + for: chainAssetOption.chainAsset, + stakingType: chainAssetOption.type + ) { + onchainUpdaters[stakingOption] = service + + addSyncHandler(for: service, stakingOption: stakingOption) + if isActive { service.setup() } @@ -228,17 +244,51 @@ final class MultistakingSyncService { guard let account = wallet.fetch(for: chainAsset.chain.accountRequest()), let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), - let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { + let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), + let accountAddress = try? account.accountId.toAddress(using: chainAsset.chain.chainFormat) else { return nil } + let stashItemRepository = substrateRepositoryFactory.createStashItemRepository( + for: accountAddress, chainId: chainAsset.chain.chainId + ) + return RelaychainMultistakingUpdateService( walletId: wallet.metaId, accountId: account.accountId, chainAsset: chainAsset, stakingType: stakingType, - dashboardRepository: repositoryFactory.createRelaychainRepository(), - accountRepository: repositoryFactory.createResolvedAccountRepository(), + dashboardRepository: multistakingRepositoryFactory.createRelaychainRepository(), + accountRepository: multistakingRepositoryFactory.createResolvedAccountRepository(), + cacheRepository: substrateRepositoryFactory.createChainStorageItemRepository(), + stashItemRepository: stashItemRepository, + connection: connection, + runtimeService: runtimeService, + operationQueue: operationQueue, + workingQueue: workingQueue, + logger: logger + ) + } + + private func createPoolsStaking( + for chainAsset: ChainAsset, + stakingType: StakingType + ) -> OnchainSyncServiceProtocol? { + guard + let account = wallet.fetch(for: chainAsset.chain.accountRequest()), + let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { + return nil + } + + return PoolsMultistakingUpdateService( + walletId: wallet.metaId, + accountId: account.accountId, + chainAsset: chainAsset, + stakingType: stakingType, + dashboardRepository: multistakingRepositoryFactory.createNominationPoolsRepository(), + accountRepository: multistakingRepositoryFactory.createResolvedAccountRepository(), + cacheRepository: substrateRepositoryFactory.createChainStorageItemRepository(), connection: connection, runtimeService: runtimeService, operationQueue: operationQueue, @@ -278,7 +328,8 @@ final class MultistakingSyncService { accountId: account.accountId, chainAsset: chainAsset, stakingType: stakingType, - dashboardRepository: repositoryFactory.createParachainRepository(), + dashboardRepository: multistakingRepositoryFactory.createParachainRepository(), + cacheRepository: substrateRepositoryFactory.createChainStorageItemRepository(), connection: connection, runtimeService: runtimeService, operationFactory: operationFactory, diff --git a/novawallet/Common/Services/Multistaking/MultistakingSyncServiceFactory.swift b/novawallet/Common/Services/Multistaking/MultistakingSyncServiceFactory.swift index b2a9e6c7fa..c43044403c 100644 --- a/novawallet/Common/Services/Multistaking/MultistakingSyncServiceFactory.swift +++ b/novawallet/Common/Services/Multistaking/MultistakingSyncServiceFactory.swift @@ -22,9 +22,11 @@ final class MultistakingSyncServiceFactory: MultistakingSyncServiceFactoryProtoc func createService(for wallet: MetaAccountModel) -> MultistakingSyncServiceProtocol { let operationQueue = OperationManagerFacade.sharedDefaultQueue - let repositoryFactory = MultistakingRepositoryFactory(storageFacade: storageFacade) + let multistakingRepositoryFactory = MultistakingRepositoryFactory(storageFacade: storageFacade) + let substrateRepositoryFactory = SubstrateRepositoryFactory(storageFacade: storageFacade) + let providerFactory = MultistakingProviderFactory( - repositoryFactory: repositoryFactory, + repositoryFactory: multistakingRepositoryFactory, operationQueue: operationQueue ) @@ -34,7 +36,8 @@ final class MultistakingSyncServiceFactory: MultistakingSyncServiceFactoryProtoc wallet: wallet, chainRegistry: chainRegistry, providerFactory: providerFactory, - repositoryFactory: repositoryFactory, + multistakingRepositoryFactory: multistakingRepositoryFactory, + substrateRepositoryFactory: substrateRepositoryFactory, offchainOperationFactory: offchainOperationFactory ) } diff --git a/novawallet/Common/Services/Multistaking/OffchainMultistakingUpdateService.swift b/novawallet/Common/Services/Multistaking/OffchainMultistakingUpdateService.swift index b031d14aeb..7797709bfe 100644 --- a/novawallet/Common/Services/Multistaking/OffchainMultistakingUpdateService.swift +++ b/novawallet/Common/Services/Multistaking/OffchainMultistakingUpdateService.swift @@ -70,11 +70,13 @@ final class OffchainMultistakingUpdateService: ObservableSyncService, AnyCancell private func performSyncUpInternal() { logger?.debug("Will start syncing...") - let resolvedAccountIds = resolvedAccounts.mapValues { $0.resolvedAccountId } + let bondedAccountIds = resolvedAccounts.mapValues { $0.resolvedAccountId } + let rewardAccountIds = resolvedAccounts.compactMapValues { $0.rewardsAccountId } let wrapper = operationFactory.createWrapper( from: wallet, - resolvedAccounts: resolvedAccountIds, + bondedAccounts: bondedAccountIds, + rewardAccounts: rewardAccountIds, chainAssets: chainAssets ) diff --git a/novawallet/Common/Services/Multistaking/ParachainMultistakingUpdateService.swift b/novawallet/Common/Services/Multistaking/ParachainMultistakingUpdateService.swift index 011ff62b1f..3bd49a11b4 100644 --- a/novawallet/Common/Services/Multistaking/ParachainMultistakingUpdateService.swift +++ b/novawallet/Common/Services/Multistaking/ParachainMultistakingUpdateService.swift @@ -10,6 +10,7 @@ final class ParachainMultistakingUpdateService: ObservableSyncService, AnyCancel let connection: JSONRPCEngine let runtimeService: RuntimeCodingServiceProtocol let dashboardRepository: AnyDataProviderRepository + let cacheRepository: AnyDataProviderRepository let operationFactory: ParaStkCollatorsOperationFactoryProtocol let operationQueue: OperationQueue let workingQueue: DispatchQueue @@ -23,6 +24,7 @@ final class ParachainMultistakingUpdateService: ObservableSyncService, AnyCancel chainAsset: ChainAsset, stakingType: StakingType, dashboardRepository: AnyDataProviderRepository, + cacheRepository: AnyDataProviderRepository, connection: JSONRPCEngine, runtimeService: RuntimeCodingServiceProtocol, operationFactory: ParaStkCollatorsOperationFactoryProtocol, @@ -35,6 +37,7 @@ final class ParachainMultistakingUpdateService: ObservableSyncService, AnyCancel self.chainAsset = chainAsset self.stakingType = stakingType self.dashboardRepository = dashboardRepository + self.cacheRepository = cacheRepository self.connection = connection self.runtimeService = runtimeService self.operationFactory = operationFactory @@ -59,24 +62,36 @@ final class ParachainMultistakingUpdateService: ObservableSyncService, AnyCancel } private func subscribeDelegatorState(for accountId: AccountId) { - let request = MapSubscriptionRequest( - storagePath: ParachainStaking.delegatorStatePath, - localKey: "" - ) { BytesCodable(wrappedValue: accountId) } - - subscription = CallbackStorageSubscription( - request: request, - connection: connection, - runtimeService: runtimeService, - repository: nil, - operationQueue: operationQueue, - callbackQueue: workingQueue - ) { [weak self] result in - self?.mutex.lock() - - self?.handleDelegatorState(result: result) + do { + let localKey = try LocalStorageKeyFactory().createFromStoragePath( + ParachainStaking.delegatorStatePath, + accountId: accountId, + chainId: chainAsset.chain.chainId + ) + + let request = MapSubscriptionRequest( + storagePath: ParachainStaking.delegatorStatePath, + localKey: localKey + ) { BytesCodable(wrappedValue: accountId) } + + subscription = CallbackStorageSubscription( + request: request, + connection: connection, + runtimeService: runtimeService, + repository: cacheRepository, + operationQueue: operationQueue, + callbackQueue: workingQueue + ) { [weak self] result in + self?.mutex.lock() + + self?.handleDelegatorState(result: result) + + self?.mutex.unlock() + } + } catch { + logger?.error("Subscription error: \(error)") - self?.mutex.unlock() + completeImmediate(error) } } diff --git a/novawallet/Common/Services/Multistaking/PoolsMultistakingUpdateService.swift b/novawallet/Common/Services/Multistaking/PoolsMultistakingUpdateService.swift new file mode 100644 index 0000000000..3669d52d69 --- /dev/null +++ b/novawallet/Common/Services/Multistaking/PoolsMultistakingUpdateService.swift @@ -0,0 +1,368 @@ +import Foundation +import SubstrateSdk +import RobinHood + +final class PoolsMultistakingUpdateService: ObservableSyncService, RuntimeConstantFetching { + let accountId: AccountId + let walletId: MetaAccountModel.Id + let chainAsset: ChainAsset + let stakingType: StakingType + let connection: JSONRPCEngine + let runtimeService: RuntimeCodingServiceProtocol + let dashboardRepository: AnyDataProviderRepository + let accountRepository: AnyDataProviderRepository + let cacheRepository: AnyDataProviderRepository + let workingQueue: DispatchQueue + let operationQueue: OperationQueue + + private var poolMemberSubscription: CallbackStorageSubscription? + + private var stateSubscription: CallbackBatchStorageSubscription? + + private var state: Multistaking.NominationPoolState? + + private lazy var localStorageKeyFactory = LocalStorageKeyFactory() + + init( + walletId: MetaAccountModel.Id, + accountId: AccountId, + chainAsset: ChainAsset, + stakingType: StakingType, + dashboardRepository: AnyDataProviderRepository, + accountRepository: AnyDataProviderRepository, + cacheRepository: AnyDataProviderRepository, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue, + workingQueue: DispatchQueue, + logger: LoggerProtocol + ) { + self.walletId = walletId + self.accountId = accountId + self.chainAsset = chainAsset + self.stakingType = stakingType + self.dashboardRepository = dashboardRepository + self.accountRepository = accountRepository + self.cacheRepository = cacheRepository + self.connection = connection + self.runtimeService = runtimeService + self.workingQueue = workingQueue + self.operationQueue = operationQueue + + super.init(logger: logger) + } + + override func performSyncUp() { + clearSubscriptions() + + subscribePoolResolution(for: accountId) + } + + override func stopSyncUp() { + state = nil + clearSubscriptions() + } + + private func clearSubscriptions() { + clearPoolMemberSubscription() + clearStateSubscription() + } + + private func clearPoolMemberSubscription() { + poolMemberSubscription = nil + } + + private func clearStateSubscription() { + stateSubscription?.unsubscribe() + stateSubscription = nil + } + + private func subscribePoolResolution(for accountId: AccountId) { + do { + let localKey = try localStorageKeyFactory.createFromStoragePath( + NominationPools.poolMembersPath, + accountId: accountId, + chainId: chainAsset.chain.chainId + ) + + let request = MapSubscriptionRequest(storagePath: NominationPools.poolMembersPath, localKey: localKey) { + BytesCodable(wrappedValue: accountId) + } + + poolMemberSubscription = CallbackStorageSubscription( + request: request, + connection: connection, + runtimeService: runtimeService, + repository: cacheRepository, + operationQueue: operationQueue, + callbackQueue: workingQueue + ) { [weak self] result in + self?.mutex.lock() + + self?.handlePoolMember(result: result, accountId: accountId) + + self?.mutex.unlock() + } + } catch { + logger?.error("Pool resolution failed: \(error)") + completeImmediate(error) + } + } + + private func handlePoolMember(result: Result, accountId: AccountId) { + switch result { + case let .success(optPoolMember): + markSyncingImmediate() + + if let poolMember = optPoolMember { + if let state = state, poolMember.poolId == state.poolId { + let newState = state.applying(newPoolMember: poolMember) + self.state = newState + saveState(newState) + return + } + + state = Multistaking.NominationPoolState( + poolMember: poolMember, + era: nil, + ledger: nil, + nomination: nil, + bondedPool: nil + ) + + resolvePalletIdAndSubscribeState(for: poolMember, accountId: accountId) + } else { + saveAccountChanges(for: nil, walletAccountId: accountId) + + state = nil + + saveState(nil) + } + case let .failure(error): + completeImmediate(error) + } + } + + private func resolvePalletIdAndSubscribeState(for poolMember: NominationPools.PoolMember, accountId: AccountId) { + let currentPoolSubscription = poolMemberSubscription + + fetchCompoundConstant( + for: NominationPools.palletIdPath, + runtimeCodingService: runtimeService, + operationManager: OperationManager(operationQueue: operationQueue), + fallbackValue: nil, + callbackQueue: workingQueue + ) { [weak self] (result: Result) in + self?.mutex.lock() + + defer { + self?.mutex.unlock() + } + + guard self?.poolMemberSubscription === currentPoolSubscription else { + self?.logger?.warning("Tried to query pallet id but subscription changed") + return + } + + switch result { + case let .success(palletId): + if + let poolAccountId = try? NominationPools.derivedAccount( + for: poolMember.poolId, + accountType: .bonded, + palletId: palletId.wrappedValue + ) { + self?.logger?.debug("Derived pool account id: \(poolAccountId.toHex())") + + self?.saveAccountChanges(for: poolAccountId, walletAccountId: accountId) + self?.subscribeState(for: poolAccountId, poolId: poolMember.poolId) + } else { + self?.logger?.error("Can't derive pool account id") + self?.completeImmediate(CommonError.dataCorruption) + } + case let .failure(error): + self?.logger?.error("Can't get pallet id \(error)") + self?.completeImmediate(error) + } + } + } + + private func saveAccountChanges(for poolAccountId: AccountId?, walletAccountId: AccountId) { + let stakingOption = Multistaking.Option(chainAssetId: chainAsset.chainAssetId, type: stakingType) + + let saveOperation = accountRepository.saveOperation({ + if let poolAccountId = poolAccountId { + let account = Multistaking.ResolvedAccount( + stakingOption: stakingOption, + walletAccountId: walletAccountId, + resolvedAccountId: poolAccountId, + rewardsAccountId: walletAccountId + ) + + return [account] + } else { + return [] + } + }, { + if poolAccountId == nil { + let identifier = Multistaking.ResolvedAccount.createIdentifier( + from: walletAccountId, + stakingOption: stakingOption + ) + + return [identifier] + } else { + return [] + } + }) + + saveOperation.completionBlock = { [weak self] in + self?.workingQueue.async { + do { + _ = try saveOperation.extractNoCancellableResultData() + } catch { + self?.logger?.error("Can't save pool account id") + } + } + } + + operationQueue.addOperation(saveOperation) + } + + // swiftlint:disable:next function_body_length + private func subscribeState(for poolAccountId: AccountId, poolId: NominationPools.PoolId) { + do { + clearStateSubscription() + + let eraRequest = BatchStorageSubscriptionRequest( + innerRequest: UnkeyedSubscriptionRequest( + storagePath: .activeEra, + localKey: "" + ), + mappingKey: Multistaking.NominationPoolStateChange.Key.era.rawValue + ) + + let ledgerLocalKey = try localStorageKeyFactory.createFromStoragePath( + .stakingLedger, + accountId: poolAccountId, + chainId: chainAsset.chain.chainId + ) + + let ledgerRequest = BatchStorageSubscriptionRequest( + innerRequest: MapSubscriptionRequest( + storagePath: .stakingLedger, + localKey: ledgerLocalKey, + keyParamClosure: { + BytesCodable(wrappedValue: poolAccountId) + } + ), + mappingKey: Multistaking.NominationPoolStateChange.Key.ledger.rawValue + ) + + let nominationLocalKey = try localStorageKeyFactory.createFromStoragePath( + .nominators, + accountId: poolAccountId, + chainId: chainAsset.chain.chainId + ) + + let nominationRequest = BatchStorageSubscriptionRequest( + innerRequest: MapSubscriptionRequest( + storagePath: .nominators, + localKey: nominationLocalKey, + keyParamClosure: { + BytesCodable(wrappedValue: poolAccountId) + } + ), + mappingKey: Multistaking.NominationPoolStateChange.Key.nomination.rawValue + ) + + let bondedLocalKey = try localStorageKeyFactory.createFromStoragePath( + NominationPools.bondedPoolPath, + encodableElement: poolId, + chainId: chainAsset.chain.chainId + ) + + let bondedPoolRequest = BatchStorageSubscriptionRequest( + innerRequest: MapSubscriptionRequest( + storagePath: NominationPools.bondedPoolPath, + localKey: bondedLocalKey, + keyParamClosure: { + StringScaleMapper(value: poolId) + } + ), + mappingKey: Multistaking.NominationPoolStateChange.Key.bonded.rawValue + ) + + stateSubscription = CallbackBatchStorageSubscription( + requests: [ledgerRequest, nominationRequest, eraRequest, bondedPoolRequest], + connection: connection, + runtimeService: runtimeService, + repository: cacheRepository, + operationQueue: operationQueue, + callbackQueue: workingQueue + ) { [weak self] result in + self?.mutex.lock() + + self?.handleStateSubscription(result: result) + + self?.mutex.unlock() + } + + stateSubscription?.subscribe() + } catch { + logger?.error("Subscription failed: \(error)") + completeImmediate(error) + } + } + + private func handleStateSubscription(result: Result) { + switch result { + case let .success(stateChange): + guard let state = state else { + completeImmediate(CommonError.dataCorruption) + logger?.error("Expected state but not found") + return + } + + let newState = state.applying(change: stateChange) + self.state = newState + + logger?.debug("Pool new state: \(newState)") + + saveState(newState) + case let .failure(error): + completeImmediate(error) + } + } + + private func saveState(_ state: Multistaking.NominationPoolState?) { + let stakingOption = Multistaking.OptionWithWallet( + walletId: walletId, + option: .init(chainAssetId: chainAsset.chainAssetId, type: stakingType) + ) + + let dashboardItem = Multistaking.DashboardItemNominationPoolPart( + stakingOption: stakingOption, + state: state + ) + + let saveOperation = dashboardRepository.saveOperation({ + [dashboardItem] + }, { + [] + }) + + saveOperation.completionBlock = { [weak self] in + self?.workingQueue.async { + do { + _ = try saveOperation.extractNoCancellableResultData() + self?.complete(nil) + } catch { + self?.complete(error) + } + } + } + + operationQueue.addOperation(saveOperation) + } +} diff --git a/novawallet/Common/Services/Multistaking/RelaychainMultistakingUpdateService.swift b/novawallet/Common/Services/Multistaking/RelaychainMultistakingUpdateService.swift index a39fb0cf5d..df27ece804 100644 --- a/novawallet/Common/Services/Multistaking/RelaychainMultistakingUpdateService.swift +++ b/novawallet/Common/Services/Multistaking/RelaychainMultistakingUpdateService.swift @@ -11,6 +11,8 @@ final class RelaychainMultistakingUpdateService: ObservableSyncService { let runtimeService: RuntimeCodingServiceProtocol let dashboardRepository: AnyDataProviderRepository let accountRepository: AnyDataProviderRepository + let cacheRepository: AnyDataProviderRepository + let stashItemRepository: AnyDataProviderRepository let workingQueue: DispatchQueue let operationQueue: OperationQueue @@ -27,6 +29,8 @@ final class RelaychainMultistakingUpdateService: ObservableSyncService { stakingType: StakingType, dashboardRepository: AnyDataProviderRepository, accountRepository: AnyDataProviderRepository, + cacheRepository: AnyDataProviderRepository, + stashItemRepository: AnyDataProviderRepository, connection: JSONRPCEngine, runtimeService: RuntimeCodingServiceProtocol, operationQueue: OperationQueue, @@ -39,6 +43,8 @@ final class RelaychainMultistakingUpdateService: ObservableSyncService { self.stakingType = stakingType self.dashboardRepository = dashboardRepository self.accountRepository = accountRepository + self.cacheRepository = cacheRepository + self.stashItemRepository = stashItemRepository self.connection = connection self.runtimeService = runtimeService self.workingQueue = workingQueue @@ -73,19 +79,25 @@ final class RelaychainMultistakingUpdateService: ObservableSyncService { } private func subscribeControllerResolution(for accountId: AccountId) { - let controllerRequest = MapSubscriptionRequest( - storagePath: .controller, - localKey: Multistaking.RelaychainAccountsChange.Key.controller.rawValue - ) { - BytesCodable(wrappedValue: accountId) - } + let controllerRequest = BatchStorageSubscriptionRequest( + innerRequest: MapSubscriptionRequest( + storagePath: .controller, + localKey: "" + ) { + BytesCodable(wrappedValue: accountId) + }, + mappingKey: Multistaking.RelaychainAccountsChange.Key.controller.rawValue + ) - let ledgerRequest = MapSubscriptionRequest( - storagePath: .stakingLedger, - localKey: Multistaking.RelaychainAccountsChange.Key.stash.rawValue - ) { - BytesCodable(wrappedValue: accountId) - } + let ledgerRequest = BatchStorageSubscriptionRequest( + innerRequest: MapSubscriptionRequest( + storagePath: .stakingLedger, + localKey: "" + ) { + BytesCodable(wrappedValue: accountId) + }, + mappingKey: Multistaking.RelaychainAccountsChange.Key.stash.rawValue + ) controllerSubscription = CallbackBatchStorageSubscription( requests: [controllerRequest, ledgerRequest], @@ -116,7 +128,17 @@ final class RelaychainMultistakingUpdateService: ObservableSyncService { case let .defined(controller) = accounts.controller { markSyncingImmediate() - saveStashChange(stash ?? accountId) + saveResolvedAccounts(stash ?? accountId) + + if stash != nil || controller != nil { + saveStashItem( + stash: stash ?? accountId, + controller: controller ?? accountId, + chain: chainAsset.chain + ) + } else { + saveStashItem(stash: nil, controller: nil, chain: chainAsset.chain) + } subscribeState( for: controller ?? accountId, @@ -128,51 +150,89 @@ final class RelaychainMultistakingUpdateService: ObservableSyncService { } } + // swiftlint:disable:next function_body_length private func subscribeState(for controller: AccountId, stash: AccountId) { - clearStateSubscription() + do { + clearStateSubscription() - let ledgerRequest = MapSubscriptionRequest( - storagePath: .stakingLedger, - localKey: Multistaking.RelaychainStateChange.Key.ledger.rawValue - ) { - BytesCodable(wrappedValue: controller) - } + let localKeyFactory = LocalStorageKeyFactory() - let nominationRequest = MapSubscriptionRequest( - storagePath: .nominators, - localKey: Multistaking.RelaychainStateChange.Key.nomination.rawValue - ) { - BytesCodable(wrappedValue: stash) - } + let ledgerLocalKey = try localKeyFactory.createFromStoragePath( + .stakingLedger, + accountId: controller, + chainId: chainAsset.chain.chainId + ) - let validatorRequest = MapSubscriptionRequest( - storagePath: .validatorPrefs, - localKey: Multistaking.RelaychainStateChange.Key.validatorPrefs.rawValue - ) { - BytesCodable(wrappedValue: stash) - } + let ledgerRequest = BatchStorageSubscriptionRequest( + innerRequest: MapSubscriptionRequest( + storagePath: .stakingLedger, + localKey: ledgerLocalKey + ) { + BytesCodable(wrappedValue: controller) + }, + mappingKey: Multistaking.RelaychainStateChange.Key.ledger.rawValue + ) - let eraRequest = UnkeyedSubscriptionRequest( - storagePath: .activeEra, - localKey: Multistaking.RelaychainStateChange.Key.era.rawValue - ) + let nominationLocalKey = try localKeyFactory.createFromStoragePath( + .nominators, + accountId: stash, + chainId: chainAsset.chain.chainId + ) - stateSubscription = CallbackBatchStorageSubscription( - requests: [ledgerRequest, nominationRequest, validatorRequest, eraRequest], - connection: connection, - runtimeService: runtimeService, - repository: nil, - operationQueue: operationQueue, - callbackQueue: workingQueue - ) { [weak self] result in - self?.mutex.lock() + let nominationRequest = BatchStorageSubscriptionRequest( + innerRequest: MapSubscriptionRequest( + storagePath: .nominators, + localKey: nominationLocalKey + ) { + BytesCodable(wrappedValue: stash) + }, + mappingKey: Multistaking.RelaychainStateChange.Key.nomination.rawValue + ) - self?.handleStateSubscription(result: result) + let validatorLocalKey = try localKeyFactory.createFromStoragePath( + .validatorPrefs, + accountId: stash, + chainId: chainAsset.chain.chainId + ) - self?.mutex.unlock() - } + let validatorRequest = BatchStorageSubscriptionRequest( + innerRequest: MapSubscriptionRequest( + storagePath: .validatorPrefs, + localKey: validatorLocalKey + ) { + BytesCodable(wrappedValue: stash) + }, + mappingKey: Multistaking.RelaychainStateChange.Key.validatorPrefs.rawValue + ) - stateSubscription?.subscribe() + let eraRequest = BatchStorageSubscriptionRequest( + innerRequest: UnkeyedSubscriptionRequest( + storagePath: .activeEra, + localKey: "" + ), + mappingKey: Multistaking.RelaychainStateChange.Key.era.rawValue + ) + + stateSubscription = CallbackBatchStorageSubscription( + requests: [ledgerRequest, nominationRequest, validatorRequest, eraRequest], + connection: connection, + runtimeService: runtimeService, + repository: cacheRepository, + operationQueue: operationQueue, + callbackQueue: workingQueue + ) { [weak self] result in + self?.mutex.lock() + + self?.handleStateSubscription(result: result) + + self?.mutex.unlock() + } + + stateSubscription?.subscribe() + } catch { + logger?.error("Local key failed: \(error)") + completeImmediate(error) + } } private func handleStateSubscription(result: Result) { @@ -211,16 +271,17 @@ final class RelaychainMultistakingUpdateService: ObservableSyncService { } } - private func saveStashChange(_ stashAccountId: AccountId) { + private func saveResolvedAccounts(_ stashAccountId: AccountId) { let stakingOption = Multistaking.Option( chainAssetId: chainAsset.chainAssetId, - type: .relaychain + type: stakingType ) let resolvedAccount = Multistaking.ResolvedAccount( stakingOption: stakingOption, walletAccountId: accountId, - resolvedAccountId: stashAccountId + resolvedAccountId: stashAccountId, + rewardsAccountId: stashAccountId ) let saveOperation = accountRepository.saveOperation({ @@ -272,4 +333,32 @@ final class RelaychainMultistakingUpdateService: ObservableSyncService { operationQueue.addOperation(saveOperation) } + + private func saveStashItem(stash: AccountId?, controller: AccountId?, chain: ChainModel) { + let saveOperation = stashItemRepository.replaceOperation { + if let stash = stash, let controller = controller { + let stashItem = StashItem( + stash: try stash.toAddress(using: chain.chainFormat), + controller: try controller.toAddress(using: chain.chainFormat), + chainId: chain.chainId + ) + + return [stashItem] + } else { + return [] + } + } + + saveOperation.completionBlock = { [weak self] in + self?.workingQueue.async { + do { + _ = try saveOperation.extractNoCancellableResultData() + } catch { + self?.logger?.error("Can't save stash item") + } + } + } + + operationQueue.addOperation(saveOperation) + } } diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsAccountUpdatingService.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsAccountUpdatingService.swift new file mode 100644 index 0000000000..cef4a040e1 --- /dev/null +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsAccountUpdatingService.swift @@ -0,0 +1,202 @@ +import Foundation +import RobinHood +import SubstrateSdk + +final class NominationPoolsAccountUpdatingService: BaseSyncService, NPoolsLocalStorageSubscriber, + RuntimeConstantFetching { + let accountId: AccountId + let chainAsset: ChainAsset + let connection: JSONRPCEngine + let runtimeService: RuntimeProviderProtocol + let cacheRepository: AnyDataProviderRepository + let remoteSubscriptionService: NominationPoolsPoolSubscriptionServiceProtocol + let npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol + let operationQueue: OperationQueue + + private var poolMemberProvider: AnyDataProvider? + private var remoteSubscriptionId: UUID? + private var poolId: NominationPools.PoolId? + + init( + accountId: AccountId, + chainAsset: ChainAsset, + connection: JSONRPCEngine, + runtimeService: RuntimeProviderProtocol, + cacheRepository: AnyDataProviderRepository, + npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, + remoteSubscriptionService: NominationPoolsPoolSubscriptionServiceProtocol, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.accountId = accountId + self.chainAsset = chainAsset + self.connection = connection + self.runtimeService = runtimeService + self.cacheRepository = cacheRepository + self.npoolsLocalSubscriptionFactory = npoolsLocalSubscriptionFactory + self.remoteSubscriptionService = remoteSubscriptionService + self.operationQueue = operationQueue + + super.init(logger: logger) + } + + deinit { + clearSubscriptions() + } + + override func performSyncUp() { + clearLocalSubscription() + + poolMemberProvider = subscribePoolMember( + for: accountId, + chainId: chainAsset.chain.chainId, + callbackQueue: DispatchQueue.global(qos: .userInitiated) + ) + + if poolMemberProvider == nil { + completeImmediate(CommonError.databaseSubscription) + } + } + + override func stopSyncUp() { + clearSubscriptions() + } + + private func clearSubscriptions() { + clearLocalSubscription() + clearRemoteSubscription() + } + + private func clearLocalSubscription() { + poolMemberProvider = nil + } + + private func clearRemoteSubscription() { + if let remoteSubscriptionId = remoteSubscriptionId, let poolId = poolId { + remoteSubscriptionService.detachFromPoolData( + for: remoteSubscriptionId, + chainId: chainAsset.chain.chainId, + poolId: poolId, + queue: nil, + closure: nil + ) + + self.remoteSubscriptionId = nil + } + } + + private func subscribeRemote(for poolId: NominationPools.PoolId) { + remoteSubscriptionId = remoteSubscriptionService.attachToPoolData( + for: chainAsset.chain.chainId, + poolId: poolId, + queue: .global(qos: .userInitiated) + ) { [weak self] result in + self?.mutex.lock() + + defer { + self?.mutex.unlock() + } + + switch result { + case .success: + self?.logger?.debug("Subscribe for remote pool: \(poolId)") + self?.completeImmediate(nil) + case let .failure(error): + self?.logger?.error("Couldn't subscribe remote: \(error)") + self?.completeImmediate(error) + } + } + } +} + +extension NominationPoolsAccountUpdatingService: NPoolsLocalSubscriptionHandler { + func handlePoolMember( + result: Result, + accountId _: AccountId, + chainId _: ChainModel.Id + ) { + mutex.lock() + + defer { + mutex.unlock() + } + + markSyncingImmediate() + + clearRemoteSubscription() + + switch result { + case let .success(optPoolMember): + poolId = optPoolMember?.poolId + + if let poolMember = optPoolMember { + logger?.debug("Did receive pool member: \(poolMember)") + + subscribeRemote(for: poolMember.poolId) + } else { + logger?.warning("No pool staking found") + } + case let .failure(error): + logger?.error("Local subscription error: \(error)") + + completeImmediate(error) + } + } +} + +protocol NominationPoolsAccountUpdatingFactoryProtocol { + func create( + for accountId: AccountId, + chainAsset: ChainAsset + ) throws -> NominationPoolsAccountUpdatingService +} + +final class NominationPoolsAccountUpdatingFactory: NominationPoolsAccountUpdatingFactoryProtocol { + let chainRegistry: ChainRegistryProtocol + let repositoryFactory: SubstrateRepositoryFactoryProtocol + let remoteSubscriptionService: NominationPoolsPoolSubscriptionServiceProtocol + let npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol + let operationQueue: OperationQueue + let logger: LoggerProtocol + + init( + chainRegistry: ChainRegistryProtocol, + repositoryFactory: SubstrateRepositoryFactoryProtocol, + remoteSubscriptionService: NominationPoolsPoolSubscriptionServiceProtocol, + npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.chainRegistry = chainRegistry + self.repositoryFactory = repositoryFactory + self.remoteSubscriptionService = remoteSubscriptionService + self.npoolsLocalSubscriptionFactory = npoolsLocalSubscriptionFactory + self.operationQueue = operationQueue + self.logger = logger + } + + func create( + for accountId: AccountId, + chainAsset: ChainAsset + ) throws -> NominationPoolsAccountUpdatingService { + guard let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId) else { + throw ChainRegistryError.connectionUnavailable + } + + guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { + throw ChainRegistryError.runtimeMetadaUnavailable + } + + return .init( + accountId: accountId, + chainAsset: chainAsset, + connection: connection, + runtimeService: runtimeService, + cacheRepository: repositoryFactory.createChainStorageItemRepository(), + npoolsLocalSubscriptionFactory: npoolsLocalSubscriptionFactory, + remoteSubscriptionService: remoteSubscriptionService, + operationQueue: operationQueue, + logger: logger + ) + } +} diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsPoolSubscriptionService.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsPoolSubscriptionService.swift new file mode 100644 index 0000000000..ae33b34a1e --- /dev/null +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsPoolSubscriptionService.swift @@ -0,0 +1,102 @@ +import Foundation +import SubstrateSdk + +protocol NominationPoolsPoolSubscriptionServiceProtocol { + func attachToPoolData( + for chainId: ChainModel.Id, + poolId: NominationPools.PoolId, + queue: DispatchQueue?, + closure: RemoteSubscriptionClosure? + ) -> UUID? + + func detachFromPoolData( + for subscriptionId: UUID, + chainId: ChainModel.Id, + poolId: NominationPools.PoolId, + queue: DispatchQueue?, + closure: RemoteSubscriptionClosure? + ) +} + +final class NominationPoolsPoolSubscriptionService: RemoteSubscriptionService { + private static let poolIdStoragePaths: [StorageCodingPath] = [ + NominationPools.metadataPath, + NominationPools.rewardPoolsPath, + NominationPools.subPoolsPath + ] +} + +extension NominationPoolsPoolSubscriptionService: NominationPoolsPoolSubscriptionServiceProtocol { + func attachToPoolData( + for chainId: ChainModel.Id, + poolId: NominationPools.PoolId, + queue: DispatchQueue?, + closure: RemoteSubscriptionClosure? + ) -> UUID? { + do { + let localKeyFactory = LocalStorageKeyFactory() + + let poolIdRequests: [SubscriptionRequestProtocol] = try Self.poolIdStoragePaths + .map { path in + let localKey = try localKeyFactory.createFromStoragePath( + path, + encodableElement: poolId, + chainId: chainId + ) + + return MapSubscriptionRequest(storagePath: path, localKey: localKey) { + StringScaleMapper(value: poolId) + } + } + + let allPaths = Self.poolIdStoragePaths + + let cacheKey = try localKeyFactory.createRestorableCacheKey( + from: allPaths, + encodableElement: poolId, + chainId: chainId + ) + + let allRequests = poolIdRequests + + return attachToSubscription( + with: allRequests, + chainId: chainId, + cacheKey: cacheKey, + queue: queue, + closure: closure + ) + + } catch { + callbackClosureIfProvided(closure, queue: queue, result: .failure(error)) + return nil + } + } + + func detachFromPoolData( + for subscriptionId: UUID, + chainId: ChainModel.Id, + poolId: NominationPools.PoolId, + queue: DispatchQueue?, + closure: RemoteSubscriptionClosure? + ) { + do { + let allPaths = Self.poolIdStoragePaths + + let cacheKey = try LocalStorageKeyFactory().createRestorableCacheKey( + from: allPaths, + encodableElement: poolId, + chainId: chainId + ) + + detachFromSubscription( + cacheKey, + subscriptionId: subscriptionId, + queue: queue, + closure: closure + ) + } catch { + callbackClosureIfProvided(closure, queue: queue, result: .failure(error)) + } + } +} diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsRemoteSubscriptionService.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsRemoteSubscriptionService.swift new file mode 100644 index 0000000000..e04b44e4ad --- /dev/null +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsRemoteSubscriptionService.swift @@ -0,0 +1,89 @@ +import Foundation +import SubstrateSdk + +protocol NominationPoolsRemoteSubscriptionServiceProtocol { + func attachToGlobalData( + for chainId: ChainModel.Id, + queue: DispatchQueue?, + closure: RemoteSubscriptionClosure? + ) -> UUID? + + func detachFromGlobalData( + for subscriptionId: UUID, + chainId: ChainModel.Id, + queue: DispatchQueue?, + closure: RemoteSubscriptionClosure? + ) +} + +final class NominationPoolsRemoteSubscriptionService: RemoteSubscriptionService { + private static var globalDataStoragePaths: [StorageCodingPath] { + [ + NominationPools.lastPoolIdPath, + NominationPools.minJoinBondPath, + NominationPools.maxPoolMembers, + NominationPools.counterForPoolMembers, + NominationPools.maxMembersPerPool + ] + } +} + +extension NominationPoolsRemoteSubscriptionService: NominationPoolsRemoteSubscriptionServiceProtocol { + func attachToGlobalData( + for chainId: ChainModel.Id, + queue: DispatchQueue?, + closure: RemoteSubscriptionClosure? + ) -> UUID? { + do { + let localKeyFactory = LocalStorageKeyFactory() + + let globalPaths = Self.globalDataStoragePaths + let globalLocalKeys = try globalPaths.map { storagePath in + try localKeyFactory.createFromStoragePath( + storagePath, + chainId: chainId + ) + } + + let cacheKey = try localKeyFactory.createCacheKey(from: globalPaths, chainId: chainId) + + let requests = zip(globalPaths, globalLocalKeys).map { + UnkeyedSubscriptionRequest(storagePath: $0.0, localKey: $0.1) + } + + return attachToSubscription( + with: requests, + chainId: chainId, + cacheKey: cacheKey, + queue: queue, + closure: closure + ) + } catch { + callbackClosureIfProvided(closure, queue: queue, result: .failure(error)) + return nil + } + } + + func detachFromGlobalData( + for subscriptionId: UUID, + chainId: ChainModel.Id, + queue: DispatchQueue?, + closure: RemoteSubscriptionClosure? + ) { + do { + let cacheKey = try LocalStorageKeyFactory().createCacheKey( + from: Self.globalDataStoragePaths, + chainId: chainId + ) + + detachFromSubscription( + cacheKey, + subscriptionId: subscriptionId, + queue: queue, + closure: closure + ) + } catch { + callbackClosureIfProvided(closure, queue: queue, result: .failure(error)) + } + } +} diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/StakingAccountResolver.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/StakingAccountResolver.swift deleted file mode 100644 index abf129e84e..0000000000 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/StakingAccountResolver.swift +++ /dev/null @@ -1,366 +0,0 @@ -import Foundation -import RobinHood -import IrohaCrypto -import SubstrateSdk - -final class StakingAccountResolver: WebSocketSubscribing { - struct Subscription { - let controller: StorageChildSubscribing - let ledger: StorageChildSubscribing - let subscriptionId: UInt16 - } - - struct DecodedChanges { - let controller: Data? - let ledger: StakingLedger? - } - - let accountId: AccountId - let chainId: ChainModel.Id - let chainFormat: ChainFormat - let chainRegistry: ChainRegistryProtocol - let childSubscriptionFactory: ChildSubscriptionFactoryProtocol - let operationQueue: OperationQueue - let repository: AnyDataProviderRepository - let logger: LoggerProtocol? - - private let mutex = NSLock() - - private var subscription: Subscription? - - init( - accountId: AccountId, - chainId: ChainModel.Id, - chainFormat: ChainFormat, - chainRegistry: ChainRegistryProtocol, - childSubscriptionFactory: ChildSubscriptionFactoryProtocol, - operationQueue: OperationQueue, - repository: AnyDataProviderRepository, - logger: LoggerProtocol? = nil - ) { - self.accountId = accountId - self.chainId = chainId - self.chainFormat = chainFormat - self.chainRegistry = chainRegistry - self.childSubscriptionFactory = childSubscriptionFactory - self.repository = repository - self.operationQueue = operationQueue - self.logger = logger - - resolveKeysAndSubscribe() - } - - deinit { - unsubscribe() - } - - private func resolveKeysAndSubscribe() { - do { - guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { - throw ChainRegistryError.runtimeMetadaUnavailable - } - - let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() - - let storageKeyFactory = StorageKeyFactory() - - let controllerOperation = MapKeyEncodingOperation( - path: .controller, - storageKeyFactory: storageKeyFactory, - keyParams: [accountId] - ) - - let localKeyFactory = LocalStorageKeyFactory() - let controllerLocalKey = try localKeyFactory.createFromStoragePath( - .controller, - accountId: accountId, - chainId: chainId - ) - - let ledgerOperation = MapKeyEncodingOperation( - path: .stakingLedger, - storageKeyFactory: storageKeyFactory, - keyParams: [accountId] - ) - - let ledgerLocalKey = try localKeyFactory.createFromStoragePath( - .stakingLedger, - accountId: accountId, - chainId: chainId - ) - - [controllerOperation, ledgerOperation].forEach { operation in - operation.addDependency(codingFactoryOperation) - - operation.configurationBlock = { - do { - operation.codingFactory = try codingFactoryOperation.extractNoCancellableResultData() - } catch { - operation.result = .failure(error) - } - } - } - - let syncOperation = Operation() - syncOperation.addDependency(controllerOperation) - syncOperation.addDependency(ledgerOperation) - - syncOperation.completionBlock = { [weak self] in - do { - let controllerKey = try controllerOperation.extractNoCancellableResultData()[0] - let ledgerKey = try ledgerOperation.extractNoCancellableResultData()[0] - - self?.subscribe( - with: SubscriptionStorageKeys(remote: controllerKey, local: controllerLocalKey), - ledgerKeys: SubscriptionStorageKeys(remote: ledgerKey, local: ledgerLocalKey) - ) - } catch { - self?.logger?.error("Did receiver error: \(error)") - } - } - - let operations = [codingFactoryOperation, controllerOperation, ledgerOperation, syncOperation] - - operationQueue.addOperations(operations, waitUntilFinished: false) - - } catch { - logger?.error("Did receive error: \(error)") - } - } - - private func unsubscribe() { - mutex.lock() - - defer { - mutex.unlock() - } - - guard let subscription = subscription else { - return - } - - chainRegistry.getConnection(for: chainId)?.cancelForIdentifier(subscription.subscriptionId) - self.subscription = nil - } - - private func subscribe( - with controllerKeys: SubscriptionStorageKeys, - ledgerKeys: SubscriptionStorageKeys - ) { - mutex.lock() - - defer { - mutex.unlock() - } - - do { - guard let connection = chainRegistry.getConnection(for: chainId) else { - throw ChainRegistryError.connectionUnavailable - } - - let controllerSubscription = childSubscriptionFactory.createEmptyHandlingSubscription( - keys: controllerKeys - ) - let ledgerSubscription = childSubscriptionFactory.createEmptyHandlingSubscription( - keys: ledgerKeys - ) - - let storageParams = [ - controllerKeys.remote.toHex(includePrefix: true), - ledgerKeys.remote.toHex(includePrefix: true) - ] - - let updateClosure: (StorageSubscriptionUpdate) -> Void = { [weak self] update in - self?.handleUpdate(update.params.result) - } - - let failureClosure: (Error, Bool) -> Void = { [weak self] error, unsubscribed in - self?.logger?.error("Did receive subscription error: \(error) \(unsubscribed)") - } - - let subscriptionId = try connection.subscribe( - RPCMethod.storageSubscribe, - params: [storageParams], - updateClosure: updateClosure, - failureClosure: failureClosure - ) - - subscription = Subscription( - controller: controllerSubscription, - ledger: ledgerSubscription, - subscriptionId: subscriptionId - ) - } catch { - logger?.error("Did receive error: \(error)") - } - } - - private func handleUpdate(_ update: StorageUpdate) { - mutex.lock() - - defer { - mutex.unlock() - } - - guard let subscription = subscription else { - logger?.warning("Staking update received but subscription is missing") - return - } - - let updateData = StorageUpdateData(update: update) - updateChild(subscription: subscription.controller, for: updateData) - updateChild(subscription: subscription.ledger, for: updateData) - - let decodingWrapper = createDecodingWrapper(from: updateData, subscription: subscription) - let processingOperation = createProcessingOperation( - dependingOn: decodingWrapper.targetOperation, - initAccountId: accountId, - chainFormat: chainFormat - ) - processingOperation.addDependency(decodingWrapper.targetOperation) - let saveWrapper = createSaveWrapper(dependingOn: processingOperation) - saveWrapper.allOperations.forEach { $0.addDependency(processingOperation) } - - let operations = decodingWrapper.allOperations + [processingOperation] + saveWrapper.allOperations - - operationQueue.addOperations(operations, waitUntilFinished: false) - } - - private func updateChild(subscription: StorageChildSubscribing, for update: StorageUpdateData) { - if let change = update.changes.first(where: { $0.key == subscription.remoteStorageKey }) { - subscription.processUpdate(change.value, blockHash: update.blockHash) - } - } -} - -extension StakingAccountResolver { - private func createDecodingWrapper( - from updateData: StorageUpdateData, - subscription: Subscription - ) -> CompoundOperationWrapper { - guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { - return CompoundOperationWrapper.createWithError(ChainRegistryError.runtimeMetadaUnavailable) - } - - let codingFactory = runtimeService.fetchCoderFactoryOperation() - - let controllerDecoding: BaseOperation? = createDecodingOperation( - for: subscription.controller, - path: .controller, - updateData: updateData, - coderOperation: codingFactory - ) - controllerDecoding?.addDependency(codingFactory) - - let ledgerDecoding: BaseOperation? = - createDecodingOperation( - for: subscription.ledger, - path: .stakingLedger, - updateData: updateData, - coderOperation: codingFactory - ) - ledgerDecoding?.addDependency(codingFactory) - - let mapOperation = ClosureOperation { - let controller = try controllerDecoding?.extractNoCancellableResultData() - let ledger = try ledgerDecoding?.extractNoCancellableResultData() - - return DecodedChanges(controller: controller, ledger: ledger) - } - - var dependencies: [Operation] = [codingFactory] - - if let operation = controllerDecoding { - dependencies.append(operation) - } - - if let operation = ledgerDecoding { - dependencies.append(operation) - } - - dependencies.forEach { mapOperation.addDependency($0) } - - return CompoundOperationWrapper(targetOperation: mapOperation, dependencies: dependencies) - } - - private func createDecodingOperation( - for subscription: StorageChildSubscribing, - path: StorageCodingPath, - updateData: StorageUpdateData, - coderOperation: BaseOperation - ) -> BaseOperation? where T: Decodable { - if let ledgerValue = updateData.changes - .first(where: { $0.key == subscription.remoteStorageKey })?.value { - let operation = StorageDecodingOperation(path: path, data: ledgerValue) - operation.configurationBlock = { - do { - operation.codingFactory = try coderOperation.extractNoCancellableResultData() - } catch { - operation.result = .failure(error) - } - } - - return operation - } else { - return nil - } - } - - private func createProcessingOperation( - dependingOn decodinigOperation: BaseOperation, - initAccountId: AccountId, - chainFormat: ChainFormat - ) -> BaseOperation { - ClosureOperation { - let initAddress = try initAccountId.toAddress(using: chainFormat) - let changes = try decodinigOperation.extractNoCancellableResultData() - if let controller = changes.controller { - let controllerAddress = try controller.toAddress(using: chainFormat) - return StashItem(stash: initAddress, controller: controllerAddress) - } - - if let stash = changes.ledger?.stash { - let stashAddress = try stash.toAddress(using: chainFormat) - return StashItem(stash: stashAddress, controller: initAddress) - } - - return nil - } - } - - private func createSaveWrapper( - dependingOn operation: BaseOperation - ) -> CompoundOperationWrapper { - let currentItemsOperation = repository.fetchAllOperation(with: RepositoryFetchOptions()) - - let saveOperation = repository.saveOperation({ - let currentItem = try currentItemsOperation.extractNoCancellableResultData() - .first - - if let newStashItem = try operation.extractNoCancellableResultData(), - currentItem != newStashItem { - return [newStashItem] - } else { - return [] - } - }, { - let newStashItem = try operation.extractNoCancellableResultData() - - guard let currentId = try currentItemsOperation.extractNoCancellableResultData() - .first?.identifier - else { - return [] - } - - if newStashItem == nil || newStashItem?.stash != currentId { - return [currentId] - } else { - return [] - } - }) - - saveOperation.addDependency(currentItemsOperation) - - return CompoundOperationWrapper(targetOperation: saveOperation, dependencies: [currentItemsOperation]) - } -} diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/StakingAccountSubscription.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/StakingAccountSubscription.swift index de30371ce3..abce7705c5 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/StakingAccountSubscription.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/StakingAccountSubscription.swift @@ -72,14 +72,7 @@ final class StakingAccountSubscription: WebSocketSubscribing { private func subscribeLocal() { let changesClosure: ([DataProviderChange]) -> Void = { [weak self] changes in - let stashItem: StashItem? = changes.reduce(nil) { _, item in - switch item { - case let .insert(newItem), let .update(newItem): - return newItem - case .delete: - return nil - } - } + let stashItem: StashItem? = changes.reduceToLastChange() self?.unsubscribeRemote() @@ -119,19 +112,15 @@ final class StakingAccountSubscription: WebSocketSubscribing { let stashId = try stashItem.stash.toAccountId(using: chainFormat) if stashId != accountId { - requests.append(.init(storagePath: .controller, accountId: stashId)) requests.append(.init(storagePath: .account, accountId: stashId)) } let controllerId = try stashItem.controller.toAccountId(using: chainFormat) if controllerId != accountId { - requests.append(.init(storagePath: .stakingLedger, accountId: controllerId)) requests.append(.init(storagePath: .account, accountId: controllerId)) } - requests.append(.init(storagePath: .nominators, accountId: stashId)) - requests.append(.init(storagePath: .validatorPrefs, accountId: stashId)) requests.append(.init(storagePath: .payee, accountId: stashId)) BagList.possibleModuleNames.forEach { moduleName in diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/StakingAccountUpdatingService.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/StakingAccountUpdatingService.swift index fa98a3499e..b706d6dfaf 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/StakingAccountUpdatingService.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/StakingAccountUpdatingService.swift @@ -12,7 +12,6 @@ protocol StakingAccountUpdatingServiceProtocol { } class StakingAccountUpdatingService: StakingAccountUpdatingServiceProtocol { - private var accountResolver: StakingAccountResolver? private var accountSubscription: StakingAccountSubscription? let chainRegistry: ChainRegistryProtocol @@ -43,21 +42,8 @@ class StakingAccountUpdatingService: StakingAccountUpdatingServiceProtocol { chainId: ChainModel.Id, chainFormat: ChainFormat ) throws { - let stashItemRepository = substrateRepositoryFactory.createStashItemRepository() - let address = try accountId.toAddress(using: chainFormat) - let stashItemProvider = substrateDataProviderFactory.createStashItemProvider(for: address) - - accountResolver = StakingAccountResolver( - accountId: accountId, - chainId: chainId, - chainFormat: chainFormat, - chainRegistry: chainRegistry, - childSubscriptionFactory: childSubscriptionFactory, - operationQueue: operationQueue, - repository: stashItemRepository, - logger: logger - ) + let stashItemProvider = substrateDataProviderFactory.createStashItemProvider(for: address, chainId: chainId) accountSubscription = StakingAccountSubscription( accountId: accountId, @@ -72,7 +58,6 @@ class StakingAccountUpdatingService: StakingAccountUpdatingServiceProtocol { } func clearSubscription() { - accountResolver = nil accountSubscription = nil } } diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/StakingRemoteSubscriptionService.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/StakingRemoteSubscriptionService.swift index 51ac466c27..43c4aa312c 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/StakingRemoteSubscriptionService.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/StakingRemoteSubscriptionService.swift @@ -47,23 +47,6 @@ final class StakingRemoteSubscriptionService: RemoteSubscriptionService, return bagListSizeRequests } - private static func createDataParamsCacheKey( - for chainId: ChainModel.Id, - paths: [StorageCodingPath] - ) throws -> String { - let storageKeyFactory = StorageKeyFactory() - let cacheKeyData = try paths.reduce(Data()) { result, storagePath in - let storageKeyData = try storageKeyFactory.createStorageKey( - moduleName: storagePath.moduleName, - storageName: storagePath.itemName - ) - - return result + storageKeyData - } - - return try LocalStorageKeyFactory().createKey(from: cacheKeyData, chainId: chainId) - } - func attachToGlobalData( for chainId: ChainModel.Id, queue: DispatchQueue?, @@ -83,7 +66,7 @@ final class StakingRemoteSubscriptionService: RemoteSubscriptionService, let bagListRequests = try Self.createBagsListRequests(for: localKeyFactory, chainId: chainId) let bagListPaths = bagListRequests.map(\.storagePath) - let cacheKey = try Self.createDataParamsCacheKey(for: chainId, paths: globalPaths + bagListPaths) + let cacheKey = try localKeyFactory.createCacheKey(from: globalPaths + bagListPaths, chainId: chainId) let globalRequests = zip(globalPaths, globalLocalKeys).map { UnkeyedSubscriptionRequest(storagePath: $0.0, localKey: $0.1) @@ -114,9 +97,9 @@ final class StakingRemoteSubscriptionService: RemoteSubscriptionService, let bagListRequests = try Self.createBagsListRequests(for: LocalStorageKeyFactory(), chainId: chainId) let bagListPaths = bagListRequests.map(\.storagePath) - let cacheKey = try Self.createDataParamsCacheKey( - for: chainId, - paths: Self.globalDataStoragePaths + bagListPaths + let cacheKey = try LocalStorageKeyFactory().createCacheKey( + from: Self.globalDataStoragePaths + bagListPaths, + chainId: chainId ) detachFromSubscription( diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/BatchStorageSubscriptionResult.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/BatchStorageSubscriptionResult.swift index b294064b9e..a8f1dc2331 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/BatchStorageSubscriptionResult.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/BatchStorageSubscriptionResult.swift @@ -1,8 +1,13 @@ import Foundation import SubstrateSdk +struct BatchStorageSubscriptionRequest { + let innerRequest: SubscriptionRequestProtocol + let mappingKey: String? +} + struct BatchStorageSubscriptionResultValue { - let localKey: String + let mappingKey: String? let value: JSON } diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/CallbackBatchStorageSubscription.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/CallbackBatchStorageSubscription.swift index 7987255dc3..4c194a23b4 100644 --- a/novawallet/Common/Services/WebSocketService/StorageSubscription/CallbackBatchStorageSubscription.swift +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/CallbackBatchStorageSubscription.swift @@ -3,7 +3,7 @@ import SubstrateSdk import RobinHood final class CallbackBatchStorageSubscription { - let requests: [SubscriptionRequestProtocol] + let requests: [BatchStorageSubscriptionRequest] let runtimeService: RuntimeCodingServiceProtocol let connection: JSONRPCEngine let operationQueue: OperationQueue @@ -20,7 +20,7 @@ final class CallbackBatchStorageSubscription private var mutex = NSLock() init( - requests: [SubscriptionRequestProtocol], + requests: [BatchStorageSubscriptionRequest], connection: JSONRPCEngine, runtimeService: RuntimeCodingServiceProtocol, repository: AnyDataProviderRepository?, @@ -46,7 +46,7 @@ final class CallbackBatchStorageSubscription let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() let keyEncondingWrappers = requests.map { request in - request.createKeyEncodingWrapper( + request.innerRequest.createKeyEncodingWrapper( using: StorageKeyFactory(), codingFactoryClosure: { try codingFactoryOperation.extractNoCancellableResultData() } ) @@ -145,7 +145,7 @@ final class CallbackBatchStorageSubscription private func findRequests( for changes: [StorageUpdateData.StorageUpdateChangeData], keys: [Data] - ) -> [SubscriptionRequestProtocol] { + ) -> [BatchStorageSubscriptionRequest] { let receivedKeys = Set(changes.map(\.key)) return zip(requests, keys).compactMap { receivedKeys.contains($0.1) ? $0.0 : nil } } @@ -155,13 +155,13 @@ final class CallbackBatchStorageSubscription keys: [Data], blockHash: Data? ) { - saveIfNeeded(changes: changes) + let requests = findRequests(for: changes, keys: keys) + saveIfNeeded(changes: changes, requests: requests) let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() - let requests = findRequests(for: changes, keys: keys) let decodingOperations: [BaseOperation] = zip(changes, requests).map { change, request in if let data = change.value { - let decodingOperation = StorageJSONDecodingOperation(path: request.storagePath, data: data) + let decodingOperation = StorageJSONDecodingOperation(path: request.innerRequest.storagePath, data: data) decodingOperation.configurationBlock = { do { decodingOperation.codingFactory = try codingFactoryOperation.extractNoCancellableResultData() @@ -181,7 +181,7 @@ final class CallbackBatchStorageSubscription let blockHashJson = blockHash.map { JSON.stringValue($0.toHex()) } ?? JSON.null let context = try codingFactoryOperation.extractNoCancellableResultData().createRuntimeJsonContext() let values = zip(jsonList, requests).map { - BatchStorageSubscriptionResultValue(localKey: $0.1.localKey, value: $0.0) + BatchStorageSubscriptionResultValue(mappingKey: $0.1.mappingKey, value: $0.0) } return try T( values: values, @@ -215,26 +215,28 @@ final class CallbackBatchStorageSubscription operationQueue.addOperations(operations, waitUntilFinished: false) } - private func saveIfNeeded(changes: [StorageUpdateData.StorageUpdateChangeData]) { + private func saveIfNeeded( + changes: [StorageUpdateData.StorageUpdateChangeData], + requests: [BatchStorageSubscriptionRequest] + ) { guard let repository = repository else { return } - let localKeys = requests.map(\.localKey) - let pairs = zip(changes, localKeys) + let pairs = zip(changes, requests).filter { !$0.1.innerRequest.localKey.isEmpty } let operation = repository.saveOperation({ - pairs.compactMap { change, localKey in + pairs.compactMap { change, request in guard let data = change.value else { return nil } - return ChainStorageItem(identifier: localKey, data: data) + return ChainStorageItem(identifier: request.innerRequest.localKey, data: data) } }, { - pairs.compactMap { change, localKey in + pairs.compactMap { change, request in if change.value == nil { - return localKey + return request.innerRequest.localKey } else { return nil } diff --git a/novawallet/Common/Storage/EntityToModel/CrowdloanContributionDataMapper.swift b/novawallet/Common/Storage/EntityToModel/CrowdloanContributionDataMapper.swift index b3737bf704..ff08b07ed8 100644 --- a/novawallet/Common/Storage/EntityToModel/CrowdloanContributionDataMapper.swift +++ b/novawallet/Common/Storage/EntityToModel/CrowdloanContributionDataMapper.swift @@ -4,10 +4,10 @@ import CoreData import BigInt final class CrowdloanContributionDataMapper { - var entityIdentifierFieldName: String { #keyPath(CDCrowdloanContribution.identifier) } + var entityIdentifierFieldName: String { #keyPath(CDExternalBalance.identifier) } typealias DataProviderModel = CrowdloanContributionData - typealias CoreDataEntity = CDCrowdloanContribution + typealias CoreDataEntity = CDExternalBalance } extension CrowdloanContributionDataMapper: CoreDataMapperProtocol { @@ -17,9 +17,11 @@ extension CrowdloanContributionDataMapper: CoreDataMapperProtocol { using _: NSManagedObjectContext ) throws { entity.identifier = model.identifier - entity.chainId = model.chainId - entity.paraId = Int32(model.paraId) - entity.source = model.source + entity.chainId = model.chainAssetId.chainId + entity.assetId = Int32(bitPattern: model.chainAssetId.assetId) + entity.type = ExternalAssetBalance.BalanceType.crowdloan.rawValue + entity.subtype = model.source + entity.param = String(model.paraId) entity.chainAccountId = model.accountId.toHex() entity.amount = String(model.amount) } @@ -27,13 +29,13 @@ extension CrowdloanContributionDataMapper: CoreDataMapperProtocol { 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) + let paraId = entity.param.flatMap { UInt32($0) } return .init( accountId: accountId, - chainId: entity.chainId!, - paraId: paraId, - source: entity.source, + chainAssetId: ChainAssetId(chainId: entity.chainId!, assetId: .init(bitPattern: entity.assetId)), + paraId: paraId ?? 0, + source: entity.subtype, amount: amount ) } diff --git a/novawallet/Common/Storage/EntityToModel/ExternalAssetBalanceMapper.swift b/novawallet/Common/Storage/EntityToModel/ExternalAssetBalanceMapper.swift new file mode 100644 index 0000000000..bee639b8a6 --- /dev/null +++ b/novawallet/Common/Storage/EntityToModel/ExternalAssetBalanceMapper.swift @@ -0,0 +1,36 @@ +import Foundation +import RobinHood +import CoreData +import BigInt + +final class ExternalAssetBalanceMapper { + var entityIdentifierFieldName: String { #keyPath(CDExternalBalance.identifier) } + + typealias DataProviderModel = ExternalAssetBalance + typealias CoreDataEntity = CDExternalBalance +} + +extension ExternalAssetBalanceMapper: CoreDataMapperProtocol { + func populate( + entity _: CoreDataEntity, + from _: DataProviderModel, + using _: NSManagedObjectContext + ) throws { + fatalError("Use source specific mapper to save data") + } + + func transform(entity: CoreDataEntity) throws -> DataProviderModel { + let accountId = try Data(hexString: entity.chainAccountId!) + let amount = entity.amount.map { BigUInt($0) ?? 0 } ?? 0 + + return .init( + identifier: entity.identifier!, + chainAssetId: .init(chainId: entity.chainId!, assetId: .init(bitPattern: entity.assetId)), + accountId: accountId, + amount: amount, + type: .init(rawType: entity.type!), + subtype: entity.subtype, + param: entity.param + ) + } +} diff --git a/novawallet/Common/Storage/EntityToModel/PooledAssetBalanceMapper.swift b/novawallet/Common/Storage/EntityToModel/PooledAssetBalanceMapper.swift new file mode 100644 index 0000000000..fc20c90c07 --- /dev/null +++ b/novawallet/Common/Storage/EntityToModel/PooledAssetBalanceMapper.swift @@ -0,0 +1,41 @@ +import Foundation +import RobinHood +import CoreData +import BigInt + +final class PooledAssetBalanceMapper { + var entityIdentifierFieldName: String { #keyPath(CDExternalBalance.identifier) } + + typealias DataProviderModel = PooledAssetBalance + typealias CoreDataEntity = CDExternalBalance +} + +extension PooledAssetBalanceMapper: CoreDataMapperProtocol { + func populate( + entity: CoreDataEntity, + from model: DataProviderModel, + using _: NSManagedObjectContext + ) throws { + entity.identifier = model.identifier + entity.chainId = model.chainAssetId.chainId + entity.assetId = Int32(bitPattern: model.chainAssetId.assetId) + entity.type = ExternalAssetBalance.BalanceType.nominationPools.rawValue + entity.subtype = nil + entity.param = String(model.poolId) + 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 poolId = entity.param.flatMap { UInt32($0) } + + return .init( + chainAssetId: .init(chainId: entity.chainId!, assetId: .init(bitPattern: entity.assetId)), + accountId: accountId, + amount: amount, + poolId: poolId ?? 0 + ) + } +} diff --git a/novawallet/Common/Storage/EntityToModel/StakingDashboardNominationPoolMapper.swift b/novawallet/Common/Storage/EntityToModel/StakingDashboardNominationPoolMapper.swift new file mode 100644 index 0000000000..e4426ff901 --- /dev/null +++ b/novawallet/Common/Storage/EntityToModel/StakingDashboardNominationPoolMapper.swift @@ -0,0 +1,46 @@ +import Foundation +import BigInt +import RobinHood +import CoreData + +extension Multistaking.DashboardItemNominationPoolPart: Identifiable { + var identifier: String { stakingOption.stringValue } +} + +final class StakingDashboardNominationPoolMapper { + var entityIdentifierFieldName: String { #keyPath(CDStakingDashboardItem.identifier) } + + typealias DataProviderModel = Multistaking.DashboardItemNominationPoolPart + typealias CoreDataEntity = CDStakingDashboardItem +} + +extension StakingDashboardNominationPoolMapper: CoreDataMapperProtocol { + func populate( + entity: CoreDataEntity, + from model: DataProviderModel, + using _: NSManagedObjectContext + ) throws { + entity.identifier = model.identifier + entity.walletId = model.stakingOption.walletId + + let chainAssetId = model.stakingOption.option.chainAssetId + entity.chainId = chainAssetId.chainId + entity.assetId = Int32(bitPattern: chainAssetId.assetId) + + entity.stakingType = model.stakingOption.option.type.rawValue + + if let state = model.state { + entity.stake = state.poolMemberStake.map { String($0) } + + entity.onchainState = Multistaking.DashboardItemOnchainState.from(nominationPoolState: state)?.rawValue + } else { + entity.stake = nil + entity.onchainState = nil + } + } + + func transform(entity _: CoreDataEntity) throws -> DataProviderModel { + // we only can write partial state but not read + fatalError("Unsupported method") + } +} diff --git a/novawallet/Common/Storage/EntityToModel/StakingResolvedAccountMapper.swift b/novawallet/Common/Storage/EntityToModel/StakingResolvedAccountMapper.swift index 8eb3690be1..5fe61703d7 100644 --- a/novawallet/Common/Storage/EntityToModel/StakingResolvedAccountMapper.swift +++ b/novawallet/Common/Storage/EntityToModel/StakingResolvedAccountMapper.swift @@ -4,6 +4,10 @@ import RobinHood extension Multistaking.ResolvedAccount: Identifiable { var identifier: String { + Self.createIdentifier(from: walletAccountId, stakingOption: stakingOption) + } + + static func createIdentifier(from walletAccountId: AccountId, stakingOption: Multistaking.Option) -> String { walletAccountId.toHex() + "-" + stakingOption.stringValue } } @@ -27,6 +31,7 @@ extension StakingResolvedAccountMapper: CoreDataMapperProtocol { entity.stakingType = model.stakingOption.type.rawValue entity.walletAccountId = model.walletAccountId.toHex() entity.resolvedAccountId = model.resolvedAccountId.toHex() + entity.rewardsAccountId = model.rewardsAccountId?.toHex() } func transform(entity: CoreDataEntity) throws -> DataProviderModel { @@ -43,11 +48,13 @@ extension StakingResolvedAccountMapper: CoreDataMapperProtocol { let walletAccountId = try Data(hexString: entity.walletAccountId!) let resolvedAccountId = try Data(hexString: entity.resolvedAccountId!) + let rewardsAccountId = try entity.rewardsAccountId.map { try Data(hexString: $0) } return .init( stakingOption: stakingOption, walletAccountId: walletAccountId, - resolvedAccountId: resolvedAccountId + resolvedAccountId: resolvedAccountId, + rewardsAccountId: rewardsAccountId ) } } diff --git a/novawallet/Common/Storage/EntityToModel/StakingRewardsFilterMapper.swift b/novawallet/Common/Storage/EntityToModel/StakingRewardsFilterMapper.swift index 46a75347dc..77772b53ae 100644 --- a/novawallet/Common/Storage/EntityToModel/StakingRewardsFilterMapper.swift +++ b/novawallet/Common/Storage/EntityToModel/StakingRewardsFilterMapper.swift @@ -28,7 +28,6 @@ extension StakingRewardsFilterMapper: CoreDataMapperProtocol { func transform(entity: CoreDataEntity) throws -> DataProviderModel { let chainAccountId = try Data(hexString: entity.chainAccountId!) - let chainId = entity.chainId let chainAssetId = ChainAssetId( chainId: entity.chainId!, assetId: UInt32(bitPattern: entity.assetId) diff --git a/novawallet/Common/Storage/EntityToModel/StashItemMapper.swift b/novawallet/Common/Storage/EntityToModel/StashItemMapper.swift new file mode 100644 index 0000000000..3ad4275852 --- /dev/null +++ b/novawallet/Common/Storage/EntityToModel/StashItemMapper.swift @@ -0,0 +1,27 @@ +import Foundation +import RobinHood +import CoreData + +final class StashItemMapper { + var entityIdentifierFieldName: String { #keyPath(CDStashItem.identifier) } + + typealias DataProviderModel = StashItem + typealias CoreDataEntity = CDStashItem +} + +extension StashItemMapper: CoreDataMapperProtocol { + func populate( + entity: CoreDataEntity, + from model: DataProviderModel, + using _: NSManagedObjectContext + ) throws { + entity.identifier = StashItem.createIdentifier(from: model.stash, chainId: model.chainId) + entity.stash = model.stash + entity.controller = model.controller + entity.chainId = model.chainId + } + + func transform(entity: CoreDataEntity) throws -> DataProviderModel { + .init(stash: entity.stash!, controller: entity.controller!, chainId: entity.chainId ?? "") + } +} diff --git a/novawallet/Common/Storage/LocalStorageKeyFactory.swift b/novawallet/Common/Storage/LocalStorageKeyFactory.swift index f11258a8ca..dc65287a23 100644 --- a/novawallet/Common/Storage/LocalStorageKeyFactory.swift +++ b/novawallet/Common/Storage/LocalStorageKeyFactory.swift @@ -115,3 +115,39 @@ final class LocalStorageKeyFactory: LocalStorageKeyFactoryProtocol { return Data(fullKey.suffix(fullKey.count - chainIdData.count)) } } + +extension LocalStorageKeyFactoryProtocol { + func createCacheKey(from paths: [StorageCodingPath], chainId: ChainModel.Id) throws -> String { + let storageKeyFactory = StorageKeyFactory() + let cacheKeyData = try paths.reduce(Data()) { result, storagePath in + let storageKeyData = try storageKeyFactory.createStorageKey( + moduleName: storagePath.moduleName, + storageName: storagePath.itemName + ) + + return result + storageKeyData + } + + return try createKey(from: cacheKeyData, chainId: chainId) + } + + func createRestorableCacheKey( + from paths: [StorageCodingPath], + encodableElement: E, + chainId: ChainModel.Id + ) throws -> String { + let storageKeyFactory = StorageKeyFactory() + let cacheKeyData = try paths.reduce(Data()) { result, storagePath in + let storageKeyData = try storageKeyFactory.createStorageKey( + moduleName: storagePath.moduleName, + storageName: storagePath.itemName + ) + + return result + storageKeyData + } + + let encodedElement = try encodableElement.scaleEncoded() + + return try createRestorableKey(from: cacheKeyData + encodedElement, chainId: chainId) + } +} diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion index 330212f190..46d2cdef68 100644 --- a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - SubstrateDataModel16.xcdatamodel + SubstrateDataModel19.xcdatamodel diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel17.xcdatamodel/contents b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel17.xcdatamodel/contents new file mode 100644 index 0000000000..b18cffc85a --- /dev/null +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel17.xcdatamodel/contents @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel18.xcdatamodel/contents b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel18.xcdatamodel/contents new file mode 100644 index 0000000000..27aa8133d2 --- /dev/null +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel18.xcdatamodel/contents @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel19.xcdatamodel/contents b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel19.xcdatamodel/contents new file mode 100644 index 0000000000..e1e07dbb86 --- /dev/null +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel19.xcdatamodel/contents @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/novawallet/Common/Storage/SubstrateDataStorageFacade.swift b/novawallet/Common/Storage/SubstrateDataStorageFacade.swift index 8ae88231ef..6b6e13d478 100644 --- a/novawallet/Common/Storage/SubstrateDataStorageFacade.swift +++ b/novawallet/Common/Storage/SubstrateDataStorageFacade.swift @@ -4,7 +4,7 @@ import CoreData enum SubstrateStorageParams { static let databaseName = "SubstrateDataModel.sqlite" static let modelDirectory: String = "SubstrateDataModel.momd" - static let modelVersion: SubstrateStorageVersion = .version16 + static let modelVersion: SubstrateStorageVersion = .version19 static let storageDirectoryURL: URL = { let baseURL = FileManager.default.urls( diff --git a/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolClaimRewards.swift b/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolClaimRewards.swift new file mode 100644 index 0000000000..fcd44920b9 --- /dev/null +++ b/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolClaimRewards.swift @@ -0,0 +1,10 @@ +import Foundation +import SubstrateSdk + +extension NominationPools { + struct ClaimRewardsCall: Codable { + func runtimeCall() -> RuntimeCall { + RuntimeCall(moduleName: NominationPools.module, callName: "claim_payout", args: self) + } + } +} diff --git a/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolUnstake.swift b/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolUnstake.swift new file mode 100644 index 0000000000..ecd4686efc --- /dev/null +++ b/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolUnstake.swift @@ -0,0 +1,19 @@ +import Foundation +import SubstrateSdk +import BigInt + +extension NominationPools { + struct UnbondCall: Codable { + enum CodingKeys: String, CodingKey { + case memberAccount = "member_account" + case unbondingPoints = "unbonding_points" + } + + let memberAccount: MultiAddress + @StringCodable var unbondingPoints: BigUInt + + func runtimeCall() -> RuntimeCall { + RuntimeCall(moduleName: NominationPools.module, callName: "unbond", args: self) + } + } +} diff --git a/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolsBondExtraCall.swift b/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolsBondExtraCall.swift new file mode 100644 index 0000000000..0f5e98cccd --- /dev/null +++ b/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolsBondExtraCall.swift @@ -0,0 +1,52 @@ +import Foundation +import SubstrateSdk +import BigInt + +extension NominationPools { + struct BondExtraCall: Codable { + enum SourceType: Codable { + static let freeBalanceField = "FreeBalance" + static let rewardsField = "Rewards" + + case freeBalance(BigUInt) + case rewards + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + let type = try container.decode(String.self) + + switch type { + case Self.freeBalanceField: + let balance = try container.decode(StringScaleMapper.self).value + self = .freeBalance(balance) + case Self.rewardsField: + self = .rewards + default: + throw DecodingError.dataCorrupted( + .init(codingPath: decoder.codingPath, debugDescription: "Unsupported \(type)") + ) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + + switch self { + case let .freeBalance(balance): + try container.encode(Self.freeBalanceField) + try container.encode(StringScaleMapper(value: balance)) + case .rewards: + try container.encode(Self.rewardsField) + try container.encode(JSON.null) + } + } + } + + let extra: SourceType + + func runtimeCall() -> RuntimeCall { + RuntimeCall(moduleName: NominationPools.module, callName: "bond_extra", args: self) + } + } +} diff --git a/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolsJoin.swift b/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolsJoin.swift new file mode 100644 index 0000000000..9de18a1e3f --- /dev/null +++ b/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolsJoin.swift @@ -0,0 +1,19 @@ +import Foundation +import SubstrateSdk +import BigInt + +extension NominationPools { + struct JoinCall: Codable { + enum CodingKeys: String, CodingKey { + case amount + case poolId = "pool_id" + } + + @StringCodable var amount: BigUInt + @StringCodable var poolId: NominationPools.PoolId + + func runtimeCall() -> RuntimeCall { + RuntimeCall(moduleName: NominationPools.module, callName: "join", args: self) + } + } +} diff --git a/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolsRedeemCall.swift b/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolsRedeemCall.swift new file mode 100644 index 0000000000..1557fe55b4 --- /dev/null +++ b/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolsRedeemCall.swift @@ -0,0 +1,18 @@ +import Foundation +import SubstrateSdk + +extension NominationPools { + struct RedeemCall: Codable { + enum CodingKeys: String, CodingKey { + case memberAccount = "member_account" + case numberOfSlashingSpans = "num_slashing_spans" + } + + let memberAccount: MultiAddress + @StringCodable var numberOfSlashingSpans: UInt32 + + func runtimeCall() -> RuntimeCall { + RuntimeCall(moduleName: NominationPools.module, callName: "withdraw_unbonded", args: self) + } + } +} diff --git a/novawallet/Common/Substrate/Types/CallCodingPath.swift b/novawallet/Common/Substrate/Types/CallCodingPath.swift index dbcb3c47fd..406b0499ea 100644 --- a/novawallet/Common/Substrate/Types/CallCodingPath.swift +++ b/novawallet/Common/Substrate/Types/CallCodingPath.swift @@ -152,8 +152,16 @@ extension CallCodingPath { CallCodingPath(moduleName: "Substrate", callName: "reward") } - var isRewardOrSlash: Bool { - [.slash, .reward].contains(self) + static var poolReward: CallCodingPath { + CallCodingPath(moduleName: "Substrate", callName: "poolReward") + } + + static var poolSlash: CallCodingPath { + CallCodingPath(moduleName: "Substrate", callName: "poolSlash") + } + + var isAnyStakingRewardOrSlash: Bool { + [.slash, .reward, .poolReward, .poolSlash].contains(self) } } @@ -165,11 +173,11 @@ extension CallCodingPath { return false } - if !filter.contains(.rewardsAndSlashes), isRewardOrSlash { + if !filter.contains(.rewardsAndSlashes), isAnyStakingRewardOrSlash { return false } - if !filter.contains(.extrinsics), !isSubstrateOrEvmTransfer, !isRewardOrSlash { + if !filter.contains(.extrinsics), !isSubstrateOrEvmTransfer, !isAnyStakingRewardOrSlash { return false } diff --git a/novawallet/Common/Substrate/Types/NominationPools/NominationPools+CodingPath.swift b/novawallet/Common/Substrate/Types/NominationPools/NominationPools+CodingPath.swift new file mode 100644 index 0000000000..0854737400 --- /dev/null +++ b/novawallet/Common/Substrate/Types/NominationPools/NominationPools+CodingPath.swift @@ -0,0 +1,47 @@ +import Foundation + +extension NominationPools { + static var poolMembersPath: StorageCodingPath { + .init(moduleName: "NominationPools", itemName: "PoolMembers") + } + + static var palletIdPath: ConstantCodingPath { + .init(moduleName: "NominationPools", constantName: "PalletId") + } + + static var bondedPoolPath: StorageCodingPath { + .init(moduleName: "NominationPools", itemName: "BondedPools") + } + + static var lastPoolIdPath: StorageCodingPath { + .init(moduleName: "NominationPools", itemName: "LastPoolId") + } + + static var minJoinBondPath: StorageCodingPath { + .init(moduleName: "NominationPools", itemName: "MinJoinBond") + } + + static var metadataPath: StorageCodingPath { + .init(moduleName: "NominationPools", itemName: "Metadata") + } + + static var rewardPoolsPath: StorageCodingPath { + .init(moduleName: "NominationPools", itemName: "RewardPools") + } + + static var subPoolsPath: StorageCodingPath { + .init(moduleName: "NominationPools", itemName: "SubPoolsStorage") + } + + static var maxPoolMembers: StorageCodingPath { + .init(moduleName: "NominationPools", itemName: "MaxPoolMembers") + } + + static var counterForPoolMembers: StorageCodingPath { + .init(moduleName: "NominationPools", itemName: "CounterForPoolMembers") + } + + static var maxMembersPerPool: StorageCodingPath { + .init(moduleName: "NominationPools", itemName: "MaxPoolMembersPerPool") + } +} diff --git a/novawallet/Common/Substrate/Types/NominationPools/NominationPools+Functions.swift b/novawallet/Common/Substrate/Types/NominationPools/NominationPools+Functions.swift new file mode 100644 index 0000000000..06e63e8030 --- /dev/null +++ b/novawallet/Common/Substrate/Types/NominationPools/NominationPools+Functions.swift @@ -0,0 +1,78 @@ +import Foundation +import SubstrateSdk +import BigInt + +extension NominationPools { + static func derivedAccountPrefix(for palletId: Data) throws -> Data { + guard let prefix = "modl".data(using: .utf8) else { + throw CommonError.dataCorruption + } + + let scaleEncoder = ScaleEncoder() + scaleEncoder.appendRaw(data: prefix) + scaleEncoder.appendRaw(data: palletId) + + return scaleEncoder.encode() + } + + static func derivedAccount(for poolId: PoolId, accountType: AccountType, palletId: Data) throws -> AccountId { + guard let prefix = "modl".data(using: .utf8) else { + throw CommonError.dataCorruption + } + + let scaleEncoder = ScaleEncoder() + scaleEncoder.appendRaw(data: prefix) + scaleEncoder.appendRaw(data: palletId) + try accountType.rawValue.encode(scaleEncoder: scaleEncoder) + try poolId.encode(scaleEncoder: scaleEncoder) + scaleEncoder.appendRaw(data: Data(repeating: 0, count: SubstrateConstants.accountIdLength)) + + let result = scaleEncoder.encode() + + return result.prefix(SubstrateConstants.accountIdLength) + } + + static func pointsToBalance(for targetPoints: BigUInt, totalPoints: BigUInt, poolBalance: BigUInt) -> BigUInt { + guard poolBalance != 0, totalPoints != 0, targetPoints != 0 else { + return 0 + } + + return (poolBalance * targetPoints) / totalPoints + } + + static func balanceToPoints( + for targetBalance: BigUInt, + totalPoints: BigUInt, + poolBalance: BigUInt, + roundingUp: Bool + ) -> BigUInt { + guard poolBalance != 0, totalPoints != 0, targetBalance != 0 else { + return 0 + } + + let multBalancePoints = targetBalance * totalPoints + let (quotient, reminder) = multBalancePoints.quotientAndRemainder(dividingBy: poolBalance) + + if roundingUp, reminder > 0 { + return quotient + 1 + } else { + return quotient + } + } + + static func unstakingBalanceToPoints( + for targetBalance: BigUInt, + totalPoints: BigUInt, + poolBalance: BigUInt, + memberStakedPoints: BigUInt + ) -> BigUInt { + let unstakingPoints = balanceToPoints( + for: targetBalance, + totalPoints: totalPoints, + poolBalance: poolBalance, + roundingUp: true + ) + + return min(unstakingPoints, memberStakedPoints) + } +} diff --git a/novawallet/Common/Substrate/Types/NominationPools/NominationPools.swift b/novawallet/Common/Substrate/Types/NominationPools/NominationPools.swift new file mode 100644 index 0000000000..671c7a6e94 --- /dev/null +++ b/novawallet/Common/Substrate/Types/NominationPools/NominationPools.swift @@ -0,0 +1,141 @@ +import Foundation +import SubstrateSdk +import BigInt + +enum NominationPools { + static let module = "NominationPools" + typealias PoolId = UInt32 + + struct PoolMember: Decodable, Equatable { + @StringCodable var poolId: PoolId + @StringCodable var points: BigUInt + @StringCodable var lastRecordedRewardCounter: BigUInt + let unbondingEras: [SupportPallet.KeyValue, StringScaleMapper>] + } + + enum PoolState: Decodable, Equatable { + case open + case blocked + case destroying + case unsuppored + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + let rawValue = try container.decode(String.self) + + switch rawValue { + case "Open": + self = .open + case "Blocked": + self = .blocked + case "Destroying": + self = .destroying + default: + self = .unsuppored + } + } + } + + struct AccountCommission: Decodable, Equatable { + let percent: BigUInt + let accountId: AccountId + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + percent = try container.decode(StringScaleMapper.self).value + accountId = try container.decode(BytesCodable.self).wrappedValue + } + } + + struct Commission: Decodable, Equatable { + let current: AccountCommission? + } + + struct BondedPool: Decodable, Equatable { + @StringCodable var points: BigUInt + @StringCodable var memberCounter: UInt32 + let commission: Commission? + let state: PoolState + + func checkPoolSpare(for maxMembersPerPool: UInt32?) -> Bool { + guard state == .open else { + return false + } + + if let maxMembersPerPool = maxMembersPerPool { + return memberCounter < maxMembersPerPool + } else { + return true + } + } + } + + struct RewardPool: Decodable, Equatable { + @StringCodable var lastRecordedRewardCounter: BigUInt + } + + struct UnbondPool: Decodable, Equatable { + @StringCodable var points: BigUInt + @StringCodable var balance: BigUInt + } + + struct SubPools: Decodable, Equatable { + let noEra: UnbondPool + let withEra: [SupportPallet.KeyValue, UnbondPool>] + + func getPoolsByEra() -> [EraIndex: NominationPools.UnbondPool] { + withEra.reduce(into: [EraIndex: NominationPools.UnbondPool]()) { + $0[$1.key.value] = $1.value + } + } + + func redeemableBalance(for member: NominationPools.PoolMember, in era: EraIndex) -> BigUInt { + let poolsByEra = getPoolsByEra() + + return member.unbondingEras.reduce(BigUInt(0)) { redeemable, unbondingKeyValue in + let unbondingEra = unbondingKeyValue.key.value + let unbondingPoints = unbondingKeyValue.value.value + + guard era >= unbondingEra else { + return redeemable + } + + let subPool = poolsByEra[unbondingEra] ?? noEra + + let newAmount = NominationPools.pointsToBalance( + for: unbondingPoints, + totalPoints: subPool.points, + poolBalance: subPool.balance + ) + + return redeemable + newAmount + } + } + + func unbondingBalance(for member: NominationPools.PoolMember) -> BigUInt { + let poolsByEra = getPoolsByEra() + + return member.unbondingEras.reduce(BigUInt(0)) { total, unbondingKeyValue in + let unbondingEra = unbondingKeyValue.key.value + let unbondingPoints = unbondingKeyValue.value.value + + let subPool = poolsByEra[unbondingEra] ?? noEra + + let newAmount = NominationPools.pointsToBalance( + for: unbondingPoints, + totalPoints: subPool.points, + poolBalance: subPool.balance + ) + + return total + newAmount + } + } + } + + enum AccountType: UInt8 { + case bonded + case reward + } +} diff --git a/novawallet/Common/Substrate/Types/ParachainStaking/ConstantCodingPath+ParachainStaking.swift b/novawallet/Common/Substrate/Types/ParachainStaking/ConstantCodingPath+ParachainStaking.swift index 6a51156e65..cd5343b45f 100644 --- a/novawallet/Common/Substrate/Types/ParachainStaking/ConstantCodingPath+ParachainStaking.swift +++ b/novawallet/Common/Substrate/Types/ParachainStaking/ConstantCodingPath+ParachainStaking.swift @@ -26,4 +26,11 @@ extension ParachainStaking { constantName: "DelegationBondLessDelay" ) } + + static var rewardPaymentDelay: ConstantCodingPath { + ConstantCodingPath( + moduleName: "ParachainStaking", + constantName: "RewardPaymentDelay" + ) + } } diff --git a/novawallet/Common/Substrate/Types/SlashingSpans.swift b/novawallet/Common/Substrate/Types/SlashingSpans.swift index 8b93bf2a9f..49484ac420 100644 --- a/novawallet/Common/Substrate/Types/SlashingSpans.swift +++ b/novawallet/Common/Substrate/Types/SlashingSpans.swift @@ -5,3 +5,9 @@ struct SlashingSpans: Decodable { @StringCodable var lastNonzeroSlash: UInt32 let prior: [StringScaleMapper] } + +extension SlashingSpans { + var numOfSlashingSpans: UInt32 { + UInt32(prior.count) + 1 + } +} diff --git a/novawallet/Common/Substrate/Types/Staking/Staking.swift b/novawallet/Common/Substrate/Types/Staking/Staking.swift index 2adb2e644e..d274ce4014 100644 --- a/novawallet/Common/Substrate/Types/Staking/Staking.swift +++ b/novawallet/Common/Substrate/Types/Staking/Staking.swift @@ -10,4 +10,8 @@ enum Staking { static var historyDepthCostantPath: ConstantCodingPath { ConstantCodingPath(moduleName: module, constantName: "HistoryDepth") } + + static var maxUnlockingChunksConstantPath: ConstantCodingPath { + ConstantCodingPath(moduleName: module, constantName: "MaxUnlockingChunks") + } } diff --git a/novawallet/Common/Substrate/Types/Support/SupportPallet.swift b/novawallet/Common/Substrate/Types/Support/SupportPallet.swift index cf0c5ea8c0..768d83fe8a 100644 --- a/novawallet/Common/Substrate/Types/Support/SupportPallet.swift +++ b/novawallet/Common/Substrate/Types/Support/SupportPallet.swift @@ -55,4 +55,16 @@ enum SupportPallet { } } } + + struct KeyValue: Decodable, Equatable { + let key: K + let value: V + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + key = try container.decode(K.self) + value = try container.decode(V.self) + } + } } diff --git a/novawallet/Common/View/StackTable/StackAddressCell.swift b/novawallet/Common/View/StackTable/StackAddressCell.swift new file mode 100644 index 0000000000..73fdf25be8 --- /dev/null +++ b/novawallet/Common/View/StackTable/StackAddressCell.swift @@ -0,0 +1,124 @@ +import Foundation +import SoraUI + +final class StackAddressCell: RowView> { + var titleView: LoadableIconDetailsView { rowContentView.titleView } + var indicatorView: UIImageView { rowContentView.valueView } + + var skeletonView: SkrullableView? + + var isLoading: Bool = false + + convenience init() { + self.init(frame: CGRect(origin: .zero, size: CGSize(width: 340, height: 44.0))) + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .clear + + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + if isLoading { + updateLoadingState() + skeletonView?.restartSkrulling() + } + } + + func bind(viewModel: LoadableViewModelState) { + stopLoadingIfNeeded() + + switch viewModel { + case let .cached(addressViewModel), let .loaded(addressViewModel): + titleView.detailsLabel.lineBreakMode = addressViewModel.lineBreakMode + titleView.bind(viewModel: addressViewModel.cellViewModel) + case .loading: + startLoadingIfNeeded() + } + } + + private func configure() { + titleView.detailsLabel.apply(style: .regularSubhedlinePrimary) + + titleView.mode = .iconDetails + titleView.spacing = 12 + titleView.iconWidth = 24 + titleView.imageView.contentMode = .scaleAspectFit + titleView.detailsLabel.numberOfLines = 1 + + indicatorView.image = R.image.iconInfoFilled()? + .withRenderingMode(.alwaysTemplate) + .tinted(with: R.color.colorIconSecondary()!) + } +} + +extension StackAddressCell: StackTableViewCellProtocol {} + +extension StackAddressCell: SkeletonableView { + var skeletonSuperview: UIView { + self + } + + var hidingViews: [UIView] { + [titleView, indicatorView] + } + + func createSkeletons(for spaceSize: CGSize) -> [Skeletonable] { + [ + SingleSkeleton.createRow( + on: self, + containerView: self, + spaceSize: spaceSize, + offset: CGPoint(x: contentInsets.left, y: 10), + size: CGSize(width: 24, height: 24) + ), + + SingleSkeleton.createRow( + on: self, + containerView: self, + spaceSize: spaceSize, + offset: CGPoint(x: contentInsets.left + 36, y: 17), + size: CGSize(width: 135, height: 10) + ) + ] + } + + func didStartSkeleton() { + isLoading = true + } + + func didStopSkeleton() { + isLoading = false + } +} + +extension StackAddressCell: SkeletonLoadable { + func didDisappearSkeleton() { + if isLoading { + skeletonView?.stopSkrulling() + } + } + + func didAppearSkeleton() { + if isLoading { + skeletonView?.restartSkrulling() + } + } + + func didUpdateSkeletonLayout() { + if isLoading { + updateLoadingState() + skeletonView?.restartSkrulling() + } + } +} diff --git a/novawallet/Common/View/StackTable/StackSwitchCell.swift b/novawallet/Common/View/StackTable/StackSwitchCell.swift new file mode 100644 index 0000000000..a6b0b6acd4 --- /dev/null +++ b/novawallet/Common/View/StackTable/StackSwitchCell.swift @@ -0,0 +1,43 @@ +import UIKit + +final class StackSwitchCell: RowView> { + var titleLabel: UILabel { rowContentView.titleView.valueTop } + var subtitleLabel: UILabel { rowContentView.titleView.valueBottom } + var switchControl: UISwitch { rowContentView.valueView } + + convenience init() { + self.init(frame: CGRect(origin: .zero, size: CGSize(width: 340, height: 44.0))) + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .clear + + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + hasInteractableContent = true + roundedBackgroundView.highlightedFillColor = .clear + + titleLabel.apply(style: .footnotePrimary) + titleLabel.textAlignment = .left + + subtitleLabel.apply(style: .caption1Secondary) + subtitleLabel.textAlignment = .left + + switchControl.onTintColor = R.color.colorIconAccent() + + rowContentView.titleView.spacing = 2 + + rowContentView.titleView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } +} + +extension StackSwitchCell: StackTableViewCellProtocol {} diff --git a/novawallet/Common/View/TitleHorizontalMultiValueView.swift b/novawallet/Common/View/TitleHorizontalMultiValueView.swift deleted file mode 100644 index e74554b0a7..0000000000 --- a/novawallet/Common/View/TitleHorizontalMultiValueView.swift +++ /dev/null @@ -1,38 +0,0 @@ -import UIKit - -final class TitleHorizontalMultiValueView: GenericTitleValueView { - let detailsTitleLabel: UILabel = { - let label = UILabel() - label.textColor = R.color.colorTextSecondary() - label.font = .regularFootnote - return label - }() - - let detailsValueLabel: UILabel = { - let label = UILabel() - label.textColor = R.color.colorTextPrimary() - label.font = .regularFootnote - return label - }() - - convenience init() { - self.init(frame: .zero) - } - - override init(frame: CGRect) { - super.init(frame: frame) - - configureStyle() - } - - private func configureStyle() { - titleView.textColor = R.color.colorTextSecondary() - titleView.font = .regularFootnote - - valueView.spacing = 4.0 - valueView.addArrangedSubview(detailsTitleLabel) - valueView.addArrangedSubview(detailsValueLabel) - - detailsTitleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - } -} diff --git a/novawallet/Common/View/TitleHorizontalMultivalueView/TitleHorizontalMultiValueView+Bind.swift b/novawallet/Common/View/TitleHorizontalMultivalueView/TitleHorizontalMultiValueView+Bind.swift new file mode 100644 index 0000000000..6a51af9e7d --- /dev/null +++ b/novawallet/Common/View/TitleHorizontalMultivalueView/TitleHorizontalMultiValueView+Bind.swift @@ -0,0 +1,15 @@ +import UIKit + +extension TitleHorizontalMultiValueView { + struct Model { + let title: String + let subtitle: String + let value: String + } + + func bind(model: Model) { + titleView.text = model.title + detailsTitleLabel.text = model.subtitle + detailsValueLabel.text = model.value + } +} diff --git a/novawallet/Common/View/TitleHorizontalMultivalueView/TitleHorizontalMultiValueView.swift b/novawallet/Common/View/TitleHorizontalMultivalueView/TitleHorizontalMultiValueView.swift new file mode 100644 index 0000000000..dcada449cd --- /dev/null +++ b/novawallet/Common/View/TitleHorizontalMultivalueView/TitleHorizontalMultiValueView.swift @@ -0,0 +1,87 @@ +import UIKit +import SoraUI + +class TitleHorizontalMultiValueView: GenericTitleValueView { + let detailsTitleLabel: UILabel = { + let label = UILabel() + label.textColor = R.color.colorTextSecondary() + label.font = .regularFootnote + return label + }() + + let detailsValueLabel: UILabel = { + let label = UILabel() + label.textColor = R.color.colorTextPrimary() + label.font = .regularFootnote + return label + }() + + convenience init() { + self.init(frame: .zero) + } + + override init(frame: CGRect) { + super.init(frame: frame) + + configureStyle() + } + + private func configureStyle() { + titleView.textColor = R.color.colorTextSecondary() + titleView.font = .regularFootnote + + valueView.spacing = 4.0 + valueView.addArrangedSubview(detailsTitleLabel) + valueView.addArrangedSubview(detailsValueLabel) + + detailsTitleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + } +} + +final class LoadableTitleHorizontalMultiValueView: TitleHorizontalMultiValueView { + var skeletonView: SkrullableView? + + private var isLoading: Bool = false + + override func layoutSubviews() { + super.layoutSubviews() + + if isLoading { + updateLoadingState() + skeletonView?.restartSkrulling() + } + } +} + +extension LoadableTitleHorizontalMultiValueView: SkeletonableView { + func createSkeletons(for spaceSize: CGSize) -> [Skeletonable] { + let size = CGSize(width: 57, height: 10) + let offset = CGPoint(x: spaceSize.width - size.width, y: spaceSize.height / 2.0 - size.height / 2.0) + + let row = SingleSkeleton.createRow( + on: self, + containerView: self, + spaceSize: spaceSize, + offset: offset, + size: size + ) + + return [row] + } + + var skeletonSuperview: UIView { + self + } + + var hidingViews: [UIView] { + [valueView] + } + + func didStartSkeleton() { + isLoading = true + } + + func didStopSkeleton() { + isLoading = false + } +} diff --git a/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift b/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift index 78a533cb4e..33cbb6cdea 100644 --- a/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift +++ b/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift @@ -170,7 +170,7 @@ struct ModalInfoFactory { priceFormatter: priceFormatter ) - let crowdloans = createCrowdloansViewModel( + let externalBalances = createExternalBalancesViewModel( balanceContext: balanceContext, amountFormatter: amountFormatter, priceFormatter: priceFormatter @@ -192,7 +192,7 @@ struct ModalInfoFactory { precision: precision ) - return (balanceLockKnownModels + balanceLockUnknownModels + crowdloans + reserved) + return (balanceLockKnownModels + balanceLockUnknownModels + externalBalances + reserved) .sorted { viewModel1, viewModel2 in viewModel1.value >= viewModel2.value }.map(\.viewModel) @@ -239,26 +239,29 @@ struct ModalInfoFactory { } } - private static func createCrowdloansViewModel( + private static func createExternalBalancesViewModel( balanceContext: BalanceContext, amountFormatter: LocalizableResource, priceFormatter: LocalizableResource ) -> LocksSortingViewModel { - guard balanceContext.crowdloans > 0 else { - return [] - } + balanceContext.external.flatMap { keyValue in + let group = keyValue.key + let amount = keyValue.value - let title = LocalizableResource { locale in - R.string.localizable.walletAccountLocksCrowdloans(preferredLanguages: locale.rLanguages) - } + guard amount > 0 else { + return LocksSortingViewModel() + } - return createLockFieldViewModel( - amount: balanceContext.crowdloans, - price: balanceContext.price, - localizedTitle: title, - amountFormatter: amountFormatter, - priceFormatter: priceFormatter - ) + let title = group.type.lockTitle + + return createLockFieldViewModel( + amount: amount, + price: balanceContext.price, + localizedTitle: title, + amountFormatter: amountFormatter, + priceFormatter: priceFormatter + ) + } } private static func createReservedViewModel( diff --git a/novawallet/Modules/Staking/StakingAmount/ViewModel/AssetBalanceViewModel.swift b/novawallet/Common/ViewModel/Amount/AssetBalanceViewModel.swift similarity index 100% rename from novawallet/Modules/Staking/StakingAmount/ViewModel/AssetBalanceViewModel.swift rename to novawallet/Common/ViewModel/Amount/AssetBalanceViewModel.swift diff --git a/novawallet/Modules/Staking/StakingAmount/ViewModel/BalanceViewModel.swift b/novawallet/Common/ViewModel/Amount/BalanceViewModel.swift similarity index 100% rename from novawallet/Modules/Staking/StakingAmount/ViewModel/BalanceViewModel.swift rename to novawallet/Common/ViewModel/Amount/BalanceViewModel.swift diff --git a/novawallet/Modules/Staking/StakingAmount/ViewModel/BalanceViewModelFactory.swift b/novawallet/Common/ViewModel/Amount/BalanceViewModelFactory.swift similarity index 92% rename from novawallet/Modules/Staking/StakingAmount/ViewModel/BalanceViewModelFactory.swift rename to novawallet/Common/ViewModel/Amount/BalanceViewModelFactory.swift index 0f3c4216f1..255d1945eb 100644 --- a/novawallet/Modules/Staking/StakingAmount/ViewModel/BalanceViewModelFactory.swift +++ b/novawallet/Common/ViewModel/Amount/BalanceViewModelFactory.swift @@ -34,6 +34,25 @@ extension BalanceViewModelFactoryProtocol { func amountFromValue(_ value: Decimal) -> LocalizableResource { amountFromValue(value, roundingMode: .down) } + + func balanceWithPriceIfPossible( + amount: BigUInt?, + priceData: PriceData?, + chainAsset: ChainAsset + ) -> LocalizableResource { + .init { locale in + let precision = chainAsset.assetDisplayInfo.assetPrecision + guard let amountDecimal = Decimal.fromSubstrateAmount(amount ?? 0, precision: precision) else { + return BalanceViewModel(amount: "", price: nil) + } + let balance = balanceFromPrice(amountDecimal, priceData: priceData).value(for: locale) + if balance.price != nil, let amount = amount, amount > 0 { + return balance + } else { + return BalanceViewModel(amount: balance.amount, price: nil) + } + } + } } final class BalanceViewModelFactory: BalanceViewModelFactoryProtocol { diff --git a/novawallet/Common/ViewModel/DisplayAddressViewModelFactory.swift b/novawallet/Common/ViewModel/DisplayAddressViewModelFactory.swift index a97a491b85..47e960a34c 100644 --- a/novawallet/Common/ViewModel/DisplayAddressViewModelFactory.swift +++ b/novawallet/Common/ViewModel/DisplayAddressViewModelFactory.swift @@ -5,6 +5,10 @@ protocol DisplayAddressViewModelFactoryProtocol { func createViewModel(from model: DisplayAddress) -> DisplayAddressViewModel func createViewModel(from model: DisplayAddress, using chainFormat: ChainFormat) -> DisplayAddressViewModel func createViewModel(from address: AccountAddress, name: String?, iconUrl: URL?) -> DisplayAddressViewModel + func createViewModel( + from pool: NominationPools.SelectedPool, + chainAsset: ChainAsset + ) -> DisplayAddressViewModel } extension DisplayAddressViewModelFactoryProtocol { @@ -15,6 +19,7 @@ extension DisplayAddressViewModelFactoryProtocol { final class DisplayAddressViewModelFactory: DisplayAddressViewModelFactoryProtocol { private lazy var iconGenerator = PolkadotIconGenerator() + private lazy var poolIconFactory = NominationPoolsIconFactory() func createViewModel(from model: DisplayAddress) -> DisplayAddressViewModel { createViewModel(from: model, chainFormat: nil) @@ -72,4 +77,23 @@ final class DisplayAddressViewModelFactory: DisplayAddressViewModelFactoryProtoc imageViewModel: imageViewModel ) } + + func createViewModel( + from pool: NominationPools.SelectedPool, + chainAsset: ChainAsset + ) -> DisplayAddressViewModel { + let poolIcon = poolIconFactory.createIconViewModel( + for: chainAsset, + poolId: pool.poolId, + bondedAccountId: pool.bondedAccountId + ) + + let address = pool.bondedAddress(for: chainAsset.chain.chainFormat) + + return .init( + address: address ?? "", + name: pool.name, + imageViewModel: poolIcon + ) + } } diff --git a/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift b/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift index 44c2366c07..f51f084c52 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift @@ -7,14 +7,14 @@ final class AssetDetailsInteractor { let selectedMetaAccount: MetaAccountModel let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol - let crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol + let externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactoryProtocol let purchaseProvider: PurchaseProviderProtocol let assetMapper: CustomAssetMapper private var assetLocksSubscription: StreamableProvider? private var priceSubscription: StreamableProvider? private var assetBalanceSubscription: StreamableProvider? - private var crowdloansSubscription: StreamableProvider? + private var externalBalanceSubscription: StreamableProvider? private var accountId: AccountId? { selectedMetaAccount.fetch(for: chainAsset.chain.accountRequest())?.accountId @@ -26,12 +26,12 @@ final class AssetDetailsInteractor { purchaseProvider: PurchaseProviderProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, - crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol, + externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactoryProtocol, currencyManager: CurrencyManagerProtocol ) { self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory - self.crowdloansLocalSubscriptionFactory = crowdloansLocalSubscriptionFactory + self.externalBalancesSubscriptionFactory = externalBalancesSubscriptionFactory self.selectedMetaAccount = selectedMetaAccount self.chainAsset = chainAsset self.purchaseProvider = purchaseProvider @@ -92,14 +92,16 @@ extension AssetDetailsInteractor: AssetDetailsInteractorInputProtocol { chainId: chainAsset.chain.chainId, assetId: chainAsset.asset.assetId ) - if chainAsset.chain.hasCrowdloans { - crowdloansSubscription = subscribeToCrowdloansProvider( + + if chainAsset.chain.chainAssetIdsWithExternalBalances().contains(chainAsset.chainAssetId) { + externalBalanceSubscription = subscribeToExternalAssetBalancesProvider( for: accountId, - chain: chainAsset.chain + chainAsset: chainAsset ) } else { - crowdloansSubscription = nil + externalBalanceSubscription = nil } + setAvailableOperations() } } @@ -168,22 +170,17 @@ extension AssetDetailsInteractor: SelectedCurrencyDepending { } } -extension AssetDetailsInteractor: CrowdloanContributionLocalSubscriptionHandler, CrowdloansLocalStorageSubscriber { - func handleCrowdloans( - result: Result<[DataProviderChange], Error>, - accountId: AccountId, - chain: ChainModel +extension AssetDetailsInteractor: ExternalAssetBalanceSubscriber, ExternalAssetBalanceSubscriptionHandler { + func handleExternalAssetBalances( + result: Result<[DataProviderChange], Error>, + accountId _: AccountId, + chainAsset _: ChainAsset ) { - guard self.accountId == accountId, - chainAsset.chain.chainId == chain.chainId else { - return - } - switch result { + case let .success(externalBalanceChanges): + presenter.didReceive(externalBalanceChanges: externalBalanceChanges) case let .failure(error): - presenter.didReceive(error: .crowdloans(error)) - case let .success(changes): - presenter.didReceive(crowdloanChanges: changes) + presenter.didReceive(error: .externalBalances(error)) } } } diff --git a/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift b/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift index 292b17d25f..de7d989fe2 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift @@ -15,7 +15,7 @@ final class AssetDetailsPresenter { private var priceData: PriceData? private var balance: AssetBalance? private var locks: [AssetLock] = [] - private var crowdloans: [CrowdloanContributionData] = [] + private var externalAssetBalances: [ExternalAssetBalance] = [] private var purchaseActions: [PurchaseAction] = [] private var availableOperations: AssetDetailsOperation = [] @@ -37,12 +37,12 @@ final class AssetDetailsPresenter { localizationManager = localizableManager } - private func hasLocks(for balance: AssetBalance, crowdloans: [CrowdloanContributionData]) -> Bool { - balance.locked > 0 || !crowdloans.isEmpty + private func hasLocks(for balance: AssetBalance, externalBalances: [ExternalAssetBalance]) -> Bool { + balance.locked > 0 || !externalBalances.isEmpty } - private func calculateTotalCrowdloans(for crowdloans: [CrowdloanContributionData]) -> BigUInt { - crowdloans.reduce(0) { $0 + $1.amount } + private func calculateTotalExternalBalances(for externalBalances: [ExternalAssetBalance]) -> BigUInt { + externalBalances.reduce(0) { $0 + $1.amount } } private func updateView() { @@ -62,10 +62,10 @@ final class AssetDetailsPresenter { ) view.didReceive(assetModel: assetDetailsModel) - let totalCrowdloans = calculateTotalCrowdloans(for: crowdloans) + let totalExternalBalances = calculateTotalExternalBalances(for: externalAssetBalances) let totalBalance = viewModelFactory.createBalanceViewModel( - value: balance.totalInPlank + totalCrowdloans, + value: balance.totalInPlank + totalExternalBalances, assetDisplayInfo: chainAsset.assetDisplayInfo, priceData: priceData, locale: selectedLocale @@ -79,7 +79,7 @@ final class AssetDetailsPresenter { ) let lockedBalance = viewModelFactory.createBalanceViewModel( - value: balance.locked + totalCrowdloans, + value: balance.locked + totalExternalBalances, assetDisplayInfo: chainAsset.assetDisplayInfo, priceData: priceData, locale: selectedLocale @@ -88,7 +88,7 @@ final class AssetDetailsPresenter { view.didReceive(totalBalance: totalBalance) view.didReceive(transferableBalance: transferableBalance) - let isSelectable = hasLocks(for: balance, crowdloans: crowdloans) + let isSelectable = hasLocks(for: balance, externalBalances: externalAssetBalances) view.didReceive(lockedBalance: lockedBalance, isSelectable: isSelectable) view.didReceive(availableOperations: availableOperations) @@ -182,11 +182,16 @@ extension AssetDetailsPresenter: AssetDetailsPresenterProtocol { return } let precision = chainAsset.asset.precision + + let groupedExternalBalances = externalAssetBalances + .groupByAssetType() + .mapValues { $0.decimal(precision: precision) } + let balanceContext = BalanceContext( free: balance.freeInPlank.decimal(precision: precision), reserved: balance.reservedInPlank.decimal(precision: precision), frozen: balance.frozenInPlank.decimal(precision: precision), - crowdloans: calculateTotalCrowdloans(for: crowdloans).decimal(precision: precision), + external: groupedExternalBalances, price: priceData.map { Decimal(string: $0.price) ?? 0 } ?? 0, priceChange: priceData?.dayChange ?? 0, priceId: priceData?.currencyId, @@ -228,8 +233,8 @@ extension AssetDetailsPresenter: AssetDetailsInteractorOutputProtocol { updateView() } - func didReceive(crowdloanChanges: [DataProviderChange]) { - crowdloans = crowdloans.applying(changes: crowdloanChanges) + func didReceive(externalBalanceChanges: [DataProviderChange]) { + externalAssetBalances = externalAssetBalances.applying(changes: externalBalanceChanges) updateView() } diff --git a/novawallet/Modules/AssetDetails/AssetDetailsProtocols.swift b/novawallet/Modules/AssetDetails/AssetDetailsProtocols.swift index 5a9055f216..89d5cb50d8 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsProtocols.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsProtocols.swift @@ -23,7 +23,7 @@ protocol AssetDetailsInteractorInputProtocol: AnyObject { protocol AssetDetailsInteractorOutputProtocol: AnyObject { func didReceive(balance: AssetBalance?) func didReceive(lockChanges: [DataProviderChange]) - func didReceive(crowdloanChanges: [DataProviderChange]) + func didReceive(externalBalanceChanges: [DataProviderChange]) func didReceive(price: PriceData?) func didReceive(error: AssetDetailsError) func didReceive(availableOperations: AssetDetailsOperation) @@ -57,5 +57,5 @@ enum AssetDetailsError: Error { case accountBalance(Error) case price(Error) case locks(Error) - case crowdloans(Error) + case externalBalances(Error) } diff --git a/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift b/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift index f090ef397e..439688da81 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsViewFactory.swift @@ -16,7 +16,7 @@ struct AssetDetailsViewFactory { purchaseProvider: PurchaseAggregator.defaultAggregator(), walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, - crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactory.shared, + externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactory.shared, currencyManager: currencyManager ) let wireframe = AssetDetailsWireframe() diff --git a/novawallet/Modules/AssetDetails/Model/BalanceContext.swift b/novawallet/Modules/AssetDetails/Model/BalanceContext.swift index 3e9aabc1ef..a05fc632cf 100644 --- a/novawallet/Modules/AssetDetails/Model/BalanceContext.swift +++ b/novawallet/Modules/AssetDetails/Model/BalanceContext.swift @@ -1,19 +1,10 @@ import Foundation struct BalanceContext { - static let freeKey = "account.balance.free.key" - static let reservedKey = "account.balance.reserved.key" - static let frozen = "account.balance.frozen.key" - static let priceKey = "account.balance.price.key" - 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 external: [ExternalBalanceAssetGroupId: Decimal] let price: Decimal let priceChange: Decimal let priceId: Int? @@ -21,134 +12,8 @@ struct BalanceContext { } extension BalanceContext { - var total: Decimal { free + reserved + crowdloans } - var locked: Decimal { reserved + frozen + crowdloans } + var externalTotal: Decimal { external.values.reduce(0) { $0 + $1 } } + var total: Decimal { free + reserved + externalTotal } + var locked: Decimal { reserved + frozen + externalTotal } var available: Decimal { free >= frozen ? free - frozen : 0.0 } } - -extension BalanceContext { - init(context: [String: String]) { - free = Self.parseContext(key: BalanceContext.freeKey, context: context) - reserved = Self.parseContext(key: BalanceContext.reservedKey, context: context) - frozen = Self.parseContext(key: BalanceContext.frozen, context: context) - - price = Self.parseContext(key: BalanceContext.priceKey, context: context) - 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) - } - - func toContext() -> [String: String] { - let locksStringRepresentation: String = { - guard let locksJSON = try? JSONEncoder().encode(balanceLocks) else { - return "" - } - - return String(data: locksJSON, encoding: .utf8) ?? "" - }() - - var dict = [ - 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 - ] - - if let priceId = priceId { - dict[BalanceContext.priceIdKey] = String(priceId) - } - - return dict - } - - private static func parseContext(key: String, context: [String: String]) -> Decimal { - if let stringValue = context[key] { - return Decimal(string: stringValue) ?? .zero - } else { - return .zero - } - } - - 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 { - return [] - } - - let balanceLocks = try? JSONDecoder().decode( - [AssetLock].self, - from: JSONData - ) - - return balanceLocks ?? [] - } -} - -extension BalanceContext { - func byChangingAssetBalance(_ assetBalance: AssetBalance, precision: Int16) -> BalanceContext { - let free = Decimal - .fromSubstrateAmount(assetBalance.freeInPlank, precision: precision) ?? .zero - let reserved = Decimal - .fromSubstrateAmount(assetBalance.reservedInPlank, precision: precision) ?? .zero - let frozen = Decimal - .fromSubstrateAmount(assetBalance.frozenInPlank, precision: precision) ?? .zero - - return BalanceContext( - free: free, - reserved: reserved, - frozen: frozen, - crowdloans: crowdloans, - price: price, - priceChange: priceChange, - priceId: priceId, - balanceLocks: balanceLocks - ) - } - - func byChangingBalanceLocks( - _ updatedLocks: [AssetLock] - ) -> BalanceContext { - BalanceContext( - free: free, - reserved: reserved, - frozen: frozen, - crowdloans: crowdloans, - price: price, - priceChange: priceChange, - priceId: priceId, - balanceLocks: updatedLocks - ) - } - - func byChangingPrice(_ newPrice: Decimal, newPriceChange: Decimal, newPriceId: Int?) -> BalanceContext { - 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/Modules/AssetList/AssetListInteractor.swift b/novawallet/Modules/AssetList/AssetListInteractor.swift index 4ec3dcd9f2..044d8d9901 100644 --- a/novawallet/Modules/AssetList/AssetListInteractor.swift +++ b/novawallet/Modules/AssetList/AssetListInteractor.swift @@ -35,7 +35,7 @@ final class AssetListInteractor: AssetListBaseInteractor { assetListObservable: AssetListStateObservable, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, nftLocalSubscriptionFactory: NftLocalSubscriptionFactoryProtocol, - crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol, + externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, eventCenter: EventCenterProtocol, settingsManager: SettingsManagerProtocol, @@ -53,7 +53,7 @@ final class AssetListInteractor: AssetListBaseInteractor { selectedWalletSettings: selectedWalletSettings, chainRegistry: chainRegistry, walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, - crowdloansLocalSubscriptionFactory: crowdloansLocalSubscriptionFactory, + externalBalancesSubscriptionFactory: externalBalancesSubscriptionFactory, priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, currencyManager: currencyManager, logger: logger diff --git a/novawallet/Modules/AssetList/AssetListPresenter.swift b/novawallet/Modules/AssetList/AssetListPresenter.swift index baace9fc26..8d1d6553b1 100644 --- a/novawallet/Modules/AssetList/AssetListPresenter.swift +++ b/novawallet/Modules/AssetList/AssetListPresenter.swift @@ -124,9 +124,9 @@ final class AssetListPresenter { walletType: MetaAccountModelType, name: String ) { - let crowdloans = crowdloansModel(prices: priceMapping) - let totalValue = createHeaderPriceState(from: priceMapping, crowdloans: crowdloans) - let totalLocks = createHeaderLockState(from: priceMapping, crowdloans: crowdloans) + let externalBalances = externalBalanceModel(prices: priceMapping) + let totalValue = createHeaderPriceState(from: priceMapping, externalBalances: externalBalances) + let totalLocks = createHeaderLockState(from: priceMapping, externalBalances: externalBalances) let viewModel = viewModelFactory.createHeaderViewModel( from: name, @@ -143,7 +143,7 @@ final class AssetListPresenter { private func createHeaderPriceState( from priceMapping: [ChainAssetId: PriceData], - crowdloans: [AssetListAssetAccountPrice] + externalBalances: [AssetListAssetAccountPrice] ) -> LoadableViewModelState<[AssetListAssetAccountPrice]> { var priceState: LoadableViewModelState<[AssetListAssetAccountPrice]> = .loaded(value: []) @@ -178,12 +178,12 @@ final class AssetListPresenter { } } - return priceState + crowdloans + return priceState + externalBalances } private func createHeaderLockState( from priceMapping: [ChainAssetId: PriceData], - crowdloans: [AssetListAssetAccountPrice] + externalBalances: [AssetListAssetAccountPrice] ) -> [AssetListAssetAccountPrice]? { guard checkNonZeroLocks() else { return nil @@ -195,7 +195,7 @@ final class AssetListPresenter { } } - return locks + crowdloans + return locks + externalBalances } private func checkNonZeroLocks() -> Bool { @@ -205,9 +205,9 @@ final class AssetListPresenter { return true } - let crowdloanContributions = (try? model.crowdloansResult?.get()) ?? [:] + let externalBalances = (try? model.externalBalanceResult?.get()) ?? [:] - if crowdloanContributions.contains(where: { $0.value.contains(where: { $0.amount > 0 }) }) { + if externalBalances.contains(where: { $0.value.contains(where: { $0.amount > 0 }) }) { return true } @@ -235,22 +235,22 @@ final class AssetListPresenter { } } - private func crowdloansModel(prices: [ChainAssetId: PriceData]) -> [AssetListAssetAccountPrice] { - switch model.crowdloansResult { + private func externalBalanceModel(prices: [ChainAssetId: PriceData]) -> [AssetListAssetAccountPrice] { + switch model.externalBalanceResult { case .failure, .none: return [] - case let .success(crowdloans): - return crowdloans.compactMap { chainId, chainCrowdloans in - guard let chain = model.allChains[chainId] else { + case let .success(externalBalance): + return externalBalance.compactMap { chainAssetId, externalAssetBalances in + guard let chain = model.allChains[chainAssetId.chainId] else { return nil } - guard let asset = chain.utilityAsset() else { + guard let asset = chain.asset(for: chainAssetId.assetId) else { return nil } - let chainAssetId = ChainAssetId(chainId: chainId, assetId: asset.assetId) + let price = prices[chainAssetId] ?? .zero() - let contributedAmount = chainCrowdloans.reduce(0) { $0 + $1.amount } + let contributedAmount = externalAssetBalances.reduce(0) { $0 + $1.amount } guard contributedAmount > 0 else { return nil @@ -381,18 +381,19 @@ extension AssetListPresenter: AssetListPresenterProtocol { let priceResult = model.priceResult, let prices = try? priceResult.get(), let locks = try? model.locksResult?.get(), - let crowdloans = try? model.crowdloansResult?.get() else { + let externalBalances = try? model.externalBalanceResult?.get() else { return } - wireframe.showBalanceBreakdown( - from: view, + let params = LocksViewInput( prices: prices, balances: model.balances.values.compactMap { try? $0.get() }, chains: model.allChains, locks: locks, - crowdloans: crowdloans + externalBalances: externalBalances ) + + wireframe.showBalanceBreakdown(from: view, params: params) } func send() { diff --git a/novawallet/Modules/AssetList/AssetListProtocols.swift b/novawallet/Modules/AssetList/AssetListProtocols.swift index 8d7ac8254e..d67f579f85 100644 --- a/novawallet/Modules/AssetList/AssetListProtocols.swift +++ b/novawallet/Modules/AssetList/AssetListProtocols.swift @@ -60,14 +60,7 @@ protocol AssetListWireframeProtocol: AnyObject, WalletSwitchPresentable, AlertPr func showNfts(from view: AssetListViewProtocol?) - func showBalanceBreakdown( - from view: AssetListViewProtocol?, - prices: [ChainAssetId: PriceData], - balances: [AssetBalance], - chains: [ChainModel.Id: ChainModel], - locks: [AssetLock], - crowdloans: [ChainModel.Id: [CrowdloanContributionData]] - ) + func showBalanceBreakdown(from view: AssetListViewProtocol?, params: LocksViewInput) func showWalletConnect(from view: AssetListViewProtocol?) diff --git a/novawallet/Modules/AssetList/AssetListViewFactory.swift b/novawallet/Modules/AssetList/AssetListViewFactory.swift index e7eed5ff47..e6143ba21c 100644 --- a/novawallet/Modules/AssetList/AssetListViewFactory.swift +++ b/novawallet/Modules/AssetList/AssetListViewFactory.swift @@ -19,7 +19,7 @@ struct AssetListViewFactory { assetListObservable: assetListObservable, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, nftLocalSubscriptionFactory: NftLocalSubscriptionFactory.shared, - crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactory.shared, + externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, eventCenter: EventCenter.shared, settingsManager: SettingsManager.shared, diff --git a/novawallet/Modules/AssetList/AssetListWireframe.swift b/novawallet/Modules/AssetList/AssetListWireframe.swift index f77199d6a8..893fbeb295 100644 --- a/novawallet/Modules/AssetList/AssetListWireframe.swift +++ b/novawallet/Modules/AssetList/AssetListWireframe.swift @@ -124,22 +124,8 @@ final class AssetListWireframe: AssetListWireframeProtocol { view?.controller.navigationController?.pushViewController(nftListView.controller, animated: true) } - func showBalanceBreakdown( - from view: AssetListViewProtocol?, - prices: [ChainAssetId: PriceData], - balances: [AssetBalance], - chains: [ChainModel.Id: ChainModel], - locks: [AssetLock], - crowdloans: [ChainModel.Id: [CrowdloanContributionData]] - ) { - guard let viewController = LocksViewFactory.createView(input: - .init( - prices: prices, - balances: balances, - chains: chains, - locks: locks, - crowdloans: crowdloans - )) else { + func showBalanceBreakdown(from view: AssetListViewProtocol?, params: LocksViewInput) { + guard let viewController = LocksViewFactory.createView(input: params) else { return } diff --git a/novawallet/Modules/AssetList/Base/AssetListBaseBuilder.swift b/novawallet/Modules/AssetList/Base/AssetListBaseBuilder.swift index 7b9789cdae..034f5bc777 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBaseBuilder.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBaseBuilder.swift @@ -16,7 +16,7 @@ class AssetListBaseBuilder { 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>? + private(set) var externalBalancesResult: Result<[ChainAssetId: [ExternalAssetBalance]], Error>? private(set) var scheduler: Scheduler? @@ -65,7 +65,7 @@ class AssetListBaseBuilder { balances = [:] groups = AssetListModelHelpers.createGroupsDiffCalculator(from: []) groupLists = [:] - crowdloansResult = nil + externalBalancesResult = nil } private func updateAssetModels() { @@ -73,7 +73,7 @@ class AssetListBaseBuilder { priceResult: priceResult, balanceResults: balanceResults, allChains: allChains, - crowdloansResult: crowdloansResult + externalBalances: externalBalancesResult ) for chain in allChains.values { @@ -110,7 +110,7 @@ class AssetListBaseBuilder { priceResult: priceResult, balanceResults: balanceResults, allChains: allChains, - crowdloansResult: crowdloansResult + externalBalances: externalBalancesResult ) var groupChanges: [DataProviderChange] = [] @@ -176,7 +176,7 @@ class AssetListBaseBuilder { priceResult: priceResult, balanceResults: balanceResults, allChains: allChains, - crowdloansResult: crowdloansResult + externalBalances: externalBalancesResult ) let assetListModel = AssetListModelHelpers.createAssetModel( @@ -243,8 +243,8 @@ class AssetListBaseBuilder { updateAssetModels() } - private func processCrowdloans(_ result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) { - crowdloansResult = result + private func processExternalBalances(_ result: Result<[ChainAssetId: [ExternalAssetBalance]], Error>) { + externalBalancesResult = result updateAssetModels() } } @@ -272,9 +272,9 @@ extension AssetListBaseBuilder { } } - func applyCrowdloans(_ result: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>) { + func applyExternalBalances(_ result: Result<[ChainAssetId: [ExternalAssetBalance]], Error>) { workingQueue.async { [weak self] in - self?.processCrowdloans(result) + self?.processExternalBalances(result) self?.scheduleRebuildModel() } diff --git a/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift b/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift index dde1389f9d..e36bcf087b 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift @@ -10,16 +10,16 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip let selectedWalletSettings: SelectedWalletSettings let chainRegistry: ChainRegistryProtocol let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol - let crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol + let externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactoryProtocol 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 var externalBalancesSubscriptions: [ChainAssetId: StreamableProvider] = [:] + private var externalBalances: [ChainAssetId: [ExternalAssetBalance]] = [:] + private var externalBalancesChainAssetIds = Set() private(set) var priceSubscription: StreamableProvider? private(set) var availableTokenPrice: [ChainAssetId: AssetModel.PriceId] = [:] @@ -30,7 +30,7 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip selectedWalletSettings: SelectedWalletSettings, chainRegistry: ChainRegistryProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, - crowdloansLocalSubscriptionFactory: CrowdloanContributionLocalSubscriptionFactoryProtocol, + externalBalancesSubscriptionFactory: ExternalBalanceLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, currencyManager: CurrencyManagerProtocol, logger: LoggerProtocol? = nil @@ -38,7 +38,7 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip self.selectedWalletSettings = selectedWalletSettings self.chainRegistry = chainRegistry self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory - self.crowdloansLocalSubscriptionFactory = crowdloansLocalSubscriptionFactory + self.externalBalancesSubscriptionFactory = externalBalancesSubscriptionFactory self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory self.logger = logger self.currencyManager = currencyManager @@ -51,11 +51,11 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip assetBalanceIdMapping = [:] } - func clearCrowdloansSubscription() { - crowdloansSubscriptions.values.forEach { $0.removeObserver(self) } - crowdloansSubscriptions = [:] - crowdloans = [:] - crowdloanChainIds = .init() + func clearExternalBalancesSubscription() { + externalBalancesSubscriptions.values.forEach { $0.removeObserver(self) } + externalBalancesSubscriptions = [:] + externalBalances = [:] + externalBalancesChainAssetIds = .init() } private func convertToAccountDependentChanges( @@ -131,12 +131,12 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip updateAssetBalanceSubscription(from: enabledChainChanges) updatePriceSubscription(from: allChanges) - updateCrowdloansSubscription(from: Array(enabledChains.values)) + updateExternalBalancesSubscription(from: Array(enabledChains.values)) } func resetWallet() { clearAccountSubscriptions() - clearCrowdloansSubscription() + clearExternalBalancesSubscription() guard let selectedMetaAccount = selectedWalletSettings.value else { return @@ -166,7 +166,7 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip enabledChains = enabledChainChanges.mergeToDict(enabledChains) updateAssetBalanceSubscription(from: enabledChainChanges) - updateCrowdloansSubscription(from: Array(enabledChains.values)) + updateExternalBalancesSubscription(from: Array(enabledChains.values)) } func updateAssetBalanceSubscription(from changes: [DataProviderChange]) { @@ -310,29 +310,32 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip priceSubscription?.refresh() } - func updateCrowdloansSubscription(from allChains: [ChainModel]) { + func updateExternalBalancesSubscription(from allChains: [ChainModel]) { guard let selectedMetaAccount = selectedWalletSettings.value else { return } - let crowdloanChains = allChains.filter { $0.hasCrowdloans } - let newCrowdloanChainIds = Set(crowdloanChains.map(\.chainId)) + let chainAssets = allChains.flatMap { $0.chainAssetsWithExternalBalances() } + let newChainAssetIds = Set(chainAssets.map(\.chainAssetId)) - guard !crowdloanChains.isEmpty, crowdloanChainIds != newCrowdloanChainIds else { + guard !chainAssets.isEmpty, externalBalancesChainAssetIds != newChainAssetIds else { return } - clearCrowdloansSubscription() - crowdloanChainIds = newCrowdloanChainIds + clearExternalBalancesSubscription() + externalBalancesChainAssetIds = newChainAssetIds - crowdloanChains.forEach { chain in - let request = chain.accountRequest() + chainAssets.forEach { chainAsset in + let request = chainAsset.chain.accountRequest() guard let accountId = selectedMetaAccount.fetch(for: request)?.accountId else { return } - crowdloansSubscriptions[chain.identifier] = subscribeToCrowdloansProvider(for: accountId, chain: chain) + externalBalancesSubscriptions[chainAsset.chainAssetId] = subscribeToExternalAssetBalancesProvider( + for: accountId, + chainAsset: chainAsset + ) } } @@ -440,42 +443,42 @@ extension AssetListBaseInteractor { } } -extension AssetListBaseInteractor: CrowdloanContributionLocalSubscriptionHandler, CrowdloansLocalStorageSubscriber { - func handleCrowdloans( - result: Result<[DataProviderChange], Error>, +extension AssetListBaseInteractor: ExternalAssetBalanceSubscriptionHandler, ExternalAssetBalanceSubscriber { + func handleExternalAssetBalances( + result: Result<[DataProviderChange], Error>, accountId: AccountId, - chain: ChainModel + chainAsset: ChainAsset ) { guard let selectedMetaAccount = selectedWalletSettings.value else { return } guard let chainAccountId = selectedMetaAccount.fetch( - for: chain.accountRequest() + for: chainAsset.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" + "Missing account for chain: \(chainAsset.chain.name)" ) return } switch result { case let .failure(error): - baseBuilder?.applyCrowdloans(.failure(error)) + baseBuilder?.applyExternalBalances(.failure(error)) case let .success(changes): - crowdloans = changes.reduce( - into: crowdloans + externalBalances = changes.reduce( + into: externalBalances ) { 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 .insert(externalBalance), let .update(externalBalance): + var items = result[chainAsset.chainAssetId] ?? [] + items.addOrReplaceSingle(externalBalance) + result[chainAsset.chainAssetId] = items case let .delete(deletedIdentifier): - result[chain.chainId]?.removeAll(where: { $0.identifier == deletedIdentifier }) + result[chainAsset.chainAssetId]?.removeAll(where: { $0.identifier == deletedIdentifier }) } } - baseBuilder?.applyCrowdloans(.success(crowdloans)) + baseBuilder?.applyExternalBalances(.success(externalBalances)) } } } diff --git a/novawallet/Modules/AssetList/Models/AssetListAssetModel.swift b/novawallet/Modules/AssetList/Models/AssetListAssetModel.swift index d221549e2f..d4c42baca7 100644 --- a/novawallet/Modules/AssetList/Models/AssetListAssetModel.swift +++ b/novawallet/Modules/AssetList/Models/AssetListAssetModel.swift @@ -9,24 +9,24 @@ struct AssetListAssetModel: Identifiable { let balanceResult: Result? let balanceValue: Decimal? - let crowdloanResult: Result? - let crowdloanValue: Decimal? + let externalBalancesResult: Result? + let externalBalancesValue: Decimal? var totalAmount: BigUInt? { let maybeBalanceAmount = try? balanceResult?.get() - let maybeCrowdloanContribution = try? crowdloanResult?.get() - if let balanceAmount = maybeBalanceAmount, let crowdloanAmount = maybeCrowdloanContribution { - return balanceAmount + crowdloanAmount + let maybeExternalBalances = try? externalBalancesResult?.get() + if let balanceAmount = maybeBalanceAmount, let externalBalancesAmount = maybeExternalBalances { + return balanceAmount + externalBalancesAmount } else { - return maybeBalanceAmount ?? maybeCrowdloanContribution + return maybeBalanceAmount ?? maybeExternalBalances } } var totalValue: Decimal? { - if let balanceValue = balanceValue, let crowdloanValue = crowdloanValue { - return balanceValue + crowdloanValue + if let balanceValue = balanceValue, let externalBalancesValue = externalBalancesValue { + return balanceValue + externalBalancesValue } else { - return balanceValue ?? crowdloanValue + return balanceValue ?? externalBalancesValue } } } diff --git a/novawallet/Modules/AssetList/Models/AssetListBuilder.swift b/novawallet/Modules/AssetList/Models/AssetListBuilder.swift index 6a760321ce..99827c4764 100644 --- a/novawallet/Modules/AssetList/Models/AssetListBuilder.swift +++ b/novawallet/Modules/AssetList/Models/AssetListBuilder.swift @@ -32,7 +32,7 @@ final class AssetListBuilder: AssetListBaseBuilder { balanceResults: balanceResults, allChains: allChains, balances: balances, - crowdloansResult: crowdloansResult, + externalBalanceResult: externalBalancesResult, nfts: nftList.allItems, locksResult: locksResult ) diff --git a/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift b/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift index c4cf01cfd9..992c98ca31 100644 --- a/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift +++ b/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift @@ -9,7 +9,7 @@ struct AssetListBuilderResult { let balanceResults: [ChainAssetId: Result] let allChains: [ChainModel.Id: ChainModel] let balances: [ChainAssetId: Result] - let crowdloansResult: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>? + let externalBalanceResult: Result<[ChainAssetId: [ExternalAssetBalance]], Error>? let nfts: [NftModel] let locksResult: Result<[AssetLock], Error>? @@ -20,7 +20,7 @@ struct AssetListBuilderResult { balanceResults: [ChainAssetId: Result] = [:], allChains: [ChainModel.Id: ChainModel] = [:], balances: [ChainAssetId: Result] = [:], - crowdloansResult: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>? = nil, + externalBalanceResult: Result<[ChainAssetId: [ExternalAssetBalance]], Error>? = nil, nfts: [NftModel] = [], locksResult: Result<[AssetLock], Error>? = nil ) { @@ -30,7 +30,7 @@ struct AssetListBuilderResult { self.balanceResults = balanceResults self.allChains = allChains self.balances = balances - self.crowdloansResult = crowdloansResult + self.externalBalanceResult = externalBalanceResult self.nfts = nfts self.locksResult = locksResult } @@ -43,7 +43,7 @@ struct AssetListBuilderResult { balanceResults: balanceResults, allChains: allChains, balances: balances, - crowdloansResult: crowdloansResult, + externalBalanceResult: externalBalanceResult, nfts: nfts, locksResult: locksResult ) diff --git a/novawallet/Modules/AssetList/Models/AssetListModelHelpers.swift b/novawallet/Modules/AssetList/Models/AssetListModelHelpers.swift index 03020f5e5a..39d1f4d811 100644 --- a/novawallet/Modules/AssetList/Models/AssetListModelHelpers.swift +++ b/novawallet/Modules/AssetList/Models/AssetListModelHelpers.swift @@ -103,11 +103,11 @@ enum AssetListModelHelpers { } }() - let crowdloanContributionsResult: Result? = { + let externalBalancesContributionResult: Result? = { do { - let allContributions = try state.crowdloansResult?.get() + let allContributions = try state.externalBalances?.get() - let contribution = allContributions?[chainModel.chainId]?.reduce(BigUInt(0)) { accum, contribution in + let contribution = allContributions?[chainAssetId]?.reduce(BigUInt(0)) { accum, contribution in accum + contribution.amount } @@ -117,8 +117,8 @@ enum AssetListModelHelpers { } }() - let maybeCrowdloanContributions: Decimal? = { - if let contributions = try? crowdloanContributionsResult?.get() { + let maybeExternalBalanceContributions: Decimal? = { + if let contributions = try? externalBalancesContributionResult?.get() { return Decimal.fromSubstrateAmount( contributions, precision: Int16(bitPattern: assetModel.precision) @@ -144,9 +144,9 @@ enum AssetListModelHelpers { } }() - let crowdloanContributionsValue: Decimal? = { - if let crowdloanContributions = maybeCrowdloanContributions, let price = maybePrice { - return crowdloanContributions * price + let externalBalanceContributionsValue: Decimal? = { + if let externalBalanceContributions = maybeExternalBalanceContributions, let price = maybePrice { + return externalBalanceContributions * price } else { return nil } @@ -156,8 +156,8 @@ enum AssetListModelHelpers { assetModel: assetModel, balanceResult: balanceResult, balanceValue: balanceValue, - crowdloanResult: crowdloanContributionsResult, - crowdloanValue: crowdloanContributionsValue + externalBalancesResult: externalBalancesContributionResult, + externalBalancesValue: externalBalanceContributionsValue ) } } diff --git a/novawallet/Modules/AssetList/Models/AssetListState.swift b/novawallet/Modules/AssetList/Models/AssetListState.swift index 3c8a28dd56..1f77e33160 100644 --- a/novawallet/Modules/AssetList/Models/AssetListState.swift +++ b/novawallet/Modules/AssetList/Models/AssetListState.swift @@ -5,25 +5,25 @@ struct AssetListState { let priceResult: Result<[ChainAssetId: PriceData], Error>? let balanceResults: [ChainAssetId: Result] let allChains: [ChainModel.Id: ChainModel] - let crowdloansResult: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>? + let externalBalances: Result<[ChainAssetId: [ExternalAssetBalance]], Error>? init( priceResult: Result<[ChainAssetId: PriceData], Error>? = nil, balanceResults: [ChainAssetId: Result] = [:], allChains: [ChainModel.Id: ChainModel] = [:], - crowdloansResult: Result<[ChainModel.Id: [CrowdloanContributionData]], Error>? = nil + externalBalances: Result<[ChainAssetId: [ExternalAssetBalance]], Error>? = nil ) { self.priceResult = priceResult self.balanceResults = balanceResults self.allChains = allChains - self.crowdloansResult = crowdloansResult + self.externalBalances = externalBalances } init(model: AssetListBuilderResult.Model) { priceResult = model.priceResult balanceResults = model.balanceResults allChains = model.allChains - crowdloansResult = model.crowdloansResult + externalBalances = model.externalBalanceResult } func chainAsset(for chainAssetId: ChainAssetId) -> ChainAsset? { diff --git a/novawallet/Modules/AssetList/Models/LoadableViewModelState+Addition.swift b/novawallet/Modules/AssetList/Models/LoadableViewModelState+Addition.swift index 118369b995..32c403ca54 100644 --- a/novawallet/Modules/AssetList/Models/LoadableViewModelState+Addition.swift +++ b/novawallet/Modules/AssetList/Models/LoadableViewModelState+Addition.swift @@ -23,3 +23,20 @@ extension LoadableViewModelState { } } } + +extension LoadableViewModelState where T: RandomAccessCollection & MutableCollection { + mutating func insert(newElement element: T.Element, at index: T.Index) { + switch self { + case .loading: + return + case let .cached(value): + var updatingValue = value + updatingValue[index] = element + self = .cached(value: updatingValue) + case let .loaded(value): + var updatingValue = value + updatingValue[index] = element + self = .loaded(value: updatingValue) + } + } +} diff --git a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserPresenter.swift b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserPresenter.swift index 43fa98aad7..9b768129c4 100644 --- a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserPresenter.swift +++ b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserPresenter.swift @@ -110,7 +110,7 @@ extension DAppBrowserPresenter: DAppBrowserPresenterProtocol { let viewModel = AlertPresentableViewModel( title: nil, - message: R.string.localizable.dappBrowserCloseConfirmation(preferredLanguages: languages), + message: R.string.localizable.commonCloseWhenChangesConfirmation(preferredLanguages: languages), actions: [closeViewModel], closeAction: R.string.localizable.commonCancel(preferredLanguages: languages) ) diff --git a/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift index 70fbb0fbc9..e0f92f391f 100644 --- a/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift +++ b/novawallet/Modules/Locks/LocksBalanceViewModelFactory.swift @@ -6,9 +6,10 @@ protocol LocksBalanceViewModelFactoryProtocol { balances: [AssetBalance], chains: [ChainModel.Id: ChainModel], prices: [ChainAssetId: PriceData], - crowdloans: [ChainModel.Id: [CrowdloanContributionData]], + externalBalances: [ChainAssetId: [ExternalAssetBalance]], locale: Locale ) -> FormattedBalance + func formatPlankValue( plank: BigUInt, chainAssetId: ChainAssetId, @@ -53,7 +54,7 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { balances: [AssetBalance], chains: [ChainModel.Id: ChainModel], prices: [ChainAssetId: PriceData], - crowdloans: [ChainModel.Id: [CrowdloanContributionData]], + externalBalances: [ChainAssetId: [ExternalAssetBalance]], locale: Locale ) -> FormattedBalance { var totalPrice: Decimal = 0 @@ -91,27 +92,27 @@ final class LocksBalanceViewModelFactory: LocksBalanceViewModelFactoryProtocol { lastPriceData = priceData } - let crowdloansTotalPrice: Decimal = crowdloans.reduce(0) { result, crowdloan in - guard let asset = chains[crowdloan.key]?.utilityAsset() else { + let externalBalanceTotalPrice: Decimal = externalBalances.reduce(0) { result, externalBalance in + guard let asset = chains[externalBalance.key.chainId]?.asset(for: externalBalance.key.assetId) else { return result } - let priceData = prices[.init(chainId: crowdloan.key, assetId: asset.assetId)] + let priceData = prices[externalBalance.key] let rate = priceData.map { Decimal(string: $0.price) ?? 0 } ?? 0 return result + calculateAmount( - from: crowdloan.value.reduce(0) { $0 + $1.amount }, + from: externalBalance.value.reduce(0) { $0 + $1.amount }, precision: asset.precision, rate: rate ) } - let total = totalPrice + crowdloansTotalPrice + let total = totalPrice + externalBalanceTotalPrice let formattedTotal = formatPrice( amount: total, priceData: lastPriceData, locale: locale ) let formattedTransferrable = formatPrice(amount: transferrablePrice, priceData: lastPriceData, locale: locale) - let totalLocks = locksPrice + crowdloansTotalPrice + let totalLocks = locksPrice + externalBalanceTotalPrice let formattedLocks = formatPrice( amount: totalLocks, priceData: lastPriceData, diff --git a/novawallet/Modules/Locks/LocksPresenter.swift b/novawallet/Modules/Locks/LocksPresenter.swift index d42cc2be70..c7291b41cc 100644 --- a/novawallet/Modules/Locks/LocksPresenter.swift +++ b/novawallet/Modules/Locks/LocksPresenter.swift @@ -30,7 +30,7 @@ final class LocksPresenter { balances: input.balances, chains: input.chains, prices: input.prices, - crowdloans: input.crowdloans, + externalBalances: input.externalBalances, locale: selectedLocale ) @@ -104,21 +104,23 @@ final class LocksPresenter { ) } - let crowdloanCells: [LocksViewSectionModel.CellViewModel] = input.crowdloans.compactMap { - guard let utilityAsset = input.chains[$0.key]?.utilityAsset() else { - return nil - } + let groupedExternalBalances = input.externalBalances + .values.flatMap { $0.filter { $0.amount > 0 } } + .groupByAssetType() + + let externalBalanceCells: [LocksViewSectionModel.CellViewModel] = groupedExternalBalances.compactMap { + let group = $0.key + let amount = $0.value + 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 + amountInPlank: amount, + chainAssetId: group.chainAssetId, + title: group.type.lockTitle.value(for: selectedLocale), + identifier: group.stringValue ) } - return locksCells + reservedCells + crowdloanCells + return locksCells + reservedCells + externalBalanceCells } private func createCell( @@ -164,12 +166,14 @@ final class LocksPresenter { let locksCellsCount = input.locks.filter { $0.amount > 0 }.count - let crowdloanCellsCount = input.crowdloans.filter { crowdloan in - crowdloan.value.first(where: { $0.amount > 0 }) != nil - }.count + + let externalBalancesCellsCount = input.externalBalances + .values.flatMap { $0.filter { $0.amount > 0 } } + .count + return view?.calculateEstimatedHeight( sections: 2, - items: locksCellsCount + reservedCellsCount + crowdloanCellsCount + items: locksCellsCount + reservedCellsCount + externalBalancesCellsCount ) ?? 0 } } diff --git a/novawallet/Modules/Locks/LocksViewInput.swift b/novawallet/Modules/Locks/LocksViewInput.swift index 8154f2c3ea..66507af3ca 100644 --- a/novawallet/Modules/Locks/LocksViewInput.swift +++ b/novawallet/Modules/Locks/LocksViewInput.swift @@ -3,5 +3,5 @@ struct LocksViewInput { let balances: [AssetBalance] let chains: [ChainModel.Id: ChainModel] let locks: [AssetLock] - let crowdloans: [ChainModel.Id: [CrowdloanContributionData]] + let externalBalances: [ChainAssetId: [ExternalAssetBalance]] } diff --git a/novawallet/Modules/OperationDetails/Model/OperationDetailsModel.swift b/novawallet/Modules/OperationDetails/Model/OperationDetailsModel.swift index 42d3d7ee71..8f33115776 100644 --- a/novawallet/Modules/OperationDetails/Model/OperationDetailsModel.swift +++ b/novawallet/Modules/OperationDetails/Model/OperationDetailsModel.swift @@ -9,10 +9,12 @@ struct OperationDetailsModel { enum OperationData { case transfer(_ model: OperationTransferModel) - case reward(_ model: OperationRewardModel) - case slash(_ model: OperationSlashModel) + case reward(_ model: OperationRewardOrSlashModel) + case slash(_ model: OperationRewardOrSlashModel) case extrinsic(_ model: OperationExtrinsicModel) case contract(_ model: OperationContractCallModel) + case poolReward(_ model: OperationPoolRewardOrSlashModel) + case poolSlash(_ model: OperationPoolRewardOrSlashModel) } let time: Date diff --git a/novawallet/Modules/OperationDetails/Model/OperationPoolRewardOrSlashModel.swift b/novawallet/Modules/OperationDetails/Model/OperationPoolRewardOrSlashModel.swift new file mode 100644 index 0000000000..52db1842e9 --- /dev/null +++ b/novawallet/Modules/OperationDetails/Model/OperationPoolRewardOrSlashModel.swift @@ -0,0 +1,20 @@ +import Foundation +import BigInt + +struct OperationPoolRewardOrSlashModel { + let eventId: String + let amount: BigUInt + let priceData: PriceData? + let pool: NominationPools.SelectedPool? +} + +extension OperationPoolRewardOrSlashModel { + func byReplacingPool(_ newPool: NominationPools.SelectedPool) -> OperationPoolRewardOrSlashModel { + .init( + eventId: eventId, + amount: amount, + priceData: priceData, + pool: newPool + ) + } +} diff --git a/novawallet/Modules/OperationDetails/Model/OperationRewardModel.swift b/novawallet/Modules/OperationDetails/Model/OperationRewardOrSlashModel.swift similarity index 81% rename from novawallet/Modules/OperationDetails/Model/OperationRewardModel.swift rename to novawallet/Modules/OperationDetails/Model/OperationRewardOrSlashModel.swift index fb82afd89d..872da4ee8a 100644 --- a/novawallet/Modules/OperationDetails/Model/OperationRewardModel.swift +++ b/novawallet/Modules/OperationDetails/Model/OperationRewardOrSlashModel.swift @@ -1,7 +1,7 @@ import Foundation import BigInt -struct OperationRewardModel { +struct OperationRewardOrSlashModel { let eventId: String let amount: BigUInt let priceData: PriceData? diff --git a/novawallet/Modules/OperationDetails/Model/OperationSlashModel.swift b/novawallet/Modules/OperationDetails/Model/OperationSlashModel.swift deleted file mode 100644 index 7a92a3346a..0000000000 --- a/novawallet/Modules/OperationDetails/Model/OperationSlashModel.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation -import BigInt - -struct OperationSlashModel { - let eventId: String - let amount: BigUInt - let priceData: PriceData? - let validator: DisplayAddress? - let era: Int? -} diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsBaseProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsBaseProvider.swift new file mode 100644 index 0000000000..329afbbf49 --- /dev/null +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsBaseProvider.swift @@ -0,0 +1,25 @@ +import Foundation + +class OperationDetailsBaseProvider { + let selectedAccount: MetaChainAccountResponse + let chainAsset: ChainAsset + let transaction: TransactionHistoryItem + + var accountAddress: AccountAddress? { + selectedAccount.chainAccount.toAddress() + } + + var chain: ChainModel { + chainAsset.chain + } + + init( + selectedAccount: MetaChainAccountResponse, + chainAsset: ChainAsset, + transaction: TransactionHistoryItem + ) { + self.selectedAccount = selectedAccount + self.chainAsset = chainAsset + self.transaction = transaction + } +} diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsContractProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsContractProvider.swift new file mode 100644 index 0000000000..fae701845f --- /dev/null +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsContractProvider.swift @@ -0,0 +1,42 @@ +import Foundation +import BigInt + +final class OperationDetailsContractProvider: OperationDetailsBaseProvider {} + +extension OperationDetailsContractProvider: OperationDetailsDataProviderProtocol { + func extractOperationData( + replacingWith newFee: BigUInt?, + priceCalculator _: TokenPriceCalculatorProtocol?, + feePriceCalculator: TokenPriceCalculatorProtocol?, + progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void + ) { + let fee: BigUInt = newFee ?? transaction.feeInPlankIntOrZero + let feePriceData = feePriceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { + PriceData.amount($0) + } + + guard let currentAccountAddress = accountAddress else { + progressClosure(nil) + return + } + + let currentDisplayAddress = DisplayAddress( + address: currentAccountAddress, + username: selectedAccount.chainAccount.name + ) + + let contractAddress = transaction.receiver.flatMap { try? Data(hex: $0).toAddress(using: chain.chainFormat) } + let contractDisplayAddress = DisplayAddress(address: contractAddress ?? "", username: "") + + let model = OperationContractCallModel( + txHash: transaction.txHash, + fee: fee, + feePriceData: feePriceData, + sender: currentDisplayAddress, + contract: contractDisplayAddress, + functionName: transaction.evmContractFunctionName + ) + + progressClosure(.contract(model)) + } +} diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderFactory.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderFactory.swift new file mode 100644 index 0000000000..01b16b4cb7 --- /dev/null +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderFactory.swift @@ -0,0 +1,97 @@ +import Foundation + +protocol OperationDetailsDataProviderFactoryProtocol { + func createProvider(for transaction: TransactionHistoryItem) -> OperationDetailsDataProviderProtocol? +} + +final class OperationDetailsDataProviderFactory { + let selectedAccount: MetaChainAccountResponse + let chainAsset: ChainAsset + let chainRegistry: ChainRegistryProtocol + let accountRepositoryFactory: AccountRepositoryFactoryProtocol + let operationQueue: OperationQueue + + init( + selectedAccount: MetaChainAccountResponse, + chainAsset: ChainAsset, + chainRegistry: ChainRegistryProtocol, + accountRepositoryFactory: AccountRepositoryFactoryProtocol, + operationQueue: OperationQueue + ) { + self.selectedAccount = selectedAccount + self.chainAsset = chainAsset + self.chainRegistry = chainRegistry + self.accountRepositoryFactory = accountRepositoryFactory + self.operationQueue = operationQueue + } +} + +extension OperationDetailsDataProviderFactory: OperationDetailsDataProviderFactoryProtocol { + // swiftlint:disable:next function_body_length + func createProvider(for transaction: TransactionHistoryItem) -> OperationDetailsDataProviderProtocol? { + guard + let address = selectedAccount.chainAccount.toAddress(), + let transactionType = transaction.type(for: address) else { + return nil + } + + switch transactionType { + case .incoming, .outgoing: + let walletRepository = accountRepositoryFactory.createMetaAccountRepository( + for: nil, + sortDescriptors: [] + ) + + return OperationDetailsTransferProvider( + selectedAccount: selectedAccount, + chainAsset: chainAsset, + transaction: transaction, + walletRepository: walletRepository, + operationQueue: operationQueue + ) + case .reward, .slash: + let walletRepository = accountRepositoryFactory.createMetaAccountRepository( + for: nil, + sortDescriptors: [] + ) + + return OperationDetailsDirectStakingProvider( + selectedAccount: selectedAccount, + chainAsset: chainAsset, + transaction: transaction, + walletRepository: walletRepository, + operationQueue: operationQueue + ) + case .extrinsic: + if chainAsset.asset.isEvmNative { + return OperationDetailsContractProvider( + selectedAccount: selectedAccount, + chainAsset: chainAsset, + transaction: transaction + ) + } else { + return OperationDetailsExtrinsicProvider( + selectedAccount: selectedAccount, + chainAsset: chainAsset, + transaction: transaction + ) + } + case .poolReward, .poolSlash: + guard + let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { + return nil + } + + return OperationDetailsPoolStakingProvider( + selectedAccount: selectedAccount, + chainAsset: chainAsset, + transaction: transaction, + poolsOperationFactory: NominationPoolsOperationFactory(operationQueue: operationQueue), + connection: connection, + runtimeService: runtimeService, + operationQueue: operationQueue + ) + } + } +} diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderProtocol.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderProtocol.swift new file mode 100644 index 0000000000..5f57292f28 --- /dev/null +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDataProviderProtocol.swift @@ -0,0 +1,11 @@ +import Foundation +import BigInt + +protocol OperationDetailsDataProviderProtocol { + func extractOperationData( + replacingWith newFee: BigUInt?, + priceCalculator: TokenPriceCalculatorProtocol?, + feePriceCalculator: TokenPriceCalculatorProtocol?, + progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void + ) +} diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDirectStakingProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDirectStakingProvider.swift new file mode 100644 index 0000000000..e8a816a78a --- /dev/null +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsDirectStakingProvider.swift @@ -0,0 +1,104 @@ +import Foundation +import RobinHood +import BigInt + +final class OperationDetailsDirectStakingProvider: OperationDetailsBaseProvider, AccountFetching { + let walletRepository: AnyDataProviderRepository + let operationQueue: OperationQueue + + init( + selectedAccount: MetaChainAccountResponse, + chainAsset: ChainAsset, + transaction: TransactionHistoryItem, + walletRepository: AnyDataProviderRepository, + operationQueue: OperationQueue + ) { + self.walletRepository = walletRepository + self.operationQueue = operationQueue + + super.init( + selectedAccount: selectedAccount, + chainAsset: chainAsset, + transaction: transaction + ) + } + + private func complete( + for model: OperationRewardOrSlashModel, + completion: @escaping (OperationDetailsModel.OperationData?) -> Void + ) { + guard let accountAddress = accountAddress else { + completion(.reward(model)) + return + } + + let isReward = transaction.type(for: accountAddress) == .reward + + if isReward { + completion(.reward(model)) + } else { + completion(.slash(model)) + } + } + + private func getEventId(from context: HistoryRewardContext?) -> String? { + guard let eventId = context?.eventId else { + return nil + } + return !eventId.isEmpty ? eventId : nil + } +} + +extension OperationDetailsDirectStakingProvider: OperationDetailsDataProviderProtocol { + func extractOperationData( + replacingWith _: BigUInt?, + priceCalculator: TokenPriceCalculatorProtocol?, + feePriceCalculator _: TokenPriceCalculatorProtocol?, + progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void + ) { + let context = try? transaction.call.map { + try JSONDecoder().decode(HistoryRewardContext.self, from: $0) + } + + let eventId = getEventId(from: context) ?? transaction.txHash + + let amount = transaction.amountInPlankIntOrZero + let priceData = priceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { + PriceData.amount($0) + } + + if let validatorId = try? transaction.sender.toAccountId() { + _ = fetchDisplayAddress( + for: [validatorId], + chain: chain, + repository: walletRepository, + operationQueue: operationQueue + ) { [weak self] result in + switch result { + case let .success(addresses): + let model = OperationRewardOrSlashModel( + eventId: eventId, + amount: amount, + priceData: priceData, + validator: addresses.first, + era: context?.era + ) + + self?.complete(for: model, completion: progressClosure) + case .failure: + progressClosure(nil) + } + } + } else { + let model = OperationRewardOrSlashModel( + eventId: eventId, + amount: amount, + priceData: priceData, + validator: nil, + era: context?.era + ) + + complete(for: model, completion: progressClosure) + } + } +} diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsExtrinsicProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsExtrinsicProvider.swift new file mode 100644 index 0000000000..fcb1a38b41 --- /dev/null +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsExtrinsicProvider.swift @@ -0,0 +1,39 @@ +import Foundation +import BigInt + +final class OperationDetailsExtrinsicProvider: OperationDetailsBaseProvider {} + +extension OperationDetailsExtrinsicProvider: OperationDetailsDataProviderProtocol { + func extractOperationData( + replacingWith newFee: BigUInt?, + priceCalculator _: TokenPriceCalculatorProtocol?, + feePriceCalculator: TokenPriceCalculatorProtocol?, + progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void + ) { + guard let accountAddress = accountAddress else { + progressClosure(nil) + return + } + + let fee = newFee ?? transaction.feeInPlankIntOrZero + let feePriceData = feePriceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { + PriceData.amount($0) + } + + let currentDisplayAddress = DisplayAddress( + address: accountAddress, + username: selectedAccount.chainAccount.name + ) + + let model = OperationExtrinsicModel( + txHash: transaction.txHash, + call: transaction.callPath.callName, + module: transaction.callPath.moduleName, + sender: currentDisplayAddress, + fee: fee, + feePriceData: feePriceData + ) + + progressClosure(.extrinsic(model)) + } +} diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsPoolStakingProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsPoolStakingProvider.swift new file mode 100644 index 0000000000..2be201ea03 --- /dev/null +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsPoolStakingProvider.swift @@ -0,0 +1,168 @@ +import Foundation +import RobinHood +import SubstrateSdk +import BigInt + +final class OperationDetailsPoolStakingProvider: OperationDetailsBaseProvider, AnyCancellableCleaning { + let poolsOperationFactory: NominationPoolsOperationFactoryProtocol + let connection: JSONRPCEngine + let runtimeService: RuntimeCodingServiceProtocol + let operationQueue: OperationQueue + + private var selectedPool: NominationPools.SelectedPool? + + private var cancellableCall: CancellableCall? + + init( + selectedAccount: MetaChainAccountResponse, + chainAsset: ChainAsset, + transaction: TransactionHistoryItem, + poolsOperationFactory: NominationPoolsOperationFactoryProtocol, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue + ) { + self.poolsOperationFactory = poolsOperationFactory + self.connection = connection + self.runtimeService = runtimeService + self.operationQueue = operationQueue + + super.init( + selectedAccount: selectedAccount, + chainAsset: chainAsset, + transaction: transaction + ) + } + + deinit { + clear(cancellable: &cancellableCall) + } + + private func reportProgress( + for model: OperationPoolRewardOrSlashModel, + progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void + ) { + selectedPool = model.pool + + guard let accountAddress = accountAddress else { + progressClosure(.poolReward(model)) + return + } + + let isReward = transaction.type(for: accountAddress) == .poolReward + + if isReward { + progressClosure(.poolReward(model)) + } else { + progressClosure(.poolSlash(model)) + } + } + + private func getEventId(from context: HistoryPoolRewardContext?) -> String? { + guard let eventId = context?.eventId else { + return nil + } + return !eventId.isEmpty ? eventId : nil + } + + private func fetchPool( + for poolId: NominationPools.PoolId, + waitingModel: OperationPoolRewardOrSlashModel, + completion: @escaping (OperationDetailsModel.OperationData?) -> Void + ) { + clear(cancellable: &cancellableCall) + + let bondedPoolWrapper = poolsOperationFactory.createBondedAccountsWrapper( + for: { [poolId] }, + runtimeService: runtimeService + ) + let metadataWrapper = poolsOperationFactory.createMetadataWrapper( + for: { [poolId] }, + connection: connection, + runtimeService: runtimeService + ) + let mergeOperation = ClosureOperation { + let bondedPools = try bondedPoolWrapper.targetOperation.extractNoCancellableResultData() + let metadataResult = try metadataWrapper.targetOperation.extractNoCancellableResultData() + guard let bondedAccountId = bondedPools[poolId] else { + return waitingModel + } + + let metadata = metadataResult.first?.value + + let pool = NominationPools.SelectedPool( + poolId: poolId, + bondedAccountId: bondedAccountId, + metadata: metadata, + maxApy: nil + ) + + return waitingModel.byReplacingPool(pool) + } + + mergeOperation.addDependency(bondedPoolWrapper.targetOperation) + mergeOperation.addDependency(metadataWrapper.targetOperation) + + let dependencies = bondedPoolWrapper.allOperations + metadataWrapper.allOperations + + let wrapper = CompoundOperationWrapper(targetOperation: mergeOperation, dependencies: dependencies) + + mergeOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard self?.cancellableCall === wrapper else { + return + } + + self?.cancellableCall = nil + + do { + let model = try mergeOperation.extractNoCancellableResultData() + self?.reportProgress(for: model, progressClosure: completion) + } catch { + completion(nil) + } + } + } + + cancellableCall = wrapper + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } +} + +extension OperationDetailsPoolStakingProvider: OperationDetailsDataProviderProtocol { + func extractOperationData( + replacingWith _: BigUInt?, + priceCalculator: TokenPriceCalculatorProtocol?, + feePriceCalculator _: TokenPriceCalculatorProtocol?, + progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void + ) { + let optContext = try? transaction.call.map { + try JSONDecoder().decode(HistoryPoolRewardContext.self, from: $0) + } + + let eventId = getEventId(from: optContext) ?? transaction.txHash + let amount = transaction.amountInPlankIntOrZero + let priceData = priceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { + PriceData.amount($0) + } + + let model = OperationPoolRewardOrSlashModel( + eventId: eventId, + amount: amount, + priceData: priceData, + pool: nil + ) + + if let selectedPool = selectedPool { + reportProgress(for: model.byReplacingPool(selectedPool), progressClosure: progressClosure) + } else if let poolId = optContext?.poolId { + // send partial model to display while loading pool's metadata + reportProgress(for: model, progressClosure: progressClosure) + + fetchPool(for: poolId, waitingModel: model, completion: progressClosure) + } else { + reportProgress(for: model, progressClosure: progressClosure) + } + } +} diff --git a/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsTransferProvider.swift b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsTransferProvider.swift new file mode 100644 index 0000000000..546002dd27 --- /dev/null +++ b/novawallet/Modules/OperationDetails/OperationDataProviders/OperationDetailsTransferProvider.swift @@ -0,0 +1,96 @@ +import Foundation +import BigInt +import RobinHood + +final class OperationDetailsTransferProvider: OperationDetailsBaseProvider, AccountFetching { + let walletRepository: AnyDataProviderRepository + let operationQueue: OperationQueue + + init( + selectedAccount: MetaChainAccountResponse, + chainAsset: ChainAsset, + transaction: TransactionHistoryItem, + walletRepository: AnyDataProviderRepository, + operationQueue: OperationQueue + ) { + self.walletRepository = walletRepository + self.operationQueue = operationQueue + + super.init( + selectedAccount: selectedAccount, + chainAsset: chainAsset, + transaction: transaction + ) + } +} + +extension OperationDetailsTransferProvider: OperationDetailsDataProviderProtocol { + func extractOperationData( + replacingWith newFee: BigUInt?, + priceCalculator: TokenPriceCalculatorProtocol?, + feePriceCalculator: TokenPriceCalculatorProtocol?, + progressClosure: @escaping (OperationDetailsModel.OperationData?) -> Void + ) { + guard let accountAddress = accountAddress else { + progressClosure(nil) + return + } + let peerAddress = (transaction.sender == accountAddress ? transaction.receiver : transaction.sender) + ?? transaction.sender + let accountId = try? peerAddress.toAccountId(using: chain.chainFormat) + let peerId = accountId?.toHex() ?? peerAddress + + guard let peerId = try? Data(hexString: peerId) else { + progressClosure(nil) + return + } + + let isOutgoing = transaction.type(for: accountAddress) == .outgoing + let amount = transaction.amountInPlankIntOrZero + let priceData = priceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { + PriceData.amount($0) + } + + let fee = newFee ?? transaction.feeInPlankIntOrZero + let feePriceData = feePriceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { + PriceData.amount($0) + } + + let currentDisplayAddress = DisplayAddress( + address: accountAddress, + username: selectedAccount.chainAccount.name + ) + + let txId = transaction.txHash + + _ = fetchDisplayAddress( + for: [peerId], + chain: chain, + repository: walletRepository, + operationQueue: operationQueue + ) { result in + switch result { + case let .success(otherDisplayAddresses): + if let otherDisplayAddress = otherDisplayAddresses.first { + let model = OperationTransferModel( + txHash: txId, + amount: amount, + amountPriceData: priceData, + fee: fee, + feePriceData: feePriceData, + sender: isOutgoing ? currentDisplayAddress : otherDisplayAddress, + receiver: isOutgoing ? otherDisplayAddress : currentDisplayAddress, + outgoing: isOutgoing + ) + + progressClosure(.transfer(model)) + } else { + progressClosure(nil) + } + + case .failure: + progressClosure(nil) + } + } + } +} diff --git a/novawallet/Modules/OperationDetails/OperationDetailsInteractor.swift b/novawallet/Modules/OperationDetails/OperationDetailsInteractor.swift index 60a51e1738..2be1c8bba8 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsInteractor.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsInteractor.swift @@ -1,5 +1,4 @@ -import UIKit - +import Foundation import BigInt import RobinHood @@ -7,7 +6,7 @@ enum OperationDetailsInteractorError: Error { case unsupportTxType } -final class OperationDetailsInteractor: AccountFetching { +final class OperationDetailsInteractor: AccountFetching, AnyCancellableCleaning { weak var presenter: OperationDetailsInteractorOutputProtocol? let transaction: TransactionHistoryItem @@ -15,15 +14,9 @@ final class OperationDetailsInteractor: AccountFetching { var chain: ChainModel { chainAsset.chain } - let walletRepository: AnyDataProviderRepository let transactionLocalSubscriptionFactory: TransactionLocalSubscriptionFactoryProtocol - let operationQueue: OperationQueue - let wallet: MetaAccountModel let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol - - private var accountAddress: AccountAddress? { - wallet.fetch(for: chain.accountRequest())?.toAddress() - } + let operationDataProvider: OperationDetailsDataProviderProtocol private var transactionProvider: StreamableProvider? private var priceProvider: AnySingleValueProvider? @@ -35,20 +28,16 @@ final class OperationDetailsInteractor: AccountFetching { init( transaction: TransactionHistoryItem, chainAsset: ChainAsset, - wallet: MetaAccountModel, - walletRepository: AnyDataProviderRepository, transactionLocalSubscriptionFactory: TransactionLocalSubscriptionFactoryProtocol, - operationQueue: OperationQueue, currencyManager: CurrencyManagerProtocol, - priceLocalSubscriptionFactory: PriceProviderFactoryProtocol + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + operationDataProvider: OperationDetailsDataProviderProtocol ) { self.transaction = transaction self.chainAsset = chainAsset - self.wallet = wallet - self.walletRepository = walletRepository self.transactionLocalSubscriptionFactory = transactionLocalSubscriptionFactory - self.operationQueue = operationQueue self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory + self.operationDataProvider = operationDataProvider self.currencyManager = currencyManager } @@ -69,271 +58,16 @@ final class OperationDetailsInteractor: AccountFetching { } } - private func extractSlashOperationData( - _ completion: @escaping (OperationDetailsModel.OperationData?) -> Void - ) { - let context = try? transaction.call.map { - try JSONDecoder().decode(HistoryRewardContext.self, from: $0) - } - - let eventId = getEventId(from: context) ?? transaction.txHash - - let amount = transaction.amountInPlankIntOrZero - let priceData = priceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { - PriceData.amount($0) - } - - if let validatorId = try? transaction.sender.toAccountId() { - _ = fetchDisplayAddress( - for: [validatorId], - chain: chain, - repository: walletRepository, - operationQueue: operationQueue - ) { result in - switch result { - case let .success(addresses): - let model = OperationSlashModel( - eventId: eventId, - amount: amount, - priceData: priceData, - validator: addresses.first, - era: context?.era - ) - - completion(.slash(model)) - case .failure: - completion(nil) - } - } - } else { - let model = OperationSlashModel( - eventId: eventId, - amount: amount, - priceData: priceData, - validator: nil, - era: context?.era - ) - - completion(.slash(model)) - } - } - - private func extractRewardOperationData( - _ completion: @escaping (OperationDetailsModel.OperationData?) -> Void - ) { - let context = try? transaction.call.map { - try JSONDecoder().decode(HistoryRewardContext.self, from: $0) - } - - let eventId = getEventId(from: context) ?? transaction.txHash - - let amount = transaction.amountInPlankIntOrZero - let priceData = priceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { - PriceData.amount($0) - } - - if let validatorId = try? transaction.sender.toAccountId() { - _ = fetchDisplayAddress( - for: [validatorId], - chain: chain, - repository: walletRepository, - operationQueue: operationQueue - ) { result in - switch result { - case let .success(addresses): - let model = OperationRewardModel( - eventId: eventId, - amount: amount, - priceData: priceData, - validator: addresses.first, - era: context?.era - ) - - completion(.reward(model)) - case .failure: - completion(nil) - } - } - } else { - let model = OperationRewardModel( - eventId: eventId, - amount: amount, - priceData: priceData, - validator: nil, - era: context?.era - ) - - completion(.reward(model)) - } - } - - private func getEventId(from context: HistoryRewardContext?) -> String? { - guard let eventId = context?.eventId else { - return nil - } - return !eventId.isEmpty ? eventId : nil - } - - private func extractExtrinsicOperationData( - newFee: BigUInt?, - _ completion: @escaping (OperationDetailsModel.OperationData?) -> Void - ) { - guard let accountAddress = accountAddress else { - completion(nil) - return - } - - let fee = newFee ?? transaction.feeInPlankIntOrZero - let feePriceData = feePriceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { - PriceData.amount($0) - } - - let currentDisplayAddress = DisplayAddress( - address: accountAddress, - username: wallet.name - ) - - let model = OperationExtrinsicModel( - txHash: transaction.txHash, - call: transaction.callPath.callName, - module: transaction.callPath.moduleName, - sender: currentDisplayAddress, - fee: fee, - feePriceData: feePriceData - ) - - completion(.extrinsic(model)) - } - - private func extractContractOperationData( - newFee: BigUInt?, - _ completion: @escaping (OperationDetailsModel.OperationData?) -> Void - ) { - let fee: BigUInt = newFee ?? transaction.feeInPlankIntOrZero - let feePriceData = feePriceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { - PriceData.amount($0) - } - - guard - let accountResponse = wallet.fetch(for: chain.accountRequest()), - let currentAccountAddress = try? accountResponse.accountId.toAddress( - using: chain.chainFormat - ) else { - completion(nil) - return - } - - let currentDisplayAddress = DisplayAddress( - address: currentAccountAddress, - username: wallet.name - ) - - let contractAddress = transaction.receiver.flatMap { try? Data(hex: $0).toAddress(using: chain.chainFormat) } - let contractDisplayAddress = DisplayAddress(address: contractAddress ?? "", username: "") - - let model = OperationContractCallModel( - txHash: transaction.txHash, - fee: fee, - feePriceData: feePriceData, - sender: currentDisplayAddress, - contract: contractDisplayAddress, - functionName: transaction.evmContractFunctionName - ) - - completion(.contract(model)) - } - - private func extractTransferOperationData( - newFee: BigUInt?, - _ completion: @escaping (OperationDetailsModel.OperationData?) -> Void - ) { - guard let accountAddress = accountAddress else { - completion(nil) - return - } - let peerAddress = (transaction.sender == accountAddress ? transaction.receiver : transaction.sender) - ?? transaction.sender - let accountId = try? peerAddress.toAccountId(using: chain.chainFormat) - let peerId = accountId?.toHex() ?? peerAddress - - guard let peerId = try? Data(hexString: peerId) else { - completion(nil) - return - } - - let isOutgoing = transaction.type(for: accountAddress) == .outgoing - let amount = transaction.amountInPlankIntOrZero - let priceData = priceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { - PriceData.amount($0) - } - - let fee = newFee ?? transaction.feeInPlankIntOrZero - let feePriceData = feePriceCalculator?.calculatePrice(for: UInt64(bitPattern: transaction.timestamp)).map { - PriceData.amount($0) - } - - let currentDisplayAddress = DisplayAddress( - address: accountAddress, - username: wallet.name - ) - - let txId = transaction.txHash - - _ = fetchDisplayAddress( - for: [peerId], - chain: chain, - repository: walletRepository, - operationQueue: operationQueue - ) { result in - switch result { - case let .success(otherDisplayAddresses): - if let otherDisplayAddress = otherDisplayAddresses.first { - let model = OperationTransferModel( - txHash: txId, - amount: amount, - amountPriceData: priceData, - fee: fee, - feePriceData: feePriceData, - sender: isOutgoing ? currentDisplayAddress : otherDisplayAddress, - receiver: isOutgoing ? otherDisplayAddress : currentDisplayAddress, - outgoing: isOutgoing - ) - - completion(.transfer(model)) - } else { - completion(nil) - } - - case .failure: - completion(nil) - } - } - } - private func extractOperationData( replacingIfExists newFee: BigUInt?, _ completion: @escaping (OperationDetailsModel.OperationData?) -> Void ) { - guard let accountAddress = accountAddress else { - completion(nil) - return - } - switch transaction.type(for: accountAddress) { - case .incoming, .outgoing: - extractTransferOperationData(newFee: newFee, completion) - case .reward: - extractRewardOperationData(completion) - case .slash: - extractSlashOperationData(completion) - case .extrinsic: - if chainAsset.asset.isEvmNative { - extractContractOperationData(newFee: newFee, completion) - } else { - extractExtrinsicOperationData(newFee: newFee, completion) - } - case .none: - completion(nil) - } + operationDataProvider.extractOperationData( + replacingWith: newFee, + priceCalculator: priceCalculator, + feePriceCalculator: feePriceCalculator, + progressClosure: completion + ) } private func provideModel( diff --git a/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift b/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift index 9cb513e38f..ba36da0d06 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsPresenter.swift @@ -100,6 +100,11 @@ extension OperationDetailsPresenter: OperationDetailsPresenterProtocol { } case let .contract(contractModel): presentAddressOptions(contractModel.sender.address) + case let .poolReward(poolRewardOrSlashModel), let .poolSlash(poolRewardOrSlashModel): + guard let address = poolRewardOrSlashModel.pool?.bondedAddress(for: chainAsset.chain.chainFormat) else { + return + } + presentAddressOptions(address) case .none: break } @@ -128,6 +133,8 @@ extension OperationDetailsPresenter: OperationDetailsPresenterProtocol { presentEventIdOptions(slashModel.eventId) case let .contract(contractModel): presentTransactionHashOptions(contractModel.txHash) + case let .poolReward(poolRewardOrSlashModel), let .poolSlash(poolRewardOrSlashModel): + presentEventIdOptions(poolRewardOrSlashModel.eventId) case .none: break } diff --git a/novawallet/Modules/OperationDetails/OperationDetailsViewController.swift b/novawallet/Modules/OperationDetails/OperationDetailsViewController.swift index 10f9a8297d..a86aa7855d 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsViewController.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsViewController.swift @@ -156,7 +156,7 @@ final class OperationDetailsViewController: UIViewController, ViewHolder { } private func applyReward( - viewModel: OperationRewardViewModel, + viewModel: OperationRewardOrSlashViewModel, networkViewModel: NetworkViewModel ) { let rewardView: OperationDetailsRewardView = rootView.setupLocalizableView() @@ -179,7 +179,7 @@ final class OperationDetailsViewController: UIViewController, ViewHolder { } private func applySlash( - viewModel: OperationSlashViewModel, + viewModel: OperationRewardOrSlashViewModel, networkViewModel: NetworkViewModel ) { let rewardView: OperationDetailsRewardView = rootView.setupLocalizableView() @@ -227,6 +227,43 @@ final class OperationDetailsViewController: UIViewController, ViewHolder { ) } + private func applyPoolReward( + viewModel: OperationPoolRewardOrSlashViewModel, + networkViewModel: NetworkViewModel + ) { + let rewardView = applyCommonPoolRewardOrSlash() + rewardView.bindReward(viewModel: viewModel, networkViewModel: networkViewModel) + } + + private func applyPoolSlash( + viewModel: OperationPoolRewardOrSlashViewModel, + networkViewModel: NetworkViewModel + ) { + let slashView = applyCommonPoolRewardOrSlash() + slashView.bindSlash(viewModel: viewModel, networkViewModel: networkViewModel) + } + + private func applyCommonPoolRewardOrSlash() -> OperationDetailsPoolRewardView { + let view: OperationDetailsPoolRewardView = rootView.setupLocalizableView() + view.locale = selectedLocale + + rootView.removeActionButton() + + view.poolView.addTarget( + self, + action: #selector(actionSender), + for: .touchUpInside + ) + + view.eventIdView.addTarget( + self, + action: #selector(actionOperationId), + for: .touchUpInside + ) + + return view + } + @objc func actionSender() { presenter.showSenderActions() } @@ -264,6 +301,10 @@ extension OperationDetailsViewController: OperationDetailsViewProtocol { applySlash(viewModel: slashViewModel, networkViewModel: networkViewModel) case let .contract(contractViewModel): applyContract(viewModel: contractViewModel, networkViewModel: networkViewModel) + case let .poolReward(poolRewardViewModel): + applyPoolReward(viewModel: poolRewardViewModel, networkViewModel: networkViewModel) + case let .poolSlash(poolSlashViewModel): + applyPoolSlash(viewModel: poolSlashViewModel, networkViewModel: networkViewModel) } } } diff --git a/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift b/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift index 6b51fa82d9..08cb0360b3 100644 --- a/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift +++ b/novawallet/Modules/OperationDetails/OperationDetailsViewFactory.swift @@ -8,16 +8,31 @@ struct OperationDetailsViewFactory { for transaction: TransactionHistoryItem, chainAsset: ChainAsset ) -> OperationDetailsViewProtocol? { - guard let currencyManager = CurrencyManager.shared else { + guard + let currencyManager = CurrencyManager.shared, + let wallet = SelectedWalletSettings.shared.value, + let selectedAccount = wallet.fetchMetaChainAccount(for: chainAsset.chain.accountRequest()) else { return nil } - let storageFacade = UserDataStorageFacade.shared - let accountRepositoryFactory = AccountRepositoryFactory(storageFacade: storageFacade) - let walletRepository = accountRepositoryFactory.createMetaAccountRepository( - for: nil, - sortDescriptors: [] + + let chainRegistry = ChainRegistryFacade.sharedRegistry + let accountRepositoryFactory = AccountRepositoryFactory(storageFacade: UserDataStorageFacade.shared) + + let operationDetailsDataProviderFactory = OperationDetailsDataProviderFactory( + selectedAccount: selectedAccount, + chainAsset: chainAsset, + chainRegistry: chainRegistry, + accountRepositoryFactory: accountRepositoryFactory, + operationQueue: OperationManagerFacade.sharedDefaultQueue ) + guard + let operationDetailsDataProvider = operationDetailsDataProviderFactory.createProvider( + for: transaction + ) else { + return nil + } + let transactionLocalSubscriptionFactory = TransactionLocalSubscriptionFactory( storageFacade: SubstrateDataStorageFacade.shared, operationQueue: OperationManagerFacade.sharedDefaultQueue @@ -26,12 +41,10 @@ struct OperationDetailsViewFactory { let interactor = OperationDetailsInteractor( transaction: transaction, chainAsset: chainAsset, - wallet: SelectedWalletSettings.shared.value, - walletRepository: AnyDataProviderRepository(walletRepository), transactionLocalSubscriptionFactory: transactionLocalSubscriptionFactory, - operationQueue: OperationManagerFacade.sharedDefaultQueue, currencyManager: currencyManager, - priceLocalSubscriptionFactory: PriceProviderFactory.shared + priceLocalSubscriptionFactory: PriceProviderFactory.shared, + operationDataProvider: operationDetailsDataProvider ) let wireframe = OperationDetailsWireframe() diff --git a/novawallet/Modules/OperationDetails/View/OperationDetailsPoolRewardView.swift b/novawallet/Modules/OperationDetails/View/OperationDetailsPoolRewardView.swift new file mode 100644 index 0000000000..471a2bf4ad --- /dev/null +++ b/novawallet/Modules/OperationDetails/View/OperationDetailsPoolRewardView.swift @@ -0,0 +1,102 @@ +import UIKit + +final class OperationDetailsPoolRewardView: LocalizableView { + let poolTableView = StackTableView() + let eventTableView = StackTableView() + + let poolView: StackInfoTableCell = .create { view in + view.iconImageView.contentMode = .scaleAspectFit + } + + let networkView = StackNetworkCell() + + let eventIdView = StackInfoTableCell() + let typeView = StackTableCell() + + var locale = Locale.current { + didSet { + if oldValue != locale { + setupLocalization() + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .clear + + setupLayout() + setupLocalization() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func bindReward(viewModel: OperationPoolRewardOrSlashViewModel, networkViewModel: NetworkViewModel) { + let type = R.string.localizable.stakingReward(preferredLanguages: locale.rLanguages) + bindCommon(viewModel: viewModel, networkViewModel: networkViewModel, type: type) + } + + func bindSlash(viewModel: OperationPoolRewardOrSlashViewModel, networkViewModel: NetworkViewModel) { + let type = R.string.localizable.stakingSlash(preferredLanguages: locale.rLanguages) + bindCommon(viewModel: viewModel, networkViewModel: networkViewModel, type: type) + } + + private func bindCommon( + viewModel: OperationPoolRewardOrSlashViewModel, + networkViewModel: NetworkViewModel, + type: String + ) { + bindPool(for: viewModel.pool) + networkView.bind(viewModel: networkViewModel) + eventIdView.bind(details: viewModel.eventId) + typeView.bind(details: type) + } + + private func bindPool(for poolViewModel: DisplayAddressViewModel?) { + if let poolViewModel = poolViewModel { + poolView.isHidden = false + poolView.detailsLabel.lineBreakMode = poolViewModel.lineBreakMode + poolView.bind(viewModel: poolViewModel.cellViewModel) + } else { + poolView.isHidden = true + } + } + + private func setupLocalization() { + networkView.titleLabel.text = R.string.localizable.commonNetwork(preferredLanguages: locale.rLanguages) + + eventIdView.titleLabel.text = R.string.localizable.stakingCommonEventId( + preferredLanguages: locale.rLanguages + ) + + typeView.titleLabel.text = R.string.localizable.stakingAnalyticsDetailsType( + preferredLanguages: locale.rLanguages + ) + + poolView.titleLabel.text = R.string.localizable.stakingPool(preferredLanguages: locale.rLanguages) + } + + private func setupLayout() { + addSubview(poolTableView) + poolTableView.snp.makeConstraints { make in + make.top.trailing.leading.equalToSuperview() + } + + poolTableView.addArrangedSubview(poolView) + poolTableView.addArrangedSubview(networkView) + + addSubview(eventTableView) + eventTableView.snp.makeConstraints { make in + make.top.equalTo(poolTableView.snp.bottom).offset(12) + make.leading.trailing.equalToSuperview() + make.bottom.equalToSuperview() + } + + eventTableView.addArrangedSubview(eventIdView) + eventTableView.addArrangedSubview(typeView) + } +} diff --git a/novawallet/Modules/OperationDetails/View/OperationDetailsRewardView.swift b/novawallet/Modules/OperationDetails/View/OperationDetailsRewardView.swift index 592a78e00c..0ce988a62f 100644 --- a/novawallet/Modules/OperationDetails/View/OperationDetailsRewardView.swift +++ b/novawallet/Modules/OperationDetails/View/OperationDetailsRewardView.swift @@ -38,7 +38,7 @@ final class OperationDetailsRewardView: LocalizableView { fatalError("init(coder:) has not been implemented") } - func bindReward(viewModel: OperationRewardViewModel, networkViewModel: NetworkViewModel) { + func bindReward(viewModel: OperationRewardOrSlashViewModel, networkViewModel: NetworkViewModel) { let type = R.string.localizable.stakingReward(preferredLanguages: locale.rLanguages) bindCommon( networkViewModel: networkViewModel, @@ -50,7 +50,7 @@ final class OperationDetailsRewardView: LocalizableView { updateEraView(for: viewModel.era) } - func bindSlash(viewModel: OperationSlashViewModel, networkViewModel: NetworkViewModel) { + func bindSlash(viewModel: OperationRewardOrSlashViewModel, networkViewModel: NetworkViewModel) { let type = R.string.localizable.stakingSlash(preferredLanguages: locale.rLanguages) bindCommon( networkViewModel: networkViewModel, diff --git a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModel.swift b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModel.swift index 00685f1ece..32f65641f3 100644 --- a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModel.swift +++ b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModel.swift @@ -3,10 +3,12 @@ import Foundation struct OperationDetailsViewModel { enum ContentViewModel { case transfer(_ viewModel: OperationTransferViewModel) - case reward(_ viewModel: OperationRewardViewModel) - case slash(_ viewModel: OperationSlashViewModel) + case reward(_ viewModel: OperationRewardOrSlashViewModel) + case slash(_ viewModel: OperationRewardOrSlashViewModel) case extrinsic(_ viewModel: OperationExtrinsicViewModel) case contract(_ viewModel: OperationContractCallViewModel) + case poolReward(_ viewModel: OperationPoolRewardOrSlashViewModel) + case poolSlash(_ viewModel: OperationPoolRewardOrSlashViewModel) } let time: String diff --git a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift index bcac3065ff..fbc0fda514 100644 --- a/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift +++ b/novawallet/Modules/OperationDetails/ViewModel/OperationDetailsViewModelFactory.swift @@ -17,6 +17,7 @@ final class OperationDetailsViewModelFactory { let networkViewModelFactory: NetworkViewModelFactoryProtocol let displayAddressViewModelFactory: DisplayAddressViewModelFactoryProtocol let quantityFormatter: LocalizableResource + lazy var poolIconFactory: NominationPoolsIconFactoryProtocol = NominationPoolsIconFactory() init( balanceViewModelFactory: BalanceViewModelFactoryProtocol, @@ -47,7 +48,7 @@ final class OperationDetailsViewModelFactory { R.image.iconIncomingTransfer()! return StaticImageViewModel(image: image) - case .reward, .slash: + case .reward, .slash, .poolReward, .poolSlash: let image = R.image.iconRewardOperation()! return StaticImageViewModel(image: image) case .extrinsic, .contract: @@ -89,6 +90,14 @@ final class OperationDetailsViewModelFactory { amount = model.amount priceData = model.priceData prefix = "−" + case let .poolReward(model): + amount = model.amount + priceData = model.priceData + prefix = "+" + case let .poolSlash(model): + amount = model.amount + priceData = model.priceData + prefix = "-" } return Decimal.fromSubstrateAmount( @@ -158,10 +167,10 @@ final class OperationDetailsViewModelFactory { ) } - private func createRewardViewModel( - from model: OperationRewardModel, + private func createRewardOrSlashViewModel( + from model: OperationRewardOrSlashModel, locale: Locale - ) -> OperationRewardViewModel { + ) -> OperationRewardOrSlashViewModel { let validatorViewModel = model.validator.map { model in displayAddressViewModelFactory.createViewModel(from: model) } @@ -178,45 +187,36 @@ final class OperationDetailsViewModelFactory { } } - return OperationRewardViewModel( + return OperationRewardOrSlashViewModel( eventId: model.eventId, validator: validatorViewModel, era: eraString ) } - private func createSlashViewModel( - from model: OperationSlashModel, - locale: Locale - ) -> OperationSlashViewModel { - let validatorViewModel = model.validator.map { model in - displayAddressViewModelFactory.createViewModel(from: model) - } - - let eraString: String? = model.era.map { era in - if let eraString = quantityFormatter.value(for: locale) - .string(from: NSNumber(value: era)) { - return R.string.localizable.commonEraFormat( - eraString, - preferredLanguages: locale.rLanguages - ) - } else { - return "" - } + private func createPoolRewardOrSlashViewModel( + from model: OperationPoolRewardOrSlashModel, + chainAsset: ChainAsset, + locale _: Locale + ) -> OperationPoolRewardOrSlashViewModel { + guard let pool = model.pool else { + return .init(eventId: model.eventId, pool: nil) } - return OperationSlashViewModel( - eventId: model.eventId, - validator: validatorViewModel, - era: eraString + let poolViewModel = displayAddressViewModelFactory.createViewModel( + from: pool, + chainAsset: chainAsset ) + + return OperationPoolRewardOrSlashViewModel(eventId: model.eventId, pool: poolViewModel) } private func createContentViewModel( from data: OperationDetailsModel.OperationData, - feeAssetInfo: AssetBalanceDisplayInfo, + chainAsset: ChainAsset, locale: Locale ) -> OperationDetailsViewModel.ContentViewModel { + let feeAssetInfo = chainAsset.assetDisplayInfo switch data { case let .transfer(model): let viewModel = createTransferViewModel( @@ -230,14 +230,14 @@ final class OperationDetailsViewModelFactory { let viewModel = createExtrinsicViewModel(from: model) return .extrinsic(viewModel) case let .reward(model): - let viewModel = createRewardViewModel( + let viewModel = createRewardOrSlashViewModel( from: model, locale: locale ) return .reward(viewModel) case let .slash(model): - let viewModel = createSlashViewModel( + let viewModel = createRewardOrSlashViewModel( from: model, locale: locale ) @@ -246,6 +246,20 @@ final class OperationDetailsViewModelFactory { case let .contract(model): let viewModel = createContractViewModel(from: model) return .contract(viewModel) + case let .poolReward(model): + let viewModel = createPoolRewardOrSlashViewModel( + from: model, + chainAsset: chainAsset, + locale: locale + ) + return .poolReward(viewModel) + case let .poolSlash(model): + let viewModel = createPoolRewardOrSlashViewModel( + from: model, + chainAsset: chainAsset, + locale: locale + ) + return .poolSlash(viewModel) } } } @@ -260,11 +274,10 @@ extension OperationDetailsViewModelFactory: OperationDetailsViewModelFactoryProt let networkViewModel = networkViewModelFactory.createViewModel(from: chainAsset.chain) let assetInfo = chainAsset.assetDisplayInfo - let feeAssetInfo = chainAsset.chain.utilityAsset()?.displayInfo(with: chainAsset.chain.icon) ?? assetInfo let contentViewModel = createContentViewModel( from: model.operation, - feeAssetInfo: feeAssetInfo, + chainAsset: chainAsset, locale: locale ) diff --git a/novawallet/Modules/OperationDetails/ViewModel/OperationPoolRewardOrSlashViewModel.swift b/novawallet/Modules/OperationDetails/ViewModel/OperationPoolRewardOrSlashViewModel.swift new file mode 100644 index 0000000000..38953ac3a9 --- /dev/null +++ b/novawallet/Modules/OperationDetails/ViewModel/OperationPoolRewardOrSlashViewModel.swift @@ -0,0 +1,6 @@ +import Foundation + +struct OperationPoolRewardOrSlashViewModel { + let eventId: String + let pool: DisplayAddressViewModel? +} diff --git a/novawallet/Modules/OperationDetails/ViewModel/OperationRewardViewModel.swift b/novawallet/Modules/OperationDetails/ViewModel/OperationRewardOrSlashViewModel.swift similarity index 72% rename from novawallet/Modules/OperationDetails/ViewModel/OperationRewardViewModel.swift rename to novawallet/Modules/OperationDetails/ViewModel/OperationRewardOrSlashViewModel.swift index 76ae83f56c..24540f4bc3 100644 --- a/novawallet/Modules/OperationDetails/ViewModel/OperationRewardViewModel.swift +++ b/novawallet/Modules/OperationDetails/ViewModel/OperationRewardOrSlashViewModel.swift @@ -1,6 +1,6 @@ import Foundation -struct OperationRewardViewModel { +struct OperationRewardOrSlashViewModel { let eventId: String let validator: DisplayAddressViewModel? let era: String? diff --git a/novawallet/Modules/OperationDetails/ViewModel/OperationSlashViewModel.swift b/novawallet/Modules/OperationDetails/ViewModel/OperationSlashViewModel.swift deleted file mode 100644 index 0f68aad976..0000000000 --- a/novawallet/Modules/OperationDetails/ViewModel/OperationSlashViewModel.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -struct OperationSlashViewModel { - let eventId: String - let validator: DisplayAddressViewModel? - let era: String? -} diff --git a/novawallet/Modules/Staking/ControllerAccount/ControllerAccountInteractor.swift b/novawallet/Modules/Staking/ControllerAccount/ControllerAccountInteractor.swift index b91ce7d29b..e676f49c83 100644 --- a/novawallet/Modules/Staking/ControllerAccount/ControllerAccountInteractor.swift +++ b/novawallet/Modules/Staking/ControllerAccount/ControllerAccountInteractor.swift @@ -94,7 +94,7 @@ extension ControllerAccountInteractor: ControllerAccountInteractorInputProtocol provideDeprecationFlag() if let accountAddress = selectedAccount.toAddress() { - stashItemProvider = subscribeStashItemProvider(for: accountAddress) + stashItemProvider = subscribeStashItemProvider(for: accountAddress, chainId: chainAsset.chain.chainId) } else { presenter.didReceiveStashItem(result: .failure(ChainAccountFetchingError.accountNotExists)) } diff --git a/novawallet/Modules/Staking/ControllerAccount/ControllerAccountViewFactory.swift b/novawallet/Modules/Staking/ControllerAccount/ControllerAccountViewFactory.swift index 3f927891bc..53b8a95d9a 100644 --- a/novawallet/Modules/Staking/ControllerAccount/ControllerAccountViewFactory.swift +++ b/novawallet/Modules/Staking/ControllerAccount/ControllerAccountViewFactory.swift @@ -5,7 +5,7 @@ import SubstrateSdk import RobinHood struct ControllerAccountViewFactory { - static func createView(for state: StakingSharedState) -> ControllerAccountViewProtocol? { + static func createView(for state: RelaychainStakingSharedStateProtocol) -> ControllerAccountViewProtocol? { let chainAsset = state.stakingOption.chainAsset guard @@ -43,7 +43,7 @@ struct ControllerAccountViewFactory { } private static func createInteractor( - state: StakingSharedState + state: RelaychainStakingSharedStateProtocol ) -> ControllerAccountInteractor? { let chainAsset = state.stakingOption.chainAsset @@ -80,7 +80,7 @@ struct ControllerAccountViewFactory { return ControllerAccountInteractor( selectedAccount: selectedAccount, chainAsset: chainAsset, - stakingLocalSubscriptionFactory: state.stakingLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.localSubscriptionFactory, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, runtimeService: runtimeService, diff --git a/novawallet/Modules/Staking/ControllerAccount/ControllerAccountWireframe.swift b/novawallet/Modules/Staking/ControllerAccount/ControllerAccountWireframe.swift index 123bc37903..3491b8ca8a 100644 --- a/novawallet/Modules/Staking/ControllerAccount/ControllerAccountWireframe.swift +++ b/novawallet/Modules/Staking/ControllerAccount/ControllerAccountWireframe.swift @@ -2,9 +2,9 @@ import Foundation import SoraFoundation final class ControllerAccountWireframe: ControllerAccountWireframeProtocol { - let state: StakingSharedState + let state: RelaychainStakingSharedStateProtocol - init(state: StakingSharedState) { + init(state: RelaychainStakingSharedStateProtocol) { self.state = state } diff --git a/novawallet/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationInteractor.swift b/novawallet/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationInteractor.swift index 3d5daa2305..e606f7246a 100644 --- a/novawallet/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationInteractor.swift +++ b/novawallet/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationInteractor.swift @@ -104,7 +104,7 @@ final class ControllerAccountConfirmationInteractor: AccountFetching { extension ControllerAccountConfirmationInteractor: ControllerAccountConfirmationInteractorInputProtocol { func setup() { if let address = selectedAccount.toAddress() { - stashItemProvider = subscribeStashItemProvider(for: address) + stashItemProvider = subscribeStashItemProvider(for: address, chainId: chainAsset.chain.chainId) } else { presenter.didReceiveStashItem(result: .failure(ChainAccountFetchingError.accountNotExists)) } diff --git a/novawallet/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationViewFactory.swift b/novawallet/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationViewFactory.swift index aa9ec0fed6..c1088ebbef 100644 --- a/novawallet/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationViewFactory.swift +++ b/novawallet/Modules/Staking/ControllerAccountConfirmation/ControllerAccountConfirmationViewFactory.swift @@ -6,7 +6,7 @@ import RobinHood struct ControllerAccountConfirmationViewFactory { static func createView( - for state: StakingSharedState, + for state: RelaychainStakingSharedStateProtocol, controllerAccountItem: MetaChainAccountResponse ) -> ControllerAccountConfirmationViewProtocol? { guard @@ -53,7 +53,7 @@ struct ControllerAccountConfirmationViewFactory { } private static func createInteractor( - for state: StakingSharedState, + for state: RelaychainStakingSharedStateProtocol, controllerAccountItem: ChainAccountResponse ) -> ControllerAccountConfirmationInteractor? { let chainAsset = state.stakingOption.chainAsset @@ -97,7 +97,7 @@ struct ControllerAccountConfirmationViewFactory { selectedAccount: selectedAccount, controllerAccountItem: controllerAccountItem, chainAsset: chainAsset, - stakingLocalSubscriptionFactory: state.stakingLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.localSubscriptionFactory, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, runtimeService: runtimeService, diff --git a/novawallet/Modules/Staking/Dashboard/Model/StakingDashboardBuilder.swift b/novawallet/Modules/Staking/Dashboard/Model/StakingDashboardBuilder.swift index a547112c08..ef4b318475 100644 --- a/novawallet/Modules/Staking/Dashboard/Model/StakingDashboardBuilder.swift +++ b/novawallet/Modules/Staking/Dashboard/Model/StakingDashboardBuilder.swift @@ -1,5 +1,6 @@ import Foundation import RobinHood +import BigInt final class StakingDashboardBuilder { let workingQueue: DispatchQueue @@ -12,6 +13,7 @@ final class StakingDashboardBuilder { private var prices: [AssetModel.PriceId: PriceData] = [:] private var wallet: MetaAccountModel? private var syncState: MultistakingSyncState? + private var chainAssetSyncState: [ChainAssetId: Bool]? private var currentModel: StakingDashboardModel? init( @@ -24,6 +26,22 @@ final class StakingDashboardBuilder { self.resultClosure = resultClosure } + private func getAvailableBalance(for stakingType: StakingType?, chainAssetId: ChainAssetId) -> BigUInt? { + let optAssetBalance = balances[chainAssetId] + return optAssetBalance.flatMap { assetBalance in + StakingTypeBalanceFactory(stakingType: stakingType).getAvailableBalance(from: assetBalance) + } + } + + private func reindexChainAssetSyncState() { + chainAssetSyncState = syncState?.isOnchainSyncing.reduce(into: [ChainAssetId: Bool]()) { accum, keyValue in + let option = keyValue.key + let isSyncing = keyValue.value + + accum[option.chainAssetId] = (accum[option.chainAssetId] ?? false) || isSyncing + } + } + private func deriveOnchainSync(for stakingOption: Multistaking.ChainAssetOption) -> Bool { let account = wallet?.fetch(for: stakingOption.chainAsset.chain.accountRequest()) @@ -35,11 +53,24 @@ final class StakingDashboardBuilder { } } + private func deriveOnchainSync(for chainAsset: ChainAsset) -> Bool { + let account = wallet?.fetch(for: chainAsset.chain.accountRequest()) + + if account == nil { + // don't need onchain sync if no account + return false + } else { + return chainAssetSyncState?[chainAsset.chainAssetId] ?? true + } + } + private func deriveOffchainSync() -> Bool { syncState?.isOffchainSyncing ?? true } - private func buildDashboardItem(for stakingOption: Multistaking.ChainAssetOption) -> StakingDashboardItemModel { + private func buildDashboardItem( + for stakingOption: Multistaking.ChainAssetOption + ) -> StakingDashboardItemModel.Concrete { let account = wallet?.fetch(for: stakingOption.chainAsset.chain.accountRequest()) let priceData = stakingOption.chainAsset.asset.priceId.flatMap { prices[$0] } @@ -47,22 +78,52 @@ final class StakingDashboardBuilder { let isOnchainSyncing = deriveOnchainSync(for: stakingOption) let isOffchainSyncing = deriveOffchainSync() - return StakingDashboardItemModel( + let availableBalance = getAvailableBalance( + for: stakingOption.type, + chainAssetId: stakingOption.chainAsset.chainAssetId + ) + + return .init( stakingOption: stakingOption, dashboardItem: dashboardItems[stakingOption.option], accountId: account?.accountId, - balance: balances[stakingOption.chainAsset.chainAssetId], + availableBalance: availableBalance, price: priceData, isOnchainSync: isOnchainSyncing, isOffchainSync: isOffchainSyncing ) } + private func updateCombined( + store: inout [ChainAssetId: StakingDashboardItemModel.Combined], + item: StakingDashboardItemModel.Concrete + ) { + let chainAssetId = item.stakingOption.chainAsset.chainAssetId + + var currentValue = store[chainAssetId] ?? + StakingDashboardItemModel.Combined( + concrete: item, + availableBalance: getAvailableBalance( + for: nil, + chainAssetId: item.stakingOption.chainAsset.chainAssetId + ) + ) + + currentValue = currentValue + .byChangingSyncState( + isOnchainSync: currentValue.isOnchainSync || item.isOnchainSync, + isOffchainSync: currentValue.isOffchainSync || item.isOffchainSync + ) + .replacingWithGreatestApy(for: item.maxApy) + + store[chainAssetId] = currentValue + } + private func rebuildModel() { let dashboardItems = chainAssets.flatMap { chainAsset in let chainStakings = chainAsset.asset.supportedStakings ?? [] - let dashboardItems: [StakingDashboardItemModel] = chainStakings.map { staking in + let dashboardItems: [StakingDashboardItemModel.Concrete] = chainStakings.map { staking in let stakingOption = Multistaking.ChainAssetOption(chainAsset: chainAsset, type: staking) return buildDashboardItem(for: stakingOption) } @@ -73,36 +134,42 @@ final class StakingDashboardBuilder { // separate active stakings let activeStakings = dashboardItems.filter { $0.hasStaking } - let activeAssets = Set(activeStakings.map(\.stakingOption.chainAsset.chainAssetId)) + let activeStakingAssets = Set(activeStakings.map(\.stakingOption.chainAsset.chainAssetId)) - let allInactiveStakings = dashboardItems.filter { !$0.hasStaking } + let allInactiveStakings = dashboardItems + .filter { !$0.hasStaking } + .sorted { item1, item2 in + item1.stakingOption.type.isMorePreferred(than: item2.stakingOption.type) + } /** * We allow staking to be in inactive set if: * - there is no active staking for the asset - * - there is no inactive staking for the asset * - the asset is not in the testnet * * Otherwise staking goes to the More Options */ - var inactiveStakings: [ChainAssetId: StakingDashboardItemModel] = [:] - var moreOptions: [StakingDashboardItemModel] = [] + var inactiveStakings: [ChainAssetId: StakingDashboardItemModel.Combined] = [:] + var moreOptionsConcrete: [StakingDashboardItemModel.Concrete] = [] + var moreOptionsCombined: [ChainAssetId: StakingDashboardItemModel.Combined] = [:] allInactiveStakings.forEach { dashboardItem in - let chainAsset = dashboardItem.stakingOption.chainAsset - let chainAssetId = chainAsset.chainAssetId - - if - activeAssets.contains(chainAssetId) || - inactiveStakings[chainAssetId] != nil || - chainAsset.chain.isTestnet { - moreOptions.append(dashboardItem) + let stakingOption = dashboardItem.stakingOption.option + let chain = dashboardItem.stakingOption.chainAsset.chain + + if activeStakingAssets.contains(stakingOption.chainAssetId) { + moreOptionsConcrete.append(dashboardItem) + } else if chain.isTestnet { + updateCombined(store: &moreOptionsCombined, item: dashboardItem) } else { - inactiveStakings[chainAssetId] = dashboardItem + updateCombined(store: &inactiveStakings, item: dashboardItem) } } + let moreOptions = moreOptionsConcrete.map { StakingDashboardItemModel.concrete($0) } + + moreOptionsCombined.values.map { StakingDashboardItemModel.combined($0) } + let model = StakingDashboardModel( active: activeStakings.sortedByStake(), inactive: Array(inactiveStakings.values).sortedByBalance(), @@ -122,21 +189,39 @@ final class StakingDashboardBuilder { } } - private func updateSync( - for items: [StakingDashboardItemModel], - syncChange: Set - ) -> [StakingDashboardItemModel] { - let newOffchainSync = deriveOffchainSync() + private func createSyncChange( + for currentModel: StakingDashboardModel, + newOffchainSync: Bool + ) -> StakingDashboardBuilderResult.SyncChange { + let changeByChainAsset = currentModel.all.reduce(into: Set()) { accum, item in + if newOffchainSync != item.isOffchainSync { + accum.insert(item.chainAsset) + return + } - return items.map { item in - guard syncChange.contains(item.stakingOption) else { - return item + let newOnchainSync = deriveOnchainSync(for: item.chainAsset) + + if newOnchainSync != item.isOnchainSync { + accum.insert(item.chainAsset) + } + } + + let changeByChainOption = currentModel.allConcrete.reduce( + into: Set() + ) { accum, item in + if newOffchainSync != item.isOffchainSync { + accum.insert(item.stakingOption) + return } let newOnchainSync = deriveOnchainSync(for: item.stakingOption) - return item.byChangingSyncState(isOnchainSync: newOnchainSync, isOffchainSync: newOffchainSync) + if newOnchainSync != item.isOnchainSync { + accum.insert(item.stakingOption) + } } + + return .init(byStakingOption: changeByChainOption, byStakingChainAsset: changeByChainAsset) } private func updateModelAfterSyncChange() { @@ -146,22 +231,40 @@ final class StakingDashboardBuilder { let newOffchainSync = deriveOffchainSync() - let syncChange: Set = currentModel.all.reduce(into: Set()) { state, item in - if item.isOffchainSync != newOffchainSync { - state.insert(item.stakingOption) - return - } + let syncChange = createSyncChange(for: currentModel, newOffchainSync: newOffchainSync) - let newOnchainSync = deriveOnchainSync(for: item.stakingOption) + let newActive = currentModel.active.map { item in + item.byChangingSyncState( + isOnchainSync: newOffchainSync, + isOffchainSync: deriveOnchainSync(for: item.stakingOption) + ) + } - if item.isOnchainSync != newOnchainSync { - state.insert(item.stakingOption) - } + let newInactive = currentModel.inactive.map { item in + item.byChangingSyncState( + isOnchainSync: newOffchainSync, + isOffchainSync: deriveOnchainSync(for: item.chainAsset) + ) } - let newActive = updateSync(for: currentModel.active, syncChange: syncChange) - let newInactive = updateSync(for: currentModel.inactive, syncChange: syncChange) - let newMoreOptions = updateSync(for: currentModel.more, syncChange: syncChange) + let newMoreOptions = currentModel.more.map { item in + switch item { + case let .concrete(concrete): + let newConcrete = concrete.byChangingSyncState( + isOnchainSync: newOffchainSync, + isOffchainSync: deriveOnchainSync(for: concrete.stakingOption) + ) + + return StakingDashboardItemModel.concrete(newConcrete) + case let .combined(combined): + let newCombined = combined.byChangingSyncState( + isOnchainSync: newOffchainSync, + isOffchainSync: deriveOnchainSync(for: combined.chainAsset) + ) + + return StakingDashboardItemModel.combined(newCombined) + } + } let newModel = StakingDashboardModel( active: newActive, @@ -190,7 +293,6 @@ extension StakingDashboardBuilder: StakingDashboardBuilderProtocol { self?.balances = [:] self?.syncState = nil self?.currentModel = nil - self?.rebuildModel() } } @@ -250,6 +352,7 @@ extension StakingDashboardBuilder: StakingDashboardBuilderProtocol { workingQueue.async { [weak self] in if self?.syncState != state { self?.syncState = state + self?.reindexChainAssetSyncState() self?.updateModelAfterSyncChange() } } diff --git a/novawallet/Modules/Staking/Dashboard/Model/StakingDashboardBuilderResult.swift b/novawallet/Modules/Staking/Dashboard/Model/StakingDashboardBuilderResult.swift index e082625c13..89aca86020 100644 --- a/novawallet/Modules/Staking/Dashboard/Model/StakingDashboardBuilderResult.swift +++ b/novawallet/Modules/Staking/Dashboard/Model/StakingDashboardBuilderResult.swift @@ -1,9 +1,14 @@ import Foundation struct StakingDashboardBuilderResult { + struct SyncChange { + let byStakingOption: Set + let byStakingChainAsset: Set + } + enum ChangeKind { case reload - case sync(Set) + case sync(SyncChange) } let walletId: MetaAccountModel.Id? diff --git a/novawallet/Modules/Staking/Dashboard/Model/StakingDashboardModel.swift b/novawallet/Modules/Staking/Dashboard/Model/StakingDashboardModel.swift index 5c7c762438..f557f19922 100644 --- a/novawallet/Modules/Staking/Dashboard/Model/StakingDashboardModel.swift +++ b/novawallet/Modules/Staking/Dashboard/Model/StakingDashboardModel.swift @@ -1,52 +1,134 @@ import Foundation import RobinHood +import BigInt -struct StakingDashboardItemModel: Equatable { - let stakingOption: Multistaking.ChainAssetOption - let dashboardItem: Multistaking.DashboardItem? - let accountId: AccountId? - let balance: AssetBalance? - let price: PriceData? - let isOnchainSync: Bool - let isOffchainSync: Bool - - var hasStaking: Bool { - guard let dashboardItem = dashboardItem else { - return false - } - - return dashboardItem.stake != nil - } +protocol StakingDashboardItemModelCommonProtocol { + var chainAsset: ChainAsset { get } + var accountId: AccountId? { get } + var availableBalance: BigUInt? { get } + var price: PriceData? { get } + var maxApy: Decimal? { get } + var isOnchainSync: Bool { get } + var isOffchainSync: Bool { get } +} +extension StakingDashboardItemModelCommonProtocol { var hasAnySync: Bool { isOnchainSync || isOffchainSync } func balanceValue() -> Decimal { Decimal.fiatValue( - from: balance?.freeInPlank, + from: availableBalance, price: price, - precision: stakingOption.chainAsset.assetDisplayInfo.assetPrecision + precision: chainAsset.assetDisplayInfo.assetPrecision ) } +} - func stakeValue() -> Decimal { - Decimal.fiatValue( - from: dashboardItem?.stake, - price: price, - precision: stakingOption.chainAsset.assetDisplayInfo.assetPrecision - ) +enum StakingDashboardItemModel: Equatable { + struct Concrete: Equatable, StakingDashboardItemModelCommonProtocol { + let stakingOption: Multistaking.ChainAssetOption + let dashboardItem: Multistaking.DashboardItem? + let accountId: AccountId? + let availableBalance: BigUInt? + let price: PriceData? + let isOnchainSync: Bool + let isOffchainSync: Bool + + var chainAsset: ChainAsset { + stakingOption.chainAsset + } + + var hasStaking: Bool { + dashboardItem?.hasStaking ?? false + } + + var maxApy: Decimal? { + dashboardItem?.maxApy + } + + func stakeValue() -> Decimal { + Decimal.fiatValue( + from: dashboardItem?.stake, + price: price, + precision: chainAsset.assetDisplayInfo.assetPrecision + ) + } + } + + struct Combined: Equatable, StakingDashboardItemModelCommonProtocol { + let chainAsset: ChainAsset + let maxApy: Decimal? + let accountId: AccountId? + let availableBalance: BigUInt? + let price: PriceData? + let isOnchainSync: Bool + let isOffchainSync: Bool + + init( + chainAsset: ChainAsset, + maxApy: Decimal?, + accountId: AccountId?, + availableBalance: BigUInt?, + price: PriceData?, + isOnchainSync: Bool, + isOffchainSync: Bool + ) { + self.chainAsset = chainAsset + self.maxApy = maxApy + self.accountId = accountId + self.availableBalance = availableBalance + self.price = price + self.isOnchainSync = isOnchainSync + self.isOffchainSync = isOffchainSync + } + + init(concrete: Concrete, availableBalance: BigUInt?) { + self.init( + chainAsset: concrete.chainAsset, + maxApy: concrete.dashboardItem?.maxApy, + accountId: concrete.accountId, + availableBalance: availableBalance, + price: concrete.price, + isOnchainSync: concrete.isOnchainSync, + isOffchainSync: concrete.isOffchainSync + ) + } + + func replacingWithGreatestApy(for newApy: Decimal?) -> Combined { + let updatedApy: Decimal? + + if let maxApy = maxApy, let newApy = newApy { + updatedApy = max(maxApy, newApy) + } else { + updatedApy = maxApy ?? newApy + } + + return .init( + chainAsset: chainAsset, + maxApy: updatedApy, + accountId: accountId, + availableBalance: availableBalance, + price: price, + isOnchainSync: isOnchainSync, + isOffchainSync: isOffchainSync + ) + } } + + case concrete(Concrete) + case combined(Combined) } struct StakingDashboardModel: Equatable { - let active: [StakingDashboardItemModel] - let inactive: [StakingDashboardItemModel] + let active: [StakingDashboardItemModel.Concrete] + let inactive: [StakingDashboardItemModel.Combined] let more: [StakingDashboardItemModel] init( - active: [StakingDashboardItemModel] = [], - inactive: [StakingDashboardItemModel] = [], + active: [StakingDashboardItemModel.Concrete] = [], + inactive: [StakingDashboardItemModel.Combined] = [], more: [StakingDashboardItemModel] = [] ) { self.active = active @@ -58,12 +140,31 @@ struct StakingDashboardModel: Equatable { active.isEmpty && inactive.isEmpty && more.isEmpty } - var all: [StakingDashboardItemModel] { + var allConcrete: [StakingDashboardItemModel.Concrete] { + let moreOptionsConcrete: [StakingDashboardItemModel.Concrete] = more.compactMap { item in + switch item { + case let .concrete(concrete): + return concrete + case .combined: + return nil + } + } + + return active + moreOptionsConcrete + } + + var all: [StakingDashboardItemModelCommonProtocol] { active + inactive + more } + + func getActiveCounters() -> [ChainAssetId: Int] { + active.reduce(into: [ChainAssetId: Int]()) { accum, item in + accum[item.chainAsset.chainAssetId] = (accum[item.chainAsset.chainAssetId] ?? 0) + 1 + } + } } -extension Array where Element == StakingDashboardItemModel { +extension Array where Element: StakingDashboardItemModelCommonProtocol { static var totalStakeOrder: [ChainModel.Id: Int] { [ KnowChainId.polkadot: 0, @@ -78,72 +179,171 @@ extension Array where Element == StakingDashboardItemModel { ] } - func sortedByStake() -> [StakingDashboardItemModel] { + func sortedByBalance() -> [Element] { sorted { item1, item2 in CompoundComparator.compare(list: [{ - let stake1 = item1.stakeValue() - let stake2 = item2.stakeValue() + let chain1 = item1.chainAsset.chain + let chain2 = item2.chainAsset.chain - return CompoundComparator.compare(item1: stake1, item2: stake2, isAsc: false) + return ChainModelCompator.priorityAndTestnetComparator(chain1: chain1, chain2: chain2) }, { - let chain1 = item1.stakingOption.chainAsset.chain - let chain2 = item2.stakingOption.chainAsset.chain + let balance1 = item1.balanceValue() + let balance2 = item2.balanceValue() - return ChainModelCompator.priorityAndTestnetComparator(chain1: chain1, chain2: chain2) + return CompoundComparator.compare(item1: balance1, item2: balance2, isAsc: false) }, { - let chain1 = item1.stakingOption.chainAsset.chain - let chain2 = item2.stakingOption.chainAsset.chain + let hasBalance1 = item1.availableBalance != nil ? 0 : 1 + let hasBalance2 = item2.availableBalance != nil ? 0 : 1 + + return CompoundComparator.compare(item1: hasBalance1, item2: hasBalance2, isAsc: true) + }, { + let chain1 = item1.chainAsset.chain + let chain2 = item2.chainAsset.chain + + let totalStaked1 = Self.totalStakeOrder[chain1.chainId] ?? Int.max + let totalStaked2 = Self.totalStakeOrder[chain2.chainId] ?? Int.max + + return CompoundComparator.compare(item1: totalStaked1, item2: totalStaked2, isAsc: true) + }, { + let chain1 = item1.chainAsset.chain + let chain2 = item2.chainAsset.chain return chain1.name.localizedCaseInsensitiveCompare(chain2.name) }]) } } +} - func sortedByBalance() -> [StakingDashboardItemModel] { +extension Array where Element == StakingDashboardItemModel.Concrete { + func sortedByStake() -> [StakingDashboardItemModel.Concrete] { sorted { item1, item2 in CompoundComparator.compare(list: [{ - let chain1 = item1.stakingOption.chainAsset.chain - let chain2 = item2.stakingOption.chainAsset.chain - - return ChainModelCompator.priorityAndTestnetComparator(chain1: chain1, chain2: chain2) - }, { - let balance1 = item1.balanceValue() - let balance2 = item2.balanceValue() - - return CompoundComparator.compare(item1: balance1, item2: balance2, isAsc: false) - }, { - let hasBalance1 = item1.balance != nil ? 0 : 1 - let hasBalance2 = item2.balance != nil ? 0 : 1 + let stake1 = item1.stakeValue() + let stake2 = item2.stakeValue() - return CompoundComparator.compare(item1: hasBalance1, item2: hasBalance2, isAsc: true) + return CompoundComparator.compare(item1: stake1, item2: stake2, isAsc: false) }, { let chain1 = item1.stakingOption.chainAsset.chain let chain2 = item2.stakingOption.chainAsset.chain - let totalStaked1 = Self.totalStakeOrder[chain1.chainId] ?? Int.max - let totalStaked2 = Self.totalStakeOrder[chain2.chainId] ?? Int.max - - return CompoundComparator.compare(item1: totalStaked1, item2: totalStaked2, isAsc: true) + return ChainModelCompator.priorityAndTestnetComparator(chain1: chain1, chain2: chain2) }, { let chain1 = item1.stakingOption.chainAsset.chain let chain2 = item2.stakingOption.chainAsset.chain return chain1.name.localizedCaseInsensitiveCompare(chain2.name) + }, { + let type1 = item1.stakingOption.type + let type2 = item2.stakingOption.type + + return type1.isMorePreferred(than: type2) ? .orderedAscending : .orderedDescending }]) } } } -extension StakingDashboardItemModel { - func byChangingSyncState(isOnchainSync: Bool, isOffchainSync: Bool) -> StakingDashboardItemModel { - StakingDashboardItemModel( +extension StakingDashboardItemModel.Combined { + func byChangingSyncState(isOnchainSync: Bool, isOffchainSync: Bool) -> StakingDashboardItemModel.Combined { + StakingDashboardItemModel.Combined( + chainAsset: chainAsset, + maxApy: maxApy, + accountId: accountId, + availableBalance: availableBalance, + price: price, + isOnchainSync: isOnchainSync, + isOffchainSync: isOffchainSync + ) + } +} + +extension StakingDashboardItemModel.Concrete { + func byChangingSyncState(isOnchainSync: Bool, isOffchainSync: Bool) -> StakingDashboardItemModel.Concrete { + StakingDashboardItemModel.Concrete( stakingOption: stakingOption, dashboardItem: dashboardItem, accountId: accountId, - balance: balance, + availableBalance: availableBalance, price: price, isOnchainSync: isOnchainSync, isOffchainSync: isOffchainSync ) } } + +extension StakingDashboardItemModel { + func byChangingSyncState(isOnchainSync: Bool, isOffchainSync: Bool) -> StakingDashboardItemModel { + switch self { + case let .combined(value): + let newValue = value.byChangingSyncState(isOnchainSync: isOnchainSync, isOffchainSync: isOffchainSync) + return .combined(newValue) + case let .concrete(value): + let newValue = value.byChangingSyncState(isOnchainSync: isOnchainSync, isOffchainSync: isOffchainSync) + return .concrete(newValue) + } + } +} + +extension StakingDashboardItemModel: StakingDashboardItemModelCommonProtocol { + var chainAsset: ChainAsset { + switch self { + case let .combined(value): + return value.chainAsset + case let .concrete(value): + return value.chainAsset + } + } + + var accountId: AccountId? { + switch self { + case let .combined(value): + return value.accountId + case let .concrete(value): + return value.accountId + } + } + + var availableBalance: BigUInt? { + switch self { + case let .combined(value): + return value.availableBalance + case let .concrete(value): + return value.availableBalance + } + } + + var price: PriceData? { + switch self { + case let .combined(value): + return value.price + case let .concrete(value): + return value.price + } + } + + var maxApy: Decimal? { + switch self { + case let .combined(value): + return value.maxApy + case let .concrete(value): + return value.maxApy + } + } + + var isOnchainSync: Bool { + switch self { + case let .combined(value): + return value.isOnchainSync + case let .concrete(value): + return value.isOnchainSync + } + } + + var isOffchainSync: Bool { + switch self { + case let .combined(value): + return value.isOffchainSync + case let .concrete(value): + return value.isOffchainSync + } + } +} diff --git a/novawallet/Modules/Staking/Dashboard/StakingDashboardPresenter.swift b/novawallet/Modules/Staking/Dashboard/StakingDashboardPresenter.swift index 69db96584a..35906968dd 100644 --- a/novawallet/Modules/Staking/Dashboard/StakingDashboardPresenter.swift +++ b/novawallet/Modules/Staking/Dashboard/StakingDashboardPresenter.swift @@ -61,7 +61,7 @@ final class StakingDashboardPresenter { private func updateStakingView( using model: StakingDashboardModel, - syncChange: Set + syncChange: StakingDashboardBuilderResult.SyncChange ) { let updateViewModel = viewModelFactory.createUpdateViewModel( from: model, @@ -87,11 +87,11 @@ extension StakingDashboardPresenter: StakingDashboardPresenterProtocol { } func selectInactiveStaking(at index: Int) { - guard let option = lastResult?.model.inactive[index].stakingOption else { + guard let item = lastResult?.model.inactive[index] else { return } - wireframe.showStakingDetails(from: view, option: option) + wireframe.showStartStaking(from: view, chainAsset: item.chainAsset) } func selectMoreOptions() { diff --git a/novawallet/Modules/Staking/Dashboard/StakingDashboardProtocols.swift b/novawallet/Modules/Staking/Dashboard/StakingDashboardProtocols.swift index e06c59fc84..c58915c2ce 100644 --- a/novawallet/Modules/Staking/Dashboard/StakingDashboardProtocols.swift +++ b/novawallet/Modules/Staking/Dashboard/StakingDashboardProtocols.swift @@ -38,4 +38,6 @@ protocol StakingDashboardWireframeProtocol: ErrorPresentable, AlertPresentable, from view: StakingDashboardViewProtocol?, option: Multistaking.ChainAssetOption ) + + func showStartStaking(from view: StakingDashboardViewProtocol?, chainAsset: ChainAsset) } diff --git a/novawallet/Modules/Staking/Dashboard/StakingDashboardViewFactory.swift b/novawallet/Modules/Staking/Dashboard/StakingDashboardViewFactory.swift index dd46c80885..2570faea84 100644 --- a/novawallet/Modules/Staking/Dashboard/StakingDashboardViewFactory.swift +++ b/novawallet/Modules/Staking/Dashboard/StakingDashboardViewFactory.swift @@ -46,7 +46,7 @@ struct StakingDashboardViewFactory { ) -> StakingDashboardInteractor? { let walletSettings = SelectedWalletSettings.shared - guard let wallet = walletSettings.value, let currencyManager = CurrencyManager.shared else { + guard let currencyManager = CurrencyManager.shared else { return nil } diff --git a/novawallet/Modules/Staking/Dashboard/StakingDashboardWireframe.swift b/novawallet/Modules/Staking/Dashboard/StakingDashboardWireframe.swift index 283073d853..ac4efed5e3 100644 --- a/novawallet/Modules/Staking/Dashboard/StakingDashboardWireframe.swift +++ b/novawallet/Modules/Staking/Dashboard/StakingDashboardWireframe.swift @@ -35,4 +35,16 @@ final class StakingDashboardWireframe: StakingDashboardWireframeProtocol { animated: true ) } + + func showStartStaking(from view: StakingDashboardViewProtocol?, chainAsset: ChainAsset) { + guard let startStakingView = StartStakingInfoViewFactory.createView( + chainAsset: chainAsset, + selectedStakingType: nil + ) else { + return + } + + let navigationController = ImportantFlowViewFactory.createNavigation(from: startStakingView.controller) + view?.controller.present(navigationController, animated: true, completion: nil) + } } diff --git a/novawallet/Modules/Staking/Dashboard/View/StakingDashboardActiveCell.swift b/novawallet/Modules/Staking/Dashboard/View/StakingDashboardActiveCell.swift index 729d52f608..214b9e814b 100644 --- a/novawallet/Modules/Staking/Dashboard/View/StakingDashboardActiveCell.swift +++ b/novawallet/Modules/Staking/Dashboard/View/StakingDashboardActiveCell.swift @@ -9,7 +9,22 @@ final class StakingDashboardActiveCellView: UIView { static let topOffset = 16 } - let networkView = LoadableAssetListChainView() + let networkContainerView: GenericPairValueView< + LoadableAssetListChainView, BorderedIconLabelView + > = .create { view in + view.makeHorizontal() + view.spacing = 4 + view.sView.iconDetailsView.apply(style: .chips) + view.sView.iconDetailsView.detailsLabel.numberOfLines = 1 + view.sView.iconDetailsView.iconWidth = 10 + view.sView.iconDetailsView.spacing = 4 + view.sView.contentInsets = UIEdgeInsets(top: 5, left: 6, bottom: 5, right: 6) + view.stackView.alignment = .center + } + + var networkView: LoadableAssetListChainView { networkContainerView.fView } + + var stakingTypeView: BorderedIconLabelView { networkContainerView.sView } let detailsView: BlurredView = .create { view in view.contentInsets = .zero @@ -66,6 +81,14 @@ final class StakingDashboardActiveCellView: UIView { networkView.bind(viewModel: value) } + if let stakingTypeViewModel = viewModel.stakingType { + stakingTypeView.isHidden = false + + stakingTypeView.bind(viewModel: stakingTypeViewModel) + } else { + stakingTypeView.isHidden = true + } + rewardsView.valueBottom.bind(viewModel: viewModel.totalRewards) if viewModel.totalRewards.isLoading { @@ -114,9 +137,9 @@ final class StakingDashboardActiveCellView: UIView { make.width.equalTo(130) } - addSubview(networkView) + addSubview(networkContainerView) - networkView.snp.makeConstraints { make in + networkContainerView.snp.makeConstraints { make in make.leading.equalToSuperview().inset(Constants.leadingOffset) make.top.equalToSuperview().inset(Constants.topOffset) make.trailing.lessThanOrEqualTo(detailsView.snp.leading).offset(-8) @@ -124,10 +147,12 @@ final class StakingDashboardActiveCellView: UIView { addSubview(rewardsView) rewardsView.snp.makeConstraints { make in - make.top.equalTo(networkView.snp.bottom).offset(24) + make.top.equalTo(networkContainerView.snp.bottom).offset(24) make.leading.equalToSuperview().inset(Constants.leadingOffset) make.trailing.lessThanOrEqualTo(detailsView.snp.leading).offset(-8) } + + networkView.setContentCompressionResistancePriority(.low, for: .horizontal) } } @@ -153,7 +178,7 @@ extension StakingDashboardActiveCellView: SkeletonableView { var hidingViews: [UIView] { if loadingState == .all { return [ - networkView, + networkContainerView, rewardsView ] } @@ -161,7 +186,7 @@ extension StakingDashboardActiveCellView: SkeletonableView { var hidingViews: [UIView] = [] if loadingState.contains(.network) { - hidingViews.append(networkView) + hidingViews.append(networkContainerView) } if loadingState.contains(.rewards) { diff --git a/novawallet/Modules/Staking/Dashboard/View/StakingDashboardInactiveCell.swift b/novawallet/Modules/Staking/Dashboard/View/StakingDashboardInactiveCell.swift index 4ded14a90a..5e0786ce14 100644 --- a/novawallet/Modules/Staking/Dashboard/View/StakingDashboardInactiveCell.swift +++ b/novawallet/Modules/Staking/Dashboard/View/StakingDashboardInactiveCell.swift @@ -10,14 +10,20 @@ final class StakingDashboardInactiveCell: BlurredCollectionViewCell>, + LoadableGenericIconDetailsView< + GenericPairValueView< + GenericPairValueView, UILabel + > + >, IconDetailsGenericView> > { private enum Constants { static let iconSize = CGSize(width: 36, height: 36) } - var networkLabel: ShimmerLabel { titleView.detailsView.fView } + var networkLabel: ShimmerLabel { titleView.detailsView.fView.fView } + var stakingTypeView: BorderedIconLabelView { titleView.detailsView.fView.sView } + var balanceLabel: UILabel { titleView.detailsView.sView } var estimatedEarningsView: UIView { valueView.detailsView } var estimatedEarningsLabel: ShimmerLabel { valueView.detailsView.fView } @@ -49,6 +55,14 @@ final class StakingDashboardInactiveCellView: GenericTitleValueView< newLoadingState.formUnion(.network) } + if let stakingTypeViewModel = viewModel.stakingType { + stakingTypeView.isHidden = false + + stakingTypeView.bind(viewModel: stakingTypeViewModel) + } else { + stakingTypeView.isHidden = true + } + estimatedEarningsLabel.bind(viewModel: viewModel.estimatedEarnings.map(with: { $0 ?? "" })) if viewModel.estimatedEarnings.isLoading { @@ -115,7 +129,7 @@ final class StakingDashboardInactiveCellView: GenericTitleValueView< R.string.localizable.commonAvailableFormat($0, preferredLanguages: locale.rLanguages) } - titleView.detailsView.fView.text = viewModel.name + networkLabel.text = viewModel.name titleView.detailsView.sView.text = balanceString } @@ -124,11 +138,26 @@ final class StakingDashboardInactiveCellView: GenericTitleValueView< titleView.spacing = 12 titleView.detailsView.makeVertical() - titleView.detailsView.spacing = 0 + titleView.detailsView.spacing = 3 + titleView.detailsView.stackView.alignment = .leading + + titleView.detailsView.fView.makeHorizontal() + titleView.detailsView.fView.spacing = 4 networkLabel.applyShimmer(style: .regularSubheadlinePrimary) networkLabel.textAlignment = .left + stakingTypeView.iconDetailsView.apply(style: .chips) + stakingTypeView.iconDetailsView.detailsLabel.numberOfLines = 1 + stakingTypeView.iconDetailsView.spacing = 4 + stakingTypeView.iconDetailsView.iconWidth = 10 + stakingTypeView.backgroundView.cornerRadius = 5 + stakingTypeView.contentInsets = UIEdgeInsets(top: 3, left: 6, bottom: 3, right: 6) + + stakingTypeView.snp.makeConstraints { make in + make.height.equalTo(16) + } + balanceLabel.apply(style: .caption1Secondary) balanceLabel.textAlignment = .left diff --git a/novawallet/Modules/Staking/Dashboard/ViewModel/StakingDashboardViewModel.swift b/novawallet/Modules/Staking/Dashboard/ViewModel/StakingDashboardViewModel.swift index 1d14067efe..2fd4d2bd09 100644 --- a/novawallet/Modules/Staking/Dashboard/ViewModel/StakingDashboardViewModel.swift +++ b/novawallet/Modules/Staking/Dashboard/ViewModel/StakingDashboardViewModel.swift @@ -28,12 +28,14 @@ struct StakingDashboardEnabledViewModel { let status: LoadableViewModelState let yourStake: LoadableViewModelState let estimatedEarnings: LoadableViewModelState + let stakingType: TitleIconViewModel? } struct StakingDashboardDisabledViewModel { let networkViewModel: LoadableViewModelState let estimatedEarnings: LoadableViewModelState let balance: String? + let stakingType: TitleIconViewModel? } struct StakingDashboardViewModel { diff --git a/novawallet/Modules/Staking/Dashboard/ViewModel/StakingDashboardViewModelFactory.swift b/novawallet/Modules/Staking/Dashboard/ViewModel/StakingDashboardViewModelFactory.swift index ef5fa8611a..e961efa9b9 100644 --- a/novawallet/Modules/Staking/Dashboard/ViewModel/StakingDashboardViewModelFactory.swift +++ b/novawallet/Modules/Staking/Dashboard/ViewModel/StakingDashboardViewModelFactory.swift @@ -4,7 +4,8 @@ import BigInt protocol StakingDashboardViewModelFactoryProtocol { func createActiveStakingViewModel( - for model: StakingDashboardItemModel, + for model: StakingDashboardItemModel.Concrete, + singleActive: Bool, locale: Locale ) -> StakingDashboardEnabledViewModel @@ -15,7 +16,7 @@ protocol StakingDashboardViewModelFactoryProtocol { func createUpdateViewModel( from model: StakingDashboardModel, - syncChange: Set, + syncChange: StakingDashboardBuilderResult.SyncChange, locale: Locale ) -> StakingDashboardUpdateViewModel @@ -87,7 +88,7 @@ final class StakingDashboardViewModelFactory { } private func createStakingStatus( - for model: StakingDashboardItemModel + for model: StakingDashboardItemModel.Concrete ) -> LoadableViewModelState { guard let dashboardItem = model.dashboardItem else { return .loading @@ -96,14 +97,63 @@ final class StakingDashboardViewModelFactory { let state = StakingDashboardEnabledViewModel.Status(dashboardItem: dashboardItem) return model.hasAnySync ? .cached(value: state) : .loaded(value: state) } + + private func createStakingType( + for model: StakingDashboardItemModel, + locale: Locale + ) -> TitleIconViewModel? { + switch model { + case let .concrete(concrete): + return createStakingType( + for: concrete.stakingOption, + singleActive: false, + locale: locale + ) + case .combined: + return nil + } + } + + private func createStakingType( + for stakingOption: Multistaking.ChainAssetOption, + singleActive: Bool, + locale: Locale + ) -> TitleIconViewModel? { + guard !singleActive else { + return nil + } + + let stakings = stakingOption.chainAsset.asset.supportedStakings ?? [] + + guard stakings.count > 1 else { + return nil + } + + switch stakingOption.type { + case .auraRelaychain, .azero, .relaychain: + return TitleIconViewModel( + title: R.string.localizable.stakingDirect(preferredLanguages: locale.rLanguages).uppercased(), + icon: R.image.iconStakingDirect() + ) + + case .nominationPools: + return TitleIconViewModel( + title: R.string.localizable.stakingPool(preferredLanguages: locale.rLanguages).uppercased(), + icon: R.image.iconStakingPool() + ) + case .parachain, .turing, .unsupported: + return nil + } + } } extension StakingDashboardViewModelFactory: StakingDashboardViewModelFactoryProtocol { func createActiveStakingViewModel( - for model: StakingDashboardItemModel, + for model: StakingDashboardItemModel.Concrete, + singleActive: Bool, locale: Locale ) -> StakingDashboardEnabledViewModel { - let chainAsset = model.stakingOption.chainAsset + let chainAsset = model.chainAsset let assetDisplayInfo = chainAsset.assetDisplayInfo let networkViewModel = networkViewModelFactory.createViewModel( @@ -137,12 +187,19 @@ extension StakingDashboardViewModelFactory: StakingDashboardViewModelFactoryProt let network: LoadableViewModelState = model.hasAnySync ? .cached(value: networkViewModel) : .loaded(value: networkViewModel) + let stakingType = createStakingType( + for: model.stakingOption, + singleActive: singleActive, + locale: locale + ) + return .init( networkViewModel: network, totalRewards: totalRewards, status: status, yourStake: yourStake, - estimatedEarnings: estimatedEarnings + estimatedEarnings: estimatedEarnings, + stakingType: stakingType ) } @@ -150,7 +207,7 @@ extension StakingDashboardViewModelFactory: StakingDashboardViewModelFactoryProt for model: StakingDashboardItemModel, locale: Locale ) -> StakingDashboardDisabledViewModel { - let chainAsset = model.stakingOption.chainAsset + let chainAsset = model.chainAsset let assetDisplayInfo = chainAsset.assetDisplayInfo let networkViewModel = networkViewModelFactory.createViewModel( @@ -158,13 +215,13 @@ extension StakingDashboardViewModelFactory: StakingDashboardViewModelFactoryProt ) let estimatedEarnings = createEstimatedEarnings( - from: model.dashboardItem?.maxApy, + from: model.maxApy, isSyncing: model.isOffchainSync, locale: locale ) let balance = createAmount( - for: model.balance?.freeInPlank, + for: model.availableBalance, priceData: model.price, assetDisplayInfo: assetDisplayInfo, isSyncing: false, @@ -174,10 +231,13 @@ extension StakingDashboardViewModelFactory: StakingDashboardViewModelFactoryProt let network: LoadableViewModelState = model.hasAnySync ? .cached(value: networkViewModel) : .loaded(value: networkViewModel) + let stakingType = createStakingType(for: model, locale: locale) + return .init( networkViewModel: network, estimatedEarnings: estimatedEarnings, - balance: balance + balance: balance, + stakingType: stakingType ) } @@ -185,16 +245,21 @@ extension StakingDashboardViewModelFactory: StakingDashboardViewModelFactoryProt from model: StakingDashboardModel, locale: Locale ) -> StakingDashboardViewModel { + let activeCounters = model.getActiveCounters() + let activeViewModels = model.active.map { - createActiveStakingViewModel( + let counter = activeCounters[$0.chainAsset.chainAssetId] ?? 0 + + return createActiveStakingViewModel( for: $0, + singleActive: counter <= 1, locale: locale ) } let inactiveViewModels = model.inactive.map { createInactiveStakingViewModel( - for: $0, + for: .combined($0), locale: locale ) } @@ -212,26 +277,36 @@ extension StakingDashboardViewModelFactory: StakingDashboardViewModelFactoryProt func createUpdateViewModel( from model: StakingDashboardModel, - syncChange: Set, + syncChange: StakingDashboardBuilderResult.SyncChange, locale: Locale ) -> StakingDashboardUpdateViewModel { + let activeCounters = model.getActiveCounters() + let activeViewModels: [(Int, StakingDashboardEnabledViewModel)] = model.active.enumerated().compactMap { item in - guard syncChange.contains(item.1.stakingOption) else { + guard syncChange.byStakingOption.contains(item.1.stakingOption) else { return nil } - let viewModel = createActiveStakingViewModel(for: item.1, locale: locale) + let counter = activeCounters[item.1.chainAsset.chainAssetId] ?? 0 + let viewModel = createActiveStakingViewModel( + for: item.1, + singleActive: counter <= 1, + locale: locale + ) return (item.0, viewModel) } let inactiveViewModels: [(Int, StakingDashboardDisabledViewModel)] = model.inactive .enumerated().compactMap { item in - guard syncChange.contains(item.1.stakingOption) else { + guard syncChange.byStakingChainAsset.contains(item.1.chainAsset) else { return nil } - let viewModel = createInactiveStakingViewModel(for: item.1, locale: locale) + let viewModel = createInactiveStakingViewModel( + for: .combined(item.1), + locale: locale + ) return (item.0, viewModel) } diff --git a/novawallet/Modules/Staking/StakingAmount/Model/CalculatedReward.swift b/novawallet/Modules/Staking/Model/CalculatedReward.swift similarity index 100% rename from novawallet/Modules/Staking/StakingAmount/Model/CalculatedReward.swift rename to novawallet/Modules/Staking/Model/CalculatedReward.swift diff --git a/novawallet/Modules/Staking/Model/ConsesusType.swift b/novawallet/Modules/Staking/Model/ConsesusType.swift index e46ff508c3..05c414fe95 100644 --- a/novawallet/Modules/Staking/Model/ConsesusType.swift +++ b/novawallet/Modules/Staking/Model/ConsesusType.swift @@ -4,4 +4,40 @@ enum ConsensusType { case babe case auraGeneral case auraAzero + + init?(stakingType: StakingType) { + switch stakingType { + case .relaychain: + self = .babe + case .auraRelaychain: + self = .auraGeneral + case .azero: + self = .auraAzero + case .parachain, .turing, .unsupported, .nominationPools: + return nil + } + } + + init?(asset: AssetModel) { + let optMainStakingType = asset.stakings?.sorted { type1, type2 in + type1.isMorePreferred(than: type2) + }.first + + guard let mainStakingType = optMainStakingType else { + return nil + } + + self.init(stakingType: mainStakingType) + } + + var stakingType: StakingType { + switch self { + case .babe: + return .relaychain + case .auraGeneral: + return .auraRelaychain + case .auraAzero: + return .azero + } + } } diff --git a/novawallet/Modules/Staking/Model/DirectStakingMinStakeBuilder.swift b/novawallet/Modules/Staking/Model/DirectStakingMinStakeBuilder.swift new file mode 100644 index 0000000000..8289123f8a --- /dev/null +++ b/novawallet/Modules/Staking/Model/DirectStakingMinStakeBuilder.swift @@ -0,0 +1,18 @@ +import Foundation +import BigInt + +final class DirectStakingMinStakeBuilder: ValueResolver { + init(resultClosure: @escaping (BigUInt) -> Void) { + super.init( + p1Store: .undefined, + p2Store: .undefined, + p3Store: .undefined, + p4Store: .defined(()), + p5Store: .defined(()), + resolver: { networkStakingInfo, bagSizeList, minNominatorBond, _, _ in + networkStakingInfo.calculateMinimumStake(given: minNominatorBond, votersCount: bagSizeList) + }, + resultClosure: resultClosure + ) + } +} diff --git a/novawallet/Modules/Staking/Model/ParachainStaking/ParachainStakingNetworkInfo.swift b/novawallet/Modules/Staking/Model/ParachainStaking/ParachainStakingNetworkInfo.swift index ac9ba55354..37b8132885 100644 --- a/novawallet/Modules/Staking/Model/ParachainStaking/ParachainStakingNetworkInfo.swift +++ b/novawallet/Modules/Staking/Model/ParachainStaking/ParachainStakingNetworkInfo.swift @@ -2,7 +2,7 @@ import Foundation import BigInt extension ParachainStaking { - struct NetworkInfo { + struct NetworkInfo: Equatable { let totalStake: BigUInt let minStakeForRewards: BigUInt let minTechStake: BigUInt diff --git a/novawallet/Modules/Staking/Model/ParachainStaking/ParachainStakingSharedState.swift b/novawallet/Modules/Staking/Model/ParachainStaking/ParachainStakingSharedState.swift deleted file mode 100644 index 5634aad42a..0000000000 --- a/novawallet/Modules/Staking/Model/ParachainStaking/ParachainStakingSharedState.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Foundation - -final class ParachainStakingSharedState { - let stakingOption: Multistaking.ChainAssetOption - private(set) var collatorService: ParachainStakingCollatorServiceProtocol? - private(set) var rewardCalculationService: ParaStakingRewardCalculatorServiceProtocol? - private(set) var blockTimeService: BlockTimeEstimationServiceProtocol? - let stakingLocalSubscriptionFactory: ParachainStakingLocalSubscriptionFactoryProtocol - let generalLocalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol - - init( - stakingOption: Multistaking.ChainAssetOption, - collatorService: ParachainStakingCollatorServiceProtocol?, - rewardCalculationService: ParaStakingRewardCalculatorServiceProtocol?, - blockTimeService: BlockTimeEstimationServiceProtocol?, - stakingLocalSubscriptionFactory: ParachainStakingLocalSubscriptionFactoryProtocol, - generalLocalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol - ) { - self.stakingOption = stakingOption - self.collatorService = collatorService - self.rewardCalculationService = rewardCalculationService - self.blockTimeService = blockTimeService - self.stakingLocalSubscriptionFactory = stakingLocalSubscriptionFactory - self.generalLocalSubscriptionFactory = generalLocalSubscriptionFactory - } - - func replaceCollatorService(_ newService: ParachainStakingCollatorServiceProtocol) { - collatorService = newService - } - - func replaceRewardCalculatorService( - _ newService: ParaStakingRewardCalculatorServiceProtocol - ) { - rewardCalculationService = newService - } - - func replaceBlockTimeService(_ newService: BlockTimeEstimationServiceProtocol) { - blockTimeService = newService - } -} diff --git a/novawallet/Modules/Staking/Model/Relaychain/CustomValidatorsFullList.swift b/novawallet/Modules/Staking/Model/Relaychain/CustomValidatorsFullList.swift new file mode 100644 index 0000000000..e92de29305 --- /dev/null +++ b/novawallet/Modules/Staking/Model/Relaychain/CustomValidatorsFullList.swift @@ -0,0 +1,16 @@ +import Foundation + +struct CustomValidatorsFullList { + let allValidators: [SelectedValidatorInfo] + let preferredValidators: [SelectedValidatorInfo] + + func distinctCount() -> Int { + distinctAll().count + } + + func distinctAll() -> [SelectedValidatorInfo] { + let allValidatorAddresses = Set(allValidators.map(\.address)) + + return allValidators + preferredValidators.filter { !allValidatorAddresses.contains($0.address) } + } +} diff --git a/novawallet/Modules/Staking/Model/Relaychain/ElectedAndPrefValidators.swift b/novawallet/Modules/Staking/Model/Relaychain/ElectedAndPrefValidators.swift new file mode 100644 index 0000000000..415ca13ce4 --- /dev/null +++ b/novawallet/Modules/Staking/Model/Relaychain/ElectedAndPrefValidators.swift @@ -0,0 +1,10 @@ +import Foundation + +struct ElectedAndPrefValidators { + let electedValidators: [ElectedValidatorInfo] + let preferredValidators: [SelectedValidatorInfo] + + func electedToSelectedValidators(for address: AccountAddress? = nil) -> [SelectedValidatorInfo] { + electedValidators.map { $0.toSelected(for: address) } + } +} diff --git a/novawallet/Modules/Staking/Model/Relaychain/PreparedNomination.swift b/novawallet/Modules/Staking/Model/Relaychain/PreparedNomination.swift index e6e678ac98..711e1db363 100644 --- a/novawallet/Modules/Staking/Model/Relaychain/PreparedNomination.swift +++ b/novawallet/Modules/Staking/Model/Relaychain/PreparedNomination.swift @@ -5,3 +5,10 @@ struct PreparedNomination { let targets: [SelectedValidatorInfo] let maxTargets: Int } + +struct PreparedValidators { + let targets: [SelectedValidatorInfo] + let maxTargets: Int + let electedAndPrefValidators: ElectedAndPrefValidators + let recommendedValidators: [SelectedValidatorInfo] +} diff --git a/novawallet/Modules/Staking/Model/Relaychain/SelectedStakingOption.swift b/novawallet/Modules/Staking/Model/Relaychain/SelectedStakingOption.swift new file mode 100644 index 0000000000..c436d79df7 --- /dev/null +++ b/novawallet/Modules/Staking/Model/Relaychain/SelectedStakingOption.swift @@ -0,0 +1,17 @@ +import Foundation + +enum SelectedStakingOption { + case direct(PreparedValidators) + case pool(NominationPools.SelectedPool) + + var maxApy: Decimal? { + switch self { + case let .direct(preparedValidators): + return preparedValidators.targets + .map(\.stakeReturn) + .max() + case let .pool(selectedPool): + return selectedPool.maxApy + } + } +} diff --git a/novawallet/Modules/Staking/Model/Relaychain/StakingSharedState.swift b/novawallet/Modules/Staking/Model/Relaychain/StakingSharedState.swift deleted file mode 100644 index a5f3177404..0000000000 --- a/novawallet/Modules/Staking/Model/Relaychain/StakingSharedState.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation - -enum StakingSharedStateError: Error { - case missingBlockTimeService -} - -final class StakingSharedState { - let consensus: ConsensusType - let stakingOption: Multistaking.ChainAssetOption - private(set) var eraValidatorService: EraValidatorServiceProtocol? - private(set) var rewardCalculationService: RewardCalculatorServiceProtocol? - private(set) var blockTimeService: BlockTimeEstimationServiceProtocol? - let stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol - let operationQueue: OperationQueue - - init( - consensus: ConsensusType, - stakingOption: Multistaking.ChainAssetOption, - eraValidatorService: EraValidatorServiceProtocol?, - rewardCalculationService: RewardCalculatorServiceProtocol?, - blockTimeService: BlockTimeEstimationServiceProtocol?, - stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol, - operationQueue: OperationQueue = OperationManagerFacade.sharedDefaultQueue - ) { - self.consensus = consensus - self.stakingOption = stakingOption - self.eraValidatorService = eraValidatorService - self.rewardCalculationService = rewardCalculationService - self.blockTimeService = blockTimeService - self.stakingLocalSubscriptionFactory = stakingLocalSubscriptionFactory - self.operationQueue = operationQueue - } - - func replaceEraValidatorService(_ newService: EraValidatorServiceProtocol) { - eraValidatorService = newService - } - - func replaceRewardCalculatorService(_ newService: RewardCalculatorServiceProtocol) { - rewardCalculationService = newService - } - - func replaceBlockTimeService(_ newService: BlockTimeEstimationServiceProtocol?) { - blockTimeService = newService - } -} diff --git a/novawallet/Modules/Staking/Model/StakingConstants.swift b/novawallet/Modules/Staking/Model/StakingConstants.swift index 7c61152a34..46fede1214 100644 --- a/novawallet/Modules/Staking/Model/StakingConstants.swift +++ b/novawallet/Modules/Staking/Model/StakingConstants.swift @@ -3,4 +3,24 @@ import Foundation struct StakingConstants { static let targetsClusterLimit = 2 static let maxAmount: Decimal = 1e+7 + static let maxUnlockingChunks: UInt32 = 32 + + static let recommendedPoolIds: [ChainModel.Id: NominationPools.PoolId] = [ + KnowChainId.polkadot: 54, + KnowChainId.kusama: 160, + KnowChainId.alephZero: 74 + ] + + static let recommendedValidators: [ChainModel.Id: AccountAddress] = [ + KnowChainId.polkadot: "127zarPDhVzmCXVQ7Kfr1yyaa9wsMuJ74GJW9Q7ezHfQEgh6", + KnowChainId.kusama: "DhK6qU2U5kDWeJKvPRtmnWRs8ETUGZ9S9QmNmQFuzrNoKm4", + KnowChainId.alephZero: "5DBhSX89qijHkzUt9gcqsq9RiXxDfbjxyma1z78JSCdt4SoU" + ] + + static func preferredValidatorIds(for chain: ChainModel) -> [AccountId] { + StakingConstants + .recommendedValidators[chain.chainId] + .flatMap { try? $0.toAccountId(using: chain.chainFormat) } + .map { [$0] } ?? [] + } } diff --git a/novawallet/Modules/Staking/Model/StakingSharedState/NPoolsStakingSharedState.swift b/novawallet/Modules/Staking/Model/StakingSharedState/NPoolsStakingSharedState.swift new file mode 100644 index 0000000000..eadcdf677d --- /dev/null +++ b/novawallet/Modules/Staking/Model/StakingSharedState/NPoolsStakingSharedState.swift @@ -0,0 +1,168 @@ +import Foundation + +protocol NPoolsStakingSharedStateProtocol: AnyObject { + var chainAsset: ChainAsset { get } + var timeModel: StakingTimeModel { get } + + var relaychainGlobalSubscriptionService: StakingRemoteSubscriptionServiceProtocol { get } + var relaychainLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol { get } + var eraValidatorService: EraValidatorServiceProtocol { get } + var rewardCalculatorService: RewardCalculatorServiceProtocol { get } + + var npRemoteSubscriptionService: NominationPoolsRemoteSubscriptionServiceProtocol { get } + var npAccountSubscriptionServiceFactory: NominationPoolsAccountUpdatingFactoryProtocol { get } + var activePoolsService: EraNominationPoolsServiceProtocol { get } + var npLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol { get } + + func setup(for accountId: AccountId) throws + func throttle() + + func createEraCountdownOperationFactory( + for operationQueue: OperationQueue + ) -> EraCountdownOperationFactoryProtocol + + func createStakingDurationOperationFactory() -> StakingDurationOperationFactoryProtocol +} + +final class NPoolsStakingSharedState { + let chainAsset: ChainAsset + let relaychainGlobalSubscriptionService: StakingRemoteSubscriptionServiceProtocol + let timeModel: StakingTimeModel + let relaychainLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol + let eraValidatorService: EraValidatorServiceProtocol + let rewardCalculatorService: RewardCalculatorServiceProtocol + let npRemoteSubscriptionService: NominationPoolsRemoteSubscriptionServiceProtocol + let npAccountSubscriptionServiceFactory: NominationPoolsAccountUpdatingFactoryProtocol + let activePoolsService: EraNominationPoolsServiceProtocol + let npLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol + let logger: LoggerProtocol + + var chainId: ChainModel.Id { + chainAsset.chain.chainId + } + + private var accountService: NominationPoolsAccountUpdatingService? + private var relaychainGlobalSubscription: UUID? + private var npoolsGlobalSubscription: UUID? + + private lazy var consensusDependingFactory = RelaychainConsensusStateDependingFactory() + + init( + chainAsset: ChainAsset, + relaychainGlobalSubscriptionService: StakingRemoteSubscriptionServiceProtocol, + timeModel: StakingTimeModel, + relaychainLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol, + eraValidatorService: EraValidatorServiceProtocol, + rewardCalculatorService: RewardCalculatorServiceProtocol, + npRemoteSubscriptionService: NominationPoolsRemoteSubscriptionServiceProtocol, + npAccountSubscriptionServiceFactory: NominationPoolsAccountUpdatingFactoryProtocol, + activePoolsService: EraNominationPoolsServiceProtocol, + npLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, + logger: LoggerProtocol + ) { + self.chainAsset = chainAsset + self.relaychainGlobalSubscriptionService = relaychainGlobalSubscriptionService + self.timeModel = timeModel + self.relaychainLocalSubscriptionFactory = relaychainLocalSubscriptionFactory + self.eraValidatorService = eraValidatorService + self.rewardCalculatorService = rewardCalculatorService + self.npRemoteSubscriptionService = npRemoteSubscriptionService + self.npAccountSubscriptionServiceFactory = npAccountSubscriptionServiceFactory + self.activePoolsService = activePoolsService + self.npLocalSubscriptionFactory = npLocalSubscriptionFactory + self.logger = logger + } +} + +extension NPoolsStakingSharedState: NPoolsStakingSharedStateProtocol { + func setup(for accountId: AccountId) throws { + accountService = try npAccountSubscriptionServiceFactory.create(for: accountId, chainAsset: chainAsset) + accountService?.setup() + + relaychainGlobalSubscription = relaychainGlobalSubscriptionService.attachToGlobalData( + for: chainId, + queue: .main + ) { [weak self] result in + switch result { + case .success: + self?.logger.debug("Relaychain global data subscription succeeded") + case let .failure(error): + self?.logger.error("Relaychain global data subscription failed: \(error)") + } + } + + npoolsGlobalSubscription = npRemoteSubscriptionService.attachToGlobalData( + for: chainId, + queue: .main + ) { [weak self] result in + switch result { + case .success: + self?.logger.debug("Nomination pools global data subscription succeeded") + case let .failure(error): + self?.logger.error("Nomination pools global data subscription failed: \(error)") + } + } + + eraValidatorService.setup() + rewardCalculatorService.setup() + activePoolsService.setup() + timeModel.blockTimeService?.setup() + } + + func throttle() { + accountService?.throttle() + accountService = nil + + if let subscription = relaychainGlobalSubscription { + relaychainGlobalSubscriptionService.detachFromGlobalData( + for: subscription, + chainId: chainId, + queue: .main + ) { [weak self] result in + switch result { + case .success: + self?.logger.debug("Relaychain global data unsubscribe succeeded") + case let .failure(error): + self?.logger.error("Relaychain global data unsubscribe failed: \(error)") + } + } + } + + if let subscription = npoolsGlobalSubscription { + npRemoteSubscriptionService.detachFromGlobalData( + for: subscription, + chainId: chainId, + queue: .main + ) { [weak self] result in + switch result { + case .success: + self?.logger.debug("Nomination pools global data unsubscribe succeeded") + case let .failure(error): + self?.logger.error("Nomination pools global data unsubscribe failed: \(error)") + } + } + } + + eraValidatorService.throttle() + rewardCalculatorService.throttle() + activePoolsService.throttle() + timeModel.blockTimeService?.throttle() + } + + func createEraCountdownOperationFactory( + for operationQueue: OperationQueue + ) -> EraCountdownOperationFactoryProtocol { + consensusDependingFactory.createEraCountdownOperationFactory( + for: chainAsset.chain, + timeModel: timeModel, + operationQueue: operationQueue + ) + } + + func createStakingDurationOperationFactory() -> StakingDurationOperationFactoryProtocol { + consensusDependingFactory.createStakingDurationOperationFactory( + for: chainAsset.chain, + timeModel: timeModel + ) + } +} diff --git a/novawallet/Modules/Staking/Model/StakingSharedState/ParachainStakingSharedState.swift b/novawallet/Modules/Staking/Model/StakingSharedState/ParachainStakingSharedState.swift new file mode 100644 index 0000000000..aedda12560 --- /dev/null +++ b/novawallet/Modules/Staking/Model/StakingSharedState/ParachainStakingSharedState.swift @@ -0,0 +1,118 @@ +import Foundation + +protocol ParachainStakingSharedStateProtocol: AnyObject { + var stakingOption: Multistaking.ChainAssetOption { get } + var chainRegistry: ChainRegistryProtocol { get } + var globalRemoteSubscriptionService: StakingRemoteSubscriptionServiceProtocol { get } + var accountRemoteSubscriptionService: ParachainStakingAccountSubscriptionServiceProtocol { get } + var collatorService: ParachainStakingCollatorServiceProtocol { get } + var rewardCalculationService: ParaStakingRewardCalculatorServiceProtocol { get } + var blockTimeService: BlockTimeEstimationServiceProtocol { get } + var stakingLocalSubscriptionFactory: ParachainStakingLocalSubscriptionFactoryProtocol { get } + var generalLocalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol { get } + var logger: LoggerProtocol { get } + + func setup(for accountId: AccountId?) + func throttle() +} + +final class ParachainStakingSharedState: ParachainStakingSharedStateProtocol { + let stakingOption: Multistaking.ChainAssetOption + let chainRegistry: ChainRegistryProtocol + let globalRemoteSubscriptionService: StakingRemoteSubscriptionServiceProtocol + let accountRemoteSubscriptionService: ParachainStakingAccountSubscriptionServiceProtocol + let collatorService: ParachainStakingCollatorServiceProtocol + let rewardCalculationService: ParaStakingRewardCalculatorServiceProtocol + let blockTimeService: BlockTimeEstimationServiceProtocol + let stakingLocalSubscriptionFactory: ParachainStakingLocalSubscriptionFactoryProtocol + let generalLocalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol + let logger: LoggerProtocol + + private var globalRemoteSubscription: UUID? + private var accountRemoteSubscription: UUID? + + init( + stakingOption: Multistaking.ChainAssetOption, + chainRegistry: ChainRegistryProtocol, + globalRemoteSubscriptionService: StakingRemoteSubscriptionServiceProtocol, + accountRemoteSubscriptionService: ParachainStakingAccountSubscriptionServiceProtocol, + collatorService: ParachainStakingCollatorServiceProtocol, + rewardCalculationService: ParaStakingRewardCalculatorServiceProtocol, + blockTimeService: BlockTimeEstimationServiceProtocol, + stakingLocalSubscriptionFactory: ParachainStakingLocalSubscriptionFactoryProtocol, + generalLocalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol, + logger: LoggerProtocol + ) { + self.stakingOption = stakingOption + self.chainRegistry = chainRegistry + self.globalRemoteSubscriptionService = globalRemoteSubscriptionService + self.accountRemoteSubscriptionService = accountRemoteSubscriptionService + self.collatorService = collatorService + self.rewardCalculationService = rewardCalculationService + self.blockTimeService = blockTimeService + self.stakingLocalSubscriptionFactory = stakingLocalSubscriptionFactory + self.generalLocalSubscriptionFactory = generalLocalSubscriptionFactory + self.logger = logger + } + + func setup(for accountId: AccountId?) { + let chainId = stakingOption.chainAsset.chain.chainId + + globalRemoteSubscription = globalRemoteSubscriptionService.attachToGlobalData( + for: chainId, + queue: .main + ) { [weak self] result in + switch result { + case .success: + self?.logger.debug("Parachain global remote subscription succeeded") + case let .failure(error): + self?.logger.error("Parachain global remote subscription failed: \(error)") + } + } + + if let accountId = accountId { + accountRemoteSubscription = accountRemoteSubscriptionService.attachToAccountData( + for: chainId, + accountId: accountId, + queue: .main + ) { [weak self] result in + switch result { + case .success: + self?.logger.debug("Parachain account remote subscription succeeded") + case let .failure(error): + self?.logger.error("Parachain account remote subscription failed: \(error)") + } + } + } + + collatorService.setup() + rewardCalculationService.setup() + blockTimeService.setup() + } + + func throttle() { + let chainId = stakingOption.chainAsset.chain.chainId + + if let globalRemoteSubscription = globalRemoteSubscription { + globalRemoteSubscriptionService.detachFromGlobalData( + for: globalRemoteSubscription, + chainId: chainId, + queue: nil, + closure: nil + ) + } + + if let accountRemoteSubscription = accountRemoteSubscription { + globalRemoteSubscriptionService.detachFromGlobalData( + for: accountRemoteSubscription, + chainId: chainId, + queue: nil, + closure: nil + ) + } + + collatorService.throttle() + rewardCalculationService.throttle() + blockTimeService.throttle() + } +} diff --git a/novawallet/Modules/Staking/Model/Relaychain/StakingSharedState+Duration.swift b/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainConsensusStateDepending.swift similarity index 54% rename from novawallet/Modules/Staking/Model/Relaychain/StakingSharedState+Duration.swift rename to novawallet/Modules/Staking/Model/StakingSharedState/RelaychainConsensusStateDepending.swift index e9346b986b..64b18d3b39 100644 --- a/novawallet/Modules/Staking/Model/Relaychain/StakingSharedState+Duration.swift +++ b/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainConsensusStateDepending.swift @@ -1,30 +1,61 @@ import Foundation import RobinHood +import SubstrateSdk + +protocol RelaychainConsensusStateDepending { + func createNetworkInfoOperationFactory( + for durationFactory: StakingDurationOperationFactoryProtocol, + operationQueue: OperationQueue + ) -> NetworkStakingInfoOperationFactoryProtocol + + func createEraCountdownOperationFactory( + for chain: ChainModel, + timeModel: StakingTimeModel, + operationQueue: OperationQueue + ) -> EraCountdownOperationFactoryProtocol + + func createStakingDurationOperationFactory( + for chain: ChainModel, + timeModel: StakingTimeModel + ) -> StakingDurationOperationFactoryProtocol +} + +final class RelaychainConsensusStateDependingFactory: RelaychainConsensusStateDepending { + func createNetworkInfoOperationFactory( + for durationFactory: StakingDurationOperationFactoryProtocol, + operationQueue: OperationQueue + ) -> NetworkStakingInfoOperationFactoryProtocol { + let votersInfoOperationFactory = VotersInfoOperationFactory( + operationManager: OperationManager(operationQueue: operationQueue) + ) + + return NetworkStakingInfoOperationFactory( + durationFactory: durationFactory, + votersOperationFactory: votersInfoOperationFactory + ) + } -extension StakingSharedState { func createEraCountdownOperationFactory( for chain: ChainModel, - storageRequestFactory: StorageRequestFactoryProtocol - ) throws -> EraCountdownOperationFactoryProtocol { - switch consensus { + timeModel: StakingTimeModel, + operationQueue: OperationQueue + ) -> EraCountdownOperationFactoryProtocol { + let storageRequestFactory = StorageRequestFactory( + remoteFactory: StorageKeyFactory(), + operationManager: OperationManager(operationQueue: operationQueue) + ) + + switch timeModel { case .babe: return BabeEraOperationFactory(storageRequestFactory: storageRequestFactory) - case .auraGeneral: - guard let blockTimeService = blockTimeService else { - throw StakingSharedStateError.missingBlockTimeService - } - + case let .auraGeneral(blockTimeService): return AuraEraOperationFactory( storageRequestFactory: storageRequestFactory, blockTimeService: blockTimeService, blockTimeOperationFactory: BlockTimeOperationFactory(chain: chain), sessionPeriodOperationFactory: PathStakingSessionPeriodOperationFactory(path: .electionsSessionPeriod) ) - case .auraAzero: - guard let blockTimeService = blockTimeService else { - throw StakingSharedStateError.missingBlockTimeService - } - + case let .azero(blockTimeService): return AuraEraOperationFactory( storageRequestFactory: storageRequestFactory, blockTimeService: blockTimeService, @@ -35,26 +66,19 @@ extension StakingSharedState { } func createStakingDurationOperationFactory( - for chain: ChainModel - ) throws -> StakingDurationOperationFactoryProtocol { - switch consensus { + for chain: ChainModel, + timeModel: StakingTimeModel + ) -> StakingDurationOperationFactoryProtocol { + switch timeModel { case .babe: return BabeStakingDurationFactory() - case .auraGeneral: - guard let blockTimeService = blockTimeService else { - throw StakingSharedStateError.missingBlockTimeService - } - + case let .auraGeneral(blockTimeService): return AuraStakingDurationFactory( blockTimeService: blockTimeService, blockTimeOperationFactory: BlockTimeOperationFactory(chain: chain), sessionPeriodOperationFactory: PathStakingSessionPeriodOperationFactory(path: .electionsSessionPeriod) ) - case .auraAzero: - guard let blockTimeService = blockTimeService else { - throw StakingSharedStateError.missingBlockTimeService - } - + case let .azero(blockTimeService): return AuraStakingDurationFactory( blockTimeService: blockTimeService, blockTimeOperationFactory: BlockTimeOperationFactory(chain: chain), @@ -62,18 +86,4 @@ extension StakingSharedState { ) } } - - func createNetworkInfoOperationFactory( - for chain: ChainModel - ) throws -> NetworkStakingInfoOperationFactoryProtocol { - let durationFactory = try createStakingDurationOperationFactory(for: chain) - let votersOperationFactory = VotersInfoOperationFactory( - operationManager: OperationManager(operationQueue: operationQueue) - ) - - return NetworkStakingInfoOperationFactory( - durationFactory: durationFactory, - votersOperationFactory: votersOperationFactory - ) - } } diff --git a/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainStakingSharedState.swift b/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainStakingSharedState.swift new file mode 100644 index 0000000000..c870e70fb9 --- /dev/null +++ b/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainStakingSharedState.swift @@ -0,0 +1,147 @@ +import Foundation +import RobinHood +import SubstrateSdk + +protocol RelaychainStakingSharedStateProtocol: AnyObject { + var consensus: ConsensusType { get } + var stakingOption: Multistaking.ChainAssetOption { get } + var globalRemoteSubscriptionService: StakingRemoteSubscriptionServiceProtocol { get } + var accountRemoteSubscriptionService: StakingAccountUpdatingServiceProtocol { get } + var localSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol { get } + var eraValidatorService: EraValidatorServiceProtocol { get } + var rewardCalculatorService: RewardCalculatorServiceProtocol { get } + + func setup(for accountId: AccountId?) throws + func throttle() + + func createNetworkInfoOperationFactory( + for operationQueue: OperationQueue + ) -> NetworkStakingInfoOperationFactoryProtocol + + func createEraCountdownOperationFactory(for operationQueue: OperationQueue) -> EraCountdownOperationFactoryProtocol + func createStakingDurationOperationFactory() -> StakingDurationOperationFactoryProtocol +} + +final class RelaychainStakingSharedState: RelaychainStakingSharedStateProtocol { + let consensus: ConsensusType + let timeModel: StakingTimeModel + let stakingOption: Multistaking.ChainAssetOption + let globalRemoteSubscriptionService: StakingRemoteSubscriptionServiceProtocol + let accountRemoteSubscriptionService: StakingAccountUpdatingServiceProtocol + let localSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol + let eraValidatorService: EraValidatorServiceProtocol + let rewardCalculatorService: RewardCalculatorServiceProtocol + let logger: LoggerProtocol + + private var globalSubscriptionId: UUID? + + private lazy var consensusDependingFactory = RelaychainConsensusStateDependingFactory() + + var chain: ChainModel { stakingOption.chainAsset.chain } + + init( + consensus: ConsensusType, + stakingOption: Multistaking.ChainAssetOption, + globalRemoteSubscriptionService: StakingRemoteSubscriptionServiceProtocol, + accountRemoteSubscriptionService: StakingAccountUpdatingServiceProtocol, + localSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol, + eraValidatorService: EraValidatorServiceProtocol, + rewardCalculatorService: RewardCalculatorServiceProtocol, + timeModel: StakingTimeModel, + logger: LoggerProtocol + ) { + self.consensus = consensus + self.stakingOption = stakingOption + self.globalRemoteSubscriptionService = globalRemoteSubscriptionService + self.accountRemoteSubscriptionService = accountRemoteSubscriptionService + self.localSubscriptionFactory = localSubscriptionFactory + self.eraValidatorService = eraValidatorService + self.rewardCalculatorService = rewardCalculatorService + self.timeModel = timeModel + self.logger = logger + } + + func setup(for accountId: AccountId?) throws { + globalSubscriptionId = globalRemoteSubscriptionService.attachToGlobalData( + for: chain.chainId, + queue: .main + ) { [weak self] result in + switch result { + case .success: + self?.logger.debug("Relaychain staking global data subscription succeeded") + case let .failure(error): + self?.logger.error("Relaychain staking global data subscription failed: \(error)") + } + } + + eraValidatorService.setup() + rewardCalculatorService.setup() + timeModel.blockTimeService?.setup() + + if let accountId = accountId { + try accountRemoteSubscriptionService.setupSubscription( + for: accountId, + chainId: chain.chainId, + chainFormat: chain.chainFormat + ) + + logger.debug("Relaychain staking account data subscription succeeded") + } else { + logger.debug("Relaychain staking global data subscription skipped") + } + } + + func throttle() { + if let globalSubscriptionId = globalSubscriptionId { + globalRemoteSubscriptionService.detachFromGlobalData( + for: globalSubscriptionId, + chainId: chain.chainId, + queue: .main + ) { [weak self] result in + switch result { + case .success: + self?.logger.debug("Relaychain staking global data unsubscribe succeeded") + case let .failure(error): + self?.logger.error("Relaychain staking global data unsubscribe failed: \(error)") + } + } + } + + eraValidatorService.throttle() + rewardCalculatorService.throttle() + timeModel.blockTimeService?.throttle() + + accountRemoteSubscriptionService.clearSubscription() + } + + func createNetworkInfoOperationFactory( + for operationQueue: OperationQueue + ) -> NetworkStakingInfoOperationFactoryProtocol { + let durationFactory = consensusDependingFactory.createStakingDurationOperationFactory( + for: stakingOption.chainAsset.chain, + timeModel: timeModel + ) + + return consensusDependingFactory.createNetworkInfoOperationFactory( + for: durationFactory, + operationQueue: operationQueue + ) + } + + func createEraCountdownOperationFactory( + for operationQueue: OperationQueue + ) -> EraCountdownOperationFactoryProtocol { + consensusDependingFactory.createEraCountdownOperationFactory( + for: stakingOption.chainAsset.chain, + timeModel: timeModel, + operationQueue: operationQueue + ) + } + + func createStakingDurationOperationFactory() -> StakingDurationOperationFactoryProtocol { + consensusDependingFactory.createStakingDurationOperationFactory( + for: stakingOption.chainAsset.chain, + timeModel: timeModel + ) + } +} diff --git a/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainStartStakingState.swift b/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainStartStakingState.swift new file mode 100644 index 0000000000..afb0c5285d --- /dev/null +++ b/novawallet/Modules/Staking/Model/StakingSharedState/RelaychainStartStakingState.swift @@ -0,0 +1,229 @@ +import Foundation + +protocol RelaychainStartStakingStateProtocol: AnyObject { + var stakingType: StakingType? { get } + var consensus: ConsensusType { get } + var chainAsset: ChainAsset { get } + + var recommendsMultipleStakings: Bool { get } + + var relaychainGlobalSubscriptionService: StakingRemoteSubscriptionServiceProtocol { get } + var timeModel: StakingTimeModel { get } + var relaychainAccountSubscriptionService: StakingAccountUpdatingServiceProtocol { get } + var relaychainLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol { get } + var eraValidatorService: EraValidatorServiceProtocol { get } + var relaychainRewardCalculatorService: RewardCalculatorServiceProtocol { get } + + var npRemoteSubscriptionService: NominationPoolsRemoteSubscriptionServiceProtocol? { get } + var npAccountSubscriptionServiceFactory: NominationPoolsAccountUpdatingFactoryProtocol? { get } + var activePoolsService: EraNominationPoolsServiceProtocol? { get } + var npLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol { get } + + func setup(for accountId: AccountId?) throws + func throttle() + func supportsPoolStaking() -> Bool + + func createNetworkInfoOperationFactory( + for operationQueue: OperationQueue + ) -> NetworkStakingInfoOperationFactoryProtocol + + func createEraCountdownOperationFactory( + for operationQueue: OperationQueue + ) -> EraCountdownOperationFactoryProtocol + + func createStakingDurationOperationFactory() -> StakingDurationOperationFactoryProtocol +} + +final class RelaychainStartStakingState: RelaychainStartStakingStateProtocol { + let stakingType: StakingType? + let consensus: ConsensusType + let chainAsset: ChainAsset + + let relaychainGlobalSubscriptionService: StakingRemoteSubscriptionServiceProtocol + let relaychainAccountSubscriptionService: StakingAccountUpdatingServiceProtocol + let timeModel: StakingTimeModel + let relaychainLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol + let eraValidatorService: EraValidatorServiceProtocol + let relaychainRewardCalculatorService: RewardCalculatorServiceProtocol + let logger: LoggerProtocol + + let npRemoteSubscriptionService: NominationPoolsRemoteSubscriptionServiceProtocol? + let npAccountSubscriptionServiceFactory: NominationPoolsAccountUpdatingFactoryProtocol? + let npLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol + let activePoolsService: EraNominationPoolsServiceProtocol? + + private var relaychainGlobalSubscriptionId: UUID? + private var npGlobalSubscriptionId: UUID? + private var npAccountService: NominationPoolsAccountUpdatingService? + + private lazy var consensusDependingFactory = RelaychainConsensusStateDependingFactory() + + var recommendsMultipleStakings: Bool { + stakingType == nil && chainAsset.asset.hasMultipleStakingOptions + } + + init( + stakingType: StakingType?, + consensus: ConsensusType, + chainAsset: ChainAsset, + relaychainGlobalSubscriptionService: StakingRemoteSubscriptionServiceProtocol, + relaychainAccountSubscriptionService: StakingAccountUpdatingServiceProtocol, + timeModel: StakingTimeModel, + relaychainLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol, + eraValidatorService: EraValidatorServiceProtocol, + relaychainRewardCalculatorService: RewardCalculatorServiceProtocol, + npRemoteSubstriptionService: NominationPoolsRemoteSubscriptionServiceProtocol?, + npAccountSubscriptionServiceFactory: NominationPoolsAccountUpdatingFactoryProtocol?, + npLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, + activePoolsService: EraNominationPoolsServiceProtocol?, + logger: LoggerProtocol + ) { + self.stakingType = stakingType + self.chainAsset = chainAsset + self.consensus = consensus + self.relaychainGlobalSubscriptionService = relaychainGlobalSubscriptionService + self.timeModel = timeModel + self.relaychainAccountSubscriptionService = relaychainAccountSubscriptionService + self.relaychainLocalSubscriptionFactory = relaychainLocalSubscriptionFactory + self.eraValidatorService = eraValidatorService + self.relaychainRewardCalculatorService = relaychainRewardCalculatorService + npRemoteSubscriptionService = npRemoteSubstriptionService + self.npAccountSubscriptionServiceFactory = npAccountSubscriptionServiceFactory + self.npLocalSubscriptionFactory = npLocalSubscriptionFactory + self.activePoolsService = activePoolsService + self.logger = logger + } + + func setup(for accountId: AccountId?) throws { + let chainId = chainAsset.chain.chainId + + relaychainGlobalSubscriptionId = relaychainGlobalSubscriptionService.attachToGlobalData( + for: chainId, + queue: .main + ) { [weak self] result in + switch result { + case .success: + self?.logger.debug("Relaychain global subscription succeeded") + case let .failure(error): + self?.logger.error("Relaychain global subscription failed: \(error)") + } + } + + eraValidatorService.setup() + relaychainRewardCalculatorService.setup() + timeModel.blockTimeService?.setup() + + npGlobalSubscriptionId = npRemoteSubscriptionService?.attachToGlobalData( + for: chainId, + queue: .main + ) { [weak self] result in + switch result { + case .success: + self?.logger.debug("Nomination pools global subscription succeeded") + case let .failure(error): + self?.logger.error("Nomination pools global subscription failed: \(error)") + } + } + + activePoolsService?.setup() + + if let accountId = accountId { + try relaychainAccountSubscriptionService.setupSubscription( + for: accountId, + chainId: chainId, + chainFormat: chainAsset.chain.chainFormat + ) + + npAccountService = try npAccountSubscriptionServiceFactory?.create( + for: accountId, + chainAsset: chainAsset + ) + + npAccountService?.setup() + } + } + + func throttle() { + let chainId = chainAsset.chain.chainId + + if let relaychainGlobalSubscriptionId = relaychainGlobalSubscriptionId { + relaychainGlobalSubscriptionService.detachFromGlobalData( + for: relaychainGlobalSubscriptionId, + chainId: chainId, + queue: .main + ) { [weak self] result in + switch result { + case .success: + self?.logger.debug("Relaychain global data unsubscribe succeeded") + case let .failure(error): + self?.logger.error("Relaychain global data unsubscribe failed: \(error)") + } + } + + self.relaychainGlobalSubscriptionId = nil + } + + if let npGlobalSubscriptionId = npGlobalSubscriptionId { + npRemoteSubscriptionService?.detachFromGlobalData( + for: npGlobalSubscriptionId, + chainId: chainId, + queue: .main + ) { [weak self] result in + switch result { + case .success: + self?.logger.debug("Nomination pools global data unsubscribe succeeded") + case let .failure(error): + self?.logger.error("Nomination pools global data unsubscribe failed: \(error)") + } + } + + self.npGlobalSubscriptionId = nil + } + + eraValidatorService.throttle() + relaychainRewardCalculatorService.throttle() + timeModel.blockTimeService?.throttle() + + relaychainAccountSubscriptionService.clearSubscription() + + activePoolsService?.throttle() + + npAccountService?.throttle() + npAccountService = nil + } + + func supportsPoolStaking() -> Bool { + npRemoteSubscriptionService != nil + } + + func createNetworkInfoOperationFactory( + for operationQueue: OperationQueue + ) -> NetworkStakingInfoOperationFactoryProtocol { + let durationFactory = consensusDependingFactory.createStakingDurationOperationFactory( + for: chainAsset.chain, + timeModel: timeModel + ) + + return consensusDependingFactory.createNetworkInfoOperationFactory( + for: durationFactory, + operationQueue: operationQueue + ) + } + + func createEraCountdownOperationFactory( + for operationQueue: OperationQueue + ) -> EraCountdownOperationFactoryProtocol { + consensusDependingFactory.createEraCountdownOperationFactory( + for: chainAsset.chain, + timeModel: timeModel, + operationQueue: operationQueue + ) + } + + func createStakingDurationOperationFactory() -> StakingDurationOperationFactoryProtocol { + consensusDependingFactory.createStakingDurationOperationFactory( + for: chainAsset.chain, + timeModel: timeModel + ) + } +} diff --git a/novawallet/Modules/Staking/Model/StakingSharedState/StakingSharedStateFactory.swift b/novawallet/Modules/Staking/Model/StakingSharedState/StakingSharedStateFactory.swift new file mode 100644 index 0000000000..f9a50a6832 --- /dev/null +++ b/novawallet/Modules/Staking/Model/StakingSharedState/StakingSharedStateFactory.swift @@ -0,0 +1,402 @@ +import Foundation +import SubstrateSdk +import RobinHood + +protocol StakingSharedStateFactoryProtocol { + func createRelaychain( + for stakingOption: Multistaking.ChainAssetOption + ) throws -> RelaychainStakingSharedStateProtocol + + func createNominationPools( + for chainAsset: ChainAsset, + consensus: ConsensusType + ) throws -> NPoolsStakingSharedStateProtocol + + func createParachain( + for stakingOption: Multistaking.ChainAssetOption + ) throws -> ParachainStakingSharedStateProtocol + + func createStartRelaychainStaking( + for chainAsset: ChainAsset, + consensus: ConsensusType, + selectedStakingType: StakingType? + ) throws -> RelaychainStartStakingStateProtocol +} + +enum StakingSharedStateFactoryError: Error { + case unsupported +} + +final class StakingSharedStateFactory { + struct RelaychainGlobalCommonServices { + let globalRemoteSubscriptionService: StakingRemoteSubscriptionServiceProtocol + let eraValidatorService: EraValidatorServiceProtocol + let rewardCalculatorService: RewardCalculatorServiceProtocol + let timeModel: StakingTimeModel + let localSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol + } + + struct RelaychainCommonServices { + let globalRemoteSubscriptionService: StakingRemoteSubscriptionServiceProtocol + let accountRemoteSubscriptionService: StakingAccountUpdatingServiceProtocol + let eraValidatorService: EraValidatorServiceProtocol + let rewardCalculatorService: RewardCalculatorServiceProtocol + let timeModel: StakingTimeModel + let localSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol + } + + struct NominationPoolsServices { + let remoteSubscriptionService: NominationPoolsRemoteSubscriptionServiceProtocol? + let accountSubscriptionServiceFactory: NominationPoolsAccountUpdatingFactoryProtocol? + let localSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol + let activePoolsService: EraNominationPoolsServiceProtocol? + } + + let storageFacade: StorageFacadeProtocol + let chainRegistry: ChainRegistryProtocol + let eventCenter: EventCenterProtocol + let syncOperationQueue: OperationQueue + let repositoryOperationQueue: OperationQueue + let logger: LoggerProtocol + + init( + storageFacade: StorageFacadeProtocol, + chainRegistry: ChainRegistryProtocol, + eventCenter: EventCenterProtocol, + syncOperationQueue: OperationQueue, + repositoryOperationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.storageFacade = storageFacade + self.chainRegistry = chainRegistry + self.eventCenter = eventCenter + self.syncOperationQueue = syncOperationQueue + self.repositoryOperationQueue = repositoryOperationQueue + self.logger = logger + } + + private func createRelaychainGlobalCommonServices( + for consensus: ConsensusType, + chainAsset: ChainAsset + ) throws -> RelaychainGlobalCommonServices { + let substrateRepositoryFactory = SubstrateRepositoryFactory(storageFacade: storageFacade) + + let substrateRepository = substrateRepositoryFactory.createChainStorageItemRepository() + let globalRemoteSubscriptionService = StakingRemoteSubscriptionService( + chainRegistry: chainRegistry, + repository: substrateRepository, + syncOperationManager: OperationManager(operationQueue: syncOperationQueue), + repositoryOperationManager: OperationManager(operationQueue: repositoryOperationQueue), + logger: logger + ) + + let stakingServiceFactory = StakingServiceFactory( + chainRegisty: chainRegistry, + storageFacade: storageFacade, + eventCenter: eventCenter, + operationQueue: syncOperationQueue, + logger: logger + ) + + let chainId = chainAsset.chain.chainId + let eraValidatorService = try stakingServiceFactory.createEraValidatorService(for: chainId) + + let timeModel = try stakingServiceFactory.createTimeModel(for: chainId, consensus: consensus) + + let durationFactory = RelaychainConsensusStateDependingFactory().createStakingDurationOperationFactory( + for: chainAsset.chain, + timeModel: timeModel + ) + + let localSubscriptionFactory = StakingLocalSubscriptionFactory( + chainRegistry: chainRegistry, + storageFacade: storageFacade, + operationManager: OperationManager(operationQueue: repositoryOperationQueue), + logger: logger + ) + + let rewardCalculatorService = try stakingServiceFactory.createRewardCalculatorService( + for: chainAsset, + stakingType: consensus.stakingType, + stakingLocalSubscriptionFactory: localSubscriptionFactory, + stakingDurationFactory: durationFactory, + validatorService: eraValidatorService + ) + + return .init( + globalRemoteSubscriptionService: globalRemoteSubscriptionService, + eraValidatorService: eraValidatorService, + rewardCalculatorService: rewardCalculatorService, + timeModel: timeModel, + localSubscriptionFactory: localSubscriptionFactory + ) + } + + private func createRelaychainCommonServices( + for consensus: ConsensusType, + chainAsset: ChainAsset + ) throws -> RelaychainCommonServices { + let globalServices = try createRelaychainGlobalCommonServices(for: consensus, chainAsset: chainAsset) + + let substrateRepositoryFactory = SubstrateRepositoryFactory(storageFacade: storageFacade) + + let substrateDataProviderFactory = SubstrateDataProviderFactory( + facade: storageFacade, + operationManager: OperationManager(operationQueue: repositoryOperationQueue) + ) + + let childSubscriptionFactory = ChildSubscriptionFactory( + storageFacade: storageFacade, + operationManager: OperationManager(operationQueue: repositoryOperationQueue), + eventCenter: eventCenter, + logger: logger + ) + + let accountRemoteSubscriptionService = StakingAccountUpdatingService( + chainRegistry: chainRegistry, + substrateRepositoryFactory: substrateRepositoryFactory, + substrateDataProviderFactory: substrateDataProviderFactory, + childSubscriptionFactory: childSubscriptionFactory, + operationQueue: syncOperationQueue + ) + + return .init( + globalRemoteSubscriptionService: globalServices.globalRemoteSubscriptionService, + accountRemoteSubscriptionService: accountRemoteSubscriptionService, + eraValidatorService: globalServices.eraValidatorService, + rewardCalculatorService: globalServices.rewardCalculatorService, + timeModel: globalServices.timeModel, + localSubscriptionFactory: globalServices.localSubscriptionFactory + ) + } + + // swiftlint:disable:next function_body_length + func createNominationPoolsServices( + for chainAsset: ChainAsset, + eraValidatorService: EraValidatorServiceProtocol + ) throws -> NominationPoolsServices { + let localSubscriptionFactory = NPoolsLocalSubscriptionFactory( + chainRegistry: chainRegistry, + storageFacade: storageFacade, + operationManager: OperationManager(operationQueue: repositoryOperationQueue), + logger: logger + ) + + guard chainAsset.asset.supportsNominationPoolsStaking else { + return NominationPoolsServices( + remoteSubscriptionService: nil, + accountSubscriptionServiceFactory: nil, + localSubscriptionFactory: localSubscriptionFactory, + activePoolsService: nil + ) + } + + let substrateRepositoryFactory = SubstrateRepositoryFactory(storageFacade: storageFacade) + + let substrateRepository = substrateRepositoryFactory.createChainStorageItemRepository() + + let remoteSubsriptionService = NominationPoolsRemoteSubscriptionService( + chainRegistry: chainRegistry, + repository: substrateRepository, + syncOperationManager: OperationManager(operationQueue: syncOperationQueue), + repositoryOperationManager: OperationManager(operationQueue: repositoryOperationQueue), + logger: logger + ) + + let poolSubscriptionService = NominationPoolsPoolSubscriptionService( + chainRegistry: chainRegistry, + repository: substrateRepository, + syncOperationManager: OperationManager(operationQueue: syncOperationQueue), + repositoryOperationManager: OperationManager(operationQueue: repositoryOperationQueue), + logger: logger + ) + + let accountServiceFactory = NominationPoolsAccountUpdatingFactory( + chainRegistry: chainRegistry, + repositoryFactory: substrateRepositoryFactory, + remoteSubscriptionService: poolSubscriptionService, + npoolsLocalSubscriptionFactory: localSubscriptionFactory, + operationQueue: repositoryOperationQueue, + logger: logger + ) + + guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { + throw ChainRegistryError.runtimeMetadaUnavailable + } + + let remoteOperationFactory = NominationPoolsOperationFactory(operationQueue: syncOperationQueue) + + let activePoolsService = EraNominationPoolsService( + chainAsset: chainAsset, + runtimeCodingService: runtimeService, + operationFactory: remoteOperationFactory, + npoolsLocalSubscriptionFactory: localSubscriptionFactory, + eraValidatorService: eraValidatorService, + operationQueue: syncOperationQueue + ) + + return .init( + remoteSubscriptionService: remoteSubsriptionService, + accountSubscriptionServiceFactory: accountServiceFactory, + localSubscriptionFactory: localSubscriptionFactory, + activePoolsService: activePoolsService + ) + } +} + +extension StakingSharedStateFactory: StakingSharedStateFactoryProtocol { + func createRelaychain( + for stakingOption: Multistaking.ChainAssetOption + ) throws -> RelaychainStakingSharedStateProtocol { + guard let consensus = ConsensusType(stakingType: stakingOption.type) else { + throw StakingSharedStateFactoryError.unsupported + } + + let services = try createRelaychainCommonServices(for: consensus, chainAsset: stakingOption.chainAsset) + + return RelaychainStakingSharedState( + consensus: consensus, + stakingOption: stakingOption, + globalRemoteSubscriptionService: services.globalRemoteSubscriptionService, + accountRemoteSubscriptionService: services.accountRemoteSubscriptionService, + localSubscriptionFactory: services.localSubscriptionFactory, + eraValidatorService: services.eraValidatorService, + rewardCalculatorService: services.rewardCalculatorService, + timeModel: services.timeModel, + logger: logger + ) + } + + func createNominationPools( + for chainAsset: ChainAsset, + consensus: ConsensusType + ) throws -> NPoolsStakingSharedStateProtocol { + let relaychainServices = try createRelaychainGlobalCommonServices(for: consensus, chainAsset: chainAsset) + let nominationPoolServices = try createNominationPoolsServices( + for: chainAsset, + eraValidatorService: relaychainServices.eraValidatorService + ) + + guard + let npRemoteSubscriptionService = nominationPoolServices.remoteSubscriptionService, + let npAccountSubscriptionServiceFactory = nominationPoolServices.accountSubscriptionServiceFactory, + let activePoolsService = nominationPoolServices.activePoolsService else { + throw ChainAccountFetchingError.accountNotExists + } + + return NPoolsStakingSharedState( + chainAsset: chainAsset, + relaychainGlobalSubscriptionService: relaychainServices.globalRemoteSubscriptionService, + timeModel: relaychainServices.timeModel, + relaychainLocalSubscriptionFactory: relaychainServices.localSubscriptionFactory, + eraValidatorService: relaychainServices.eraValidatorService, + rewardCalculatorService: relaychainServices.rewardCalculatorService, + npRemoteSubscriptionService: npRemoteSubscriptionService, + npAccountSubscriptionServiceFactory: npAccountSubscriptionServiceFactory, + activePoolsService: activePoolsService, + npLocalSubscriptionFactory: nominationPoolServices.localSubscriptionFactory, + logger: logger + ) + } + + // swiftlint:disable:next function_body_length + func createParachain( + for stakingOption: Multistaking.ChainAssetOption + ) throws -> ParachainStakingSharedStateProtocol { + let repositoryFactory = SubstrateRepositoryFactory() + let repository = repositoryFactory.createChainStorageItemRepository() + + let stakingAccountService = ParachainStaking.AccountSubscriptionService( + chainRegistry: chainRegistry, + repository: repository, + syncOperationManager: OperationManager(operationQueue: syncOperationQueue), + repositoryOperationManager: OperationManager(operationQueue: repositoryOperationQueue), + logger: logger + ) + + let stakingAssetService = ParachainStaking.StakingRemoteSubscriptionService( + chainRegistry: chainRegistry, + repository: repository, + syncOperationManager: OperationManager(operationQueue: syncOperationQueue), + repositoryOperationManager: OperationManager(operationQueue: repositoryOperationQueue), + logger: logger + ) + + let localSubscriptionFactory = ParachainStakingLocalSubscriptionFactory( + chainRegistry: chainRegistry, + storageFacade: storageFacade, + operationManager: OperationManager(operationQueue: repositoryOperationQueue), + logger: logger + ) + + let serviceFactory = ParachainStakingServiceFactory( + stakingProviderFactory: localSubscriptionFactory, + chainRegisty: chainRegistry, + storageFacade: storageFacade, + eventCenter: eventCenter, + operationQueue: syncOperationQueue, + logger: logger + ) + + let chainId = stakingOption.chainAsset.chain.chainId + + let collatorService = try serviceFactory.createSelectedCollatorsService(for: chainId) + let blockTimeService = try serviceFactory.createBlockTimeService(for: chainId) + let rewardService = try serviceFactory.createRewardCalculatorService( + for: chainId, + stakingType: stakingOption.type, + assetPrecision: stakingOption.chainAsset.asset.decimalPrecision, + collatorService: collatorService + ) + + let generalLocalSubscriptionFactory = GeneralStorageSubscriptionFactory( + chainRegistry: chainRegistry, + storageFacade: storageFacade, + operationManager: OperationManager(operationQueue: repositoryOperationQueue), + logger: logger + ) + + return ParachainStakingSharedState( + stakingOption: stakingOption, + chainRegistry: chainRegistry, + globalRemoteSubscriptionService: stakingAssetService, + accountRemoteSubscriptionService: stakingAccountService, + collatorService: collatorService, + rewardCalculationService: rewardService, + blockTimeService: blockTimeService, + stakingLocalSubscriptionFactory: localSubscriptionFactory, + generalLocalSubscriptionFactory: generalLocalSubscriptionFactory, + logger: logger + ) + } + + func createStartRelaychainStaking( + for chainAsset: ChainAsset, + consensus: ConsensusType, + selectedStakingType: StakingType? + ) throws -> RelaychainStartStakingStateProtocol { + let relaychainServices = try createRelaychainCommonServices(for: consensus, chainAsset: chainAsset) + + let nominationPoolsService: NominationPoolsServices = try createNominationPoolsServices( + for: chainAsset, + eraValidatorService: relaychainServices.eraValidatorService + ) + + return RelaychainStartStakingState( + stakingType: selectedStakingType, + consensus: consensus, + chainAsset: chainAsset, + relaychainGlobalSubscriptionService: relaychainServices.globalRemoteSubscriptionService, + relaychainAccountSubscriptionService: relaychainServices.accountRemoteSubscriptionService, + timeModel: relaychainServices.timeModel, + relaychainLocalSubscriptionFactory: relaychainServices.localSubscriptionFactory, + eraValidatorService: relaychainServices.eraValidatorService, + relaychainRewardCalculatorService: relaychainServices.rewardCalculatorService, + npRemoteSubstriptionService: nominationPoolsService.remoteSubscriptionService, + npAccountSubscriptionServiceFactory: nominationPoolsService.accountSubscriptionServiceFactory, + npLocalSubscriptionFactory: nominationPoolsService.localSubscriptionFactory, + activePoolsService: nominationPoolsService.activePoolsService, + logger: logger + ) + } +} diff --git a/novawallet/Modules/Staking/Model/StakingTimeModel.swift b/novawallet/Modules/Staking/Model/StakingTimeModel.swift new file mode 100644 index 0000000000..4cabfe2571 --- /dev/null +++ b/novawallet/Modules/Staking/Model/StakingTimeModel.swift @@ -0,0 +1,18 @@ +import Foundation + +enum StakingTimeModel { + case babe + case auraGeneral(BlockTimeEstimationServiceProtocol) + case azero(BlockTimeEstimationServiceProtocol) + + var blockTimeService: BlockTimeEstimationServiceProtocol? { + switch self { + case .babe: + return nil + case let .auraGeneral(blockTimeEstimationService): + return blockTimeEstimationService + case let .azero(blockTimeEstimationService): + return blockTimeEstimationService + } + } +} diff --git a/novawallet/Modules/Staking/Model/StakingType.swift b/novawallet/Modules/Staking/Model/StakingType.swift index 60c40c37cc..a74638e1c0 100644 --- a/novawallet/Modules/Staking/Model/StakingType.swift +++ b/novawallet/Modules/Staking/Model/StakingType.swift @@ -6,6 +6,7 @@ enum StakingType: String, Codable, Equatable, Hashable { case azero = "aleph-zero" case auraRelaychain = "aura-relaychain" case turing + case nominationPools = "nomination-pools" case unsupported init(rawType: String?) { @@ -15,4 +16,42 @@ enum StakingType: String, Codable, Equatable, Hashable { self = .unsupported } } + + func isMorePreferred(than stakingType: StakingType) -> Bool { + StakingClass(stakingType: self).preferringRating < StakingClass(stakingType: stakingType).preferringRating + } +} + +enum StakingClass { + case relaychain + case parachain + case nominationPools + case unsupported + + // lesser better + var preferringRating: UInt8 { + switch self { + case .relaychain: + return 0 + case .parachain: + return 1 + case .nominationPools: + return 2 + case .unsupported: + return 3 + } + } + + init(stakingType: StakingType) { + switch stakingType { + case .relaychain, .azero, .auraRelaychain: + self = .relaychain + case .parachain, .turing: + self = .parachain + case .nominationPools: + self = .nominationPools + case .unsupported: + self = .unsupported + } + } } diff --git a/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchInteractor.swift b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchInteractor.swift new file mode 100644 index 0000000000..1f2905819b --- /dev/null +++ b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchInteractor.swift @@ -0,0 +1,204 @@ +import UIKit +import SubstrateSdk +import RobinHood + +final class NominationPoolSearchInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning { + weak var presenter: NominationPoolSearchInteractorOutputProtocol? + + let poolsOperationFactory: NominationPoolsOperationFactoryProtocol + let eraNominationPoolsService: EraNominationPoolsServiceProtocol + let validatorRewardService: RewardCalculatorServiceProtocol + let connection: JSONRPCEngine + let runtimeService: RuntimeCodingServiceProtocol + let npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol + let rewardEngineOperationFactory: NPoolsRewardEngineFactoryProtocol + let chainAsset: ChainAsset + let searchOperationFactory: NominationPoolSearchOperationFactoryProtocol + + private var lastPoolIdProvider: AnyDataProvider? + private var maxPoolMembersIdProvider: AnyDataProvider? + private var lastPoolId: NominationPools.PoolId? + private var currentSearchOperation: CancellableCall? + private var searchOperationClosure: NominationPoolSearchOperationClosure? + + private var poolsCancellable: CancellableCall? + private let operationQueue: OperationQueue + private lazy var operationManager = OperationManager(operationQueue: operationQueue) + + private var currentSearchText: String? + + init( + chainAsset: ChainAsset, + poolsOperationFactory: NominationPoolsOperationFactoryProtocol, + npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, + rewardEngineOperationFactory: NPoolsRewardEngineFactoryProtocol, + eraNominationPoolsService: EraNominationPoolsServiceProtocol, + validatorRewardService: RewardCalculatorServiceProtocol, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol, + searchOperationFactory: NominationPoolSearchOperationFactoryProtocol, + operationQueue: OperationQueue + ) { + self.chainAsset = chainAsset + self.poolsOperationFactory = poolsOperationFactory + self.npoolsLocalSubscriptionFactory = npoolsLocalSubscriptionFactory + self.rewardEngineOperationFactory = rewardEngineOperationFactory + self.eraNominationPoolsService = eraNominationPoolsService + self.validatorRewardService = validatorRewardService + self.connection = connection + self.runtimeService = runtimeService + self.searchOperationFactory = searchOperationFactory + self.operationQueue = operationQueue + } + + deinit { + clear(cancellable: &poolsCancellable) + clear(cancellable: ¤tSearchOperation) + } + + private func performLastPoolIdSubscription() { + clear(dataProvider: &lastPoolIdProvider) + lastPoolIdProvider = subscribeLastPoolId(for: chainAsset.chain.chainId) + } + + private func performMaxPoolMembersSubscription() { + clear(dataProvider: &maxPoolMembersIdProvider) + maxPoolMembersIdProvider = subscribeMaxPoolMembersPerPool(for: chainAsset.chain.chainId) + } + + private func fetchAllPoolsInfo() { + guard let lastPoolId = lastPoolId else { + return + } + clear(cancellable: &poolsCancellable) + + let maxApyWrapper = rewardEngineOperationFactory.createEngineWrapper( + for: eraNominationPoolsService, + validatorRewardService: validatorRewardService, + connection: connection, + runtimeService: runtimeService + ) + + let poolStatsWrapper: CompoundOperationWrapper<[NominationPools.PoolStats]?> = + OperationCombiningService.compoundWrapper(operationManager: operationManager) { [weak self] in + guard let self = self else { + return nil + } + let maxApy = try maxApyWrapper.targetOperation.extractNoCancellableResultData() + + return self.poolsOperationFactory.createAllPoolsInfoWrapper( + rewardEngine: { maxApy }, + lastPoolId: lastPoolId, + connection: self.connection, + runtimeService: self.runtimeService + ) + } + + poolStatsWrapper.addDependency(wrapper: maxApyWrapper) + + poolStatsWrapper.targetOperation.completionBlock = { [weak self] in + guard poolStatsWrapper === self?.poolsCancellable else { + return + } + self?.poolsCancellable = nil + + DispatchQueue.main.async { + do { + let stats = try poolStatsWrapper.targetOperation.extractNoCancellableResultData() ?? [] + self?.searchOperationClosure = self?.searchOperationFactory.createOperationClosure(stats: stats) + self?.performSearchIfNeeded() + } catch { + self?.presenter?.didReceive(error: .pools(error)) + } + } + } + + poolsCancellable = poolStatsWrapper + operationQueue.addOperations( + maxApyWrapper.allOperations + poolStatsWrapper.allOperations, + waitUntilFinished: false + ) + } + + private func performSearchIfNeeded() { + guard let text = currentSearchText, let searchOperationClosure = searchOperationClosure else { + return + } + + if text.isEmpty { + presenter?.didReceive(poolStats: []) + } else { + clear(cancellable: ¤tSearchOperation) + let searchOperation = searchOperationClosure(text) + searchOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard self?.currentSearchOperation === searchOperation else { + return + } + + self?.currentSearchOperation = nil + + let result = (try? searchOperation.extractNoCancellableResultData()) ?? [] + + if !result.isEmpty { + self?.presenter?.didReceive(poolStats: result) + } else { + self?.presenter?.didReceive(error: .emptySearchResults) + } + } + } + + currentSearchOperation = searchOperation + operationQueue.addOperation(searchOperation) + } + } +} + +extension NominationPoolSearchInteractor: NominationPoolSearchInteractorInputProtocol { + func setup() { + performLastPoolIdSubscription() + performMaxPoolMembersSubscription() + fetchAllPoolsInfo() + } + + func refetchPools() { + fetchAllPoolsInfo() + } + + func remakeSubscriptions() { + performLastPoolIdSubscription() + performMaxPoolMembersSubscription() + } + + func search(for text: String) { + currentSearchText = text + + guard searchOperationClosure != nil else { + presenter?.didStartSearch(for: text) + return + } + + performSearchIfNeeded() + } +} + +extension NominationPoolSearchInteractor: NPoolsLocalStorageSubscriber, NPoolsLocalSubscriptionHandler { + func handleLastPoolId(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(value): + lastPoolId = value + fetchAllPoolsInfo() + case let .failure(error): + presenter?.didReceive(error: .pools(error)) + } + } + + func handleMaxPoolMembersPerPool(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(maxPoolMembersPerPool): + presenter?.didReceive(maxMembersPerPool: maxPoolMembersPerPool) + case let .failure(error): + presenter?.didReceive(error: .subscription(error)) + } + } +} diff --git a/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchManager.swift b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchManager.swift new file mode 100644 index 0000000000..1d65b11a80 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchManager.swift @@ -0,0 +1,36 @@ +import RobinHood + +final class NominationPoolSearchManager { + let models: [NominationPools.PoolStats] + let searchKeysExtractor: (NominationPools.PoolId) -> [String] + let keyExtractor: (NominationPools.PoolStats) -> NominationPools.PoolId + + init(stats: [NominationPools.PoolStats]) { + models = stats + + let mappedModels = models.reduce( + into: [NominationPools.PoolId: NominationPools.PoolStats]()) { result, element in + result[element.poolId] = element + } + + keyExtractor = { stats in + stats.poolId + } + + searchKeysExtractor = { poolId in + [ + mappedModels[poolId]?.metadata.map { String(data: $0, encoding: .utf8) } ?? nil, + "\(poolId)" + ].compactMap { $0 } + } + } + + func searchOperation(text: String) -> BaseOperation<[NominationPools.PoolStats]> { + SearchOperationFactory.searchOperation( + text: text, + in: models, + keyExtractor: keyExtractor, + searchKeysExtractor: searchKeysExtractor + ) + } +} diff --git a/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchOperationFactory.swift b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchOperationFactory.swift new file mode 100644 index 0000000000..cc92dfa28a --- /dev/null +++ b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchOperationFactory.swift @@ -0,0 +1,13 @@ +import RobinHood + +typealias NominationPoolSearchOperationClosure = (String) -> BaseOperation<[NominationPools.PoolStats]> + +protocol NominationPoolSearchOperationFactoryProtocol { + func createOperationClosure(stats: [NominationPools.PoolStats]) -> NominationPoolSearchOperationClosure +} + +final class NominationPoolSearchOperationFactory: NominationPoolSearchOperationFactoryProtocol { + func createOperationClosure(stats: [NominationPools.PoolStats]) -> NominationPoolSearchOperationClosure { + NominationPoolSearchManager(stats: stats).searchOperation + } +} diff --git a/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchPresenter.swift b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchPresenter.swift new file mode 100644 index 0000000000..cbae142c87 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchPresenter.swift @@ -0,0 +1,151 @@ +import Foundation +import SoraFoundation +import RobinHood + +final class NominationPoolSearchPresenter: AnyCancellableCleaning { + weak var view: NominationPoolSearchViewProtocol? + weak var delegate: StakingSelectPoolDelegate? + + let wireframe: NominationPoolSearchWireframeProtocol + let interactor: NominationPoolSearchInteractorInputProtocol + let viewModelFactory: StakingSelectPoolViewModelFactoryProtocol + let chainAsset: ChainAsset + let logger: LoggerProtocol + let operationQueue: OperationQueue + let dataValidatingFactory: NominationPoolDataValidatorFactoryProtocol + let selectedPoolId: NominationPools.PoolId? + + private var poolStats: LoadableViewModelState<[NominationPools.PoolStats]> = .loaded(value: []) + private var maxMembersPerPool: UInt32? + + init( + interactor: NominationPoolSearchInteractorInputProtocol, + wireframe: NominationPoolSearchWireframeProtocol, + selectedPoolId: NominationPools.PoolId?, + dataValidatingFactory: NominationPoolDataValidatorFactoryProtocol, + viewModelFactory: StakingSelectPoolViewModelFactoryProtocol, + chainAsset: ChainAsset, + delegate: StakingSelectPoolDelegate, + operationQueue: OperationQueue, + localizationManager: LocalizationManagerProtocol, + logger: LoggerProtocol + ) { + self.interactor = interactor + self.wireframe = wireframe + self.selectedPoolId = selectedPoolId + self.dataValidatingFactory = dataValidatingFactory + self.viewModelFactory = viewModelFactory + self.chainAsset = chainAsset + self.operationQueue = operationQueue + self.delegate = delegate + self.logger = logger + self.localizationManager = localizationManager + } + + private func provideVidewModel() { + switch poolStats { + case .loading: + view?.didReceivePools(state: .loading) + case let .cached(stats), let .loaded(stats): + let viewModel = viewModelFactory.createStakingSelectPoolViewModels( + from: stats, + selectedPoolId: selectedPoolId, + chainAsset: chainAsset, + locale: selectedLocale + ) + view?.didReceivePools(state: .loaded(viewModel: viewModel)) + } + } +} + +extension NominationPoolSearchPresenter: NominationPoolSearchPresenterProtocol { + func setup() { + interactor.setup() + view?.didReceivePools(state: .loaded(viewModel: [])) + } + + func search(for textEntry: String) { + interactor.search(for: textEntry) + } + + func selectPool(poolId: NominationPools.PoolId) { + let optPool = poolStats.value?.first(where: { $0.poolId == poolId }) + + DataValidationRunner(validators: [ + dataValidatingFactory.selectedPoolIsOpen(for: optPool, locale: selectedLocale), + dataValidatingFactory.selectedPoolIsNotFull(for: optPool, maxMembers: nil, locale: selectedLocale) + ]).runValidation { [weak self] in + guard let pool = optPool else { + return + } + + self?.delegate?.changePoolSelection( + selectedPool: .init(poolStats: pool), + isRecommended: false + ) + + self?.wireframe.complete(from: self?.view) + } + } + + func showPoolInfo(poolId: NominationPools.PoolId) { + guard let view = view, let pool = poolStats.value?.first(where: { $0.poolId == poolId }) else { + return + } + guard let address = try? pool.bondedAccountId.toAddress(using: chainAsset.chain.chainFormat) else { + return + } + wireframe.presentAccountOptions( + from: view, + address: address, + chain: chainAsset.chain, + locale: selectedLocale + ) + } +} + +extension NominationPoolSearchPresenter: NominationPoolSearchInteractorOutputProtocol { + func didReceive(poolStats: [NominationPools.PoolStats]) { + self.poolStats = .loaded(value: poolStats) + provideVidewModel() + } + + func didStartSearch(for _: String) { + poolStats = .loading + + provideVidewModel() + } + + func didReceive(maxMembersPerPool: UInt32?) { + logger.debug("Max members per pool: \(String(describing: maxMembersPerPool))") + + self.maxMembersPerPool = maxMembersPerPool + } + + func didReceive(error: NominationPoolSearchError) { + logger.error("Did receive error: \(error)") + + switch error { + case .pools: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.refetchPools() + } + case .subscription: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.remakeSubscriptions() + } + case .emptySearchResults: + let emptyMessage = R.string.localizable.stakingSearchPoolEmpty( + preferredLanguages: selectedLocale.rLanguages) + view?.didReceivePools(state: .error(emptyMessage)) + } + } +} + +extension NominationPoolSearchPresenter: Localizable { + func applyLocalization() { + if view?.isSetup == true { + provideVidewModel() + } + } +} diff --git a/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchProtocols.swift b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchProtocols.swift new file mode 100644 index 0000000000..ef536c2fcc --- /dev/null +++ b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchProtocols.swift @@ -0,0 +1,33 @@ +protocol NominationPoolSearchViewProtocol: ControllerBackedProtocol { + func didReceivePools(state: GenericViewState<[StakingSelectPoolViewModel]>) +} + +protocol NominationPoolSearchPresenterProtocol: TableSearchPresenterProtocol { + func selectPool(poolId: NominationPools.PoolId) + func showPoolInfo(poolId: NominationPools.PoolId) +} + +protocol NominationPoolSearchInteractorInputProtocol: AnyObject { + func setup() + func search(for text: String) + func refetchPools() + func remakeSubscriptions() +} + +protocol NominationPoolSearchInteractorOutputProtocol: AnyObject { + func didReceive(poolStats: [NominationPools.PoolStats]) + func didReceive(maxMembersPerPool: UInt32?) + func didStartSearch(for text: String) + func didReceive(error: NominationPoolSearchError) +} + +protocol NominationPoolSearchWireframeProtocol: ErrorPresentable, AddressOptionsPresentable, AlertPresentable, + CommonRetryable, NominationPoolErrorPresentable { + func complete(from view: ControllerBackedProtocol?) +} + +enum NominationPoolSearchError: Error { + case pools(Error) + case subscription(Error) + case emptySearchResults +} diff --git a/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchViewController.swift b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchViewController.swift new file mode 100644 index 0000000000..828a45a81a --- /dev/null +++ b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchViewController.swift @@ -0,0 +1,217 @@ +import UIKit +import SoraUI +import SoraFoundation + +final class NominationPoolSearchViewController: BaseTableSearchViewController { + typealias RootViewType = NominationPoolSearchViewLayout + + var presenter: NominationPoolSearchPresenterProtocol? { + basePresenter as? NominationPoolSearchPresenterProtocol + } + + let keyboardAppearanceStrategy: KeyboardAppearanceStrategyProtocol + private var state: GenericViewState<[StakingSelectPoolViewModel]> = .loaded(viewModel: []) + + init( + presenter: NominationPoolSearchPresenterProtocol, + localizationManager: LocalizationManagerProtocol, + keyboardAppearanceStrategy: KeyboardAppearanceStrategyProtocol + ) { + self.keyboardAppearanceStrategy = keyboardAppearanceStrategy + super.init(basePresenter: presenter) + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = NominationPoolSearchViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupTableView() + setupLocalization() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + keyboardAppearanceStrategy.onViewWillAppear(for: rootView.searchField) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + keyboardAppearanceStrategy.onViewDidAppear(for: rootView.searchField) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + rootView.searchField.resignFirstResponder() + } + + private func setupTableView() { + rootView.tableView.registerClassForCell(StakingPoolTableViewCell.self) + rootView.tableView.registerHeaderFooterView(withClass: StakingSelectPoolListHeaderView.self) + rootView.tableView.dataSource = self + rootView.tableView.delegate = self + } + + private func setupLocalization() { + title = R.string.localizable.commonSearch(preferredLanguages: selectedLocale.rLanguages) + rootView.searchField.placeholder = R.string.localizable.stakingSearchPoolPlaceholder( + preferredLanguages: selectedLocale.rLanguages) + rootView.tableView.reloadData() + } +} + +extension NominationPoolSearchViewController: NominationPoolSearchViewProtocol { + func didReceivePools(state: GenericViewState<[StakingSelectPoolViewModel]>) { + guard let rootView = self.rootView as? NominationPoolSearchViewLayout else { + return + } + self.state = state + rootView.tableView.isHidden = shouldDisplayEmptyState + reloadEmptyState(animated: false) + + switch state { + case .loading: + rootView.loadingView.isHidden = false + rootView.loadingView.start() + case let .loaded(viewModels): + rootView.loadingView.isHidden = true + rootView.loadingView.stop() + + guard !viewModels.isEmpty else { + return + } + rootView.tableView.reloadData() + case .error: + rootView.loadingView.isHidden = true + rootView.loadingView.stop() + } + } +} + +extension NominationPoolSearchViewController: UITableViewDataSource { + func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { + state.viewModel?.count ?? 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: StakingPoolTableViewCell = tableView.dequeueReusableCell(for: indexPath) + if let model = state.viewModel?[safe: indexPath.row] { + cell.bind(viewModel: model) + cell.infoAction = { [weak self] viewModel in + self?.presenter?.showPoolInfo(poolId: viewModel.id) + } + } + return cell + } +} + +extension NominationPoolSearchViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + guard let model = state.viewModel?[safe: indexPath.row] else { + return + } + presenter?.selectPool(poolId: model.id) + } + + func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat { + 44 + } + + func tableView(_: UITableView, heightForHeaderInSection _: Int) -> CGFloat { + guard let viewModels = state.viewModel, !viewModels.isEmpty else { + return 0 + } + return 26 + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection _: Int) -> UIView? { + guard let viewModels = state.viewModel, !viewModels.isEmpty else { + return nil + } + let header: StakingSelectPoolListHeaderView = tableView.dequeueReusableHeaderFooterView() + let title = R.string.localizable.commonSearchResultsNumber( + viewModels.count, + preferredLanguages: selectedLocale.rLanguages + ) + let details = R.string.localizable.stakingSelectPoolMembers(preferredLanguages: selectedLocale.rLanguages) + + header.bind( + title: title, + details: details + ) + return header + } +} + +// MARK: - EmptyStateViewOwnerProtocol + +extension NominationPoolSearchViewController: EmptyStateViewOwnerProtocol { + var emptyStateDelegate: EmptyStateDelegate { self } + var emptyStateDataSource: EmptyStateDataSource { self } +} + +// MARK: - EmptyStateDataSource + +extension NominationPoolSearchViewController: EmptyStateDataSource { + var viewForEmptyState: UIView? { + let emptyView = EmptyStateView() + switch state { + case let .error(text): + emptyView.image = R.image.iconEmptySearch() + emptyView.title = text + case .loaded: + emptyView.image = R.image.iconStartSearch() + emptyView.title = R.string.localizable + .commonSearchStartTitle_v2_2_0(preferredLanguages: selectedLocale.rLanguages) + case .loading: + return nil + } + + emptyView.titleColor = R.color.colorTextSecondary()! + emptyView.titleFont = .regularFootnote + return emptyView + } + + var contentViewForEmptyState: UIView { + rootView.emptyStateContainer + } + + var verticalSpacingForEmptyState: CGFloat? { + 26 + } +} + +// MARK: - EmptyStateDelegate + +extension NominationPoolSearchViewController: EmptyStateDelegate { + var shouldDisplayEmptyState: Bool { + switch state { + case .error: + return true + case let .loaded(viewModels): + return viewModels.isEmpty + case .loading: + return false + } + } +} + +extension NominationPoolSearchViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} diff --git a/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchViewFactory.swift b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchViewFactory.swift new file mode 100644 index 0000000000..2fe47b9c3c --- /dev/null +++ b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchViewFactory.swift @@ -0,0 +1,90 @@ +import Foundation +import SoraFoundation + +struct NominationPoolSearchViewFactory { + static func createView( + state: RelaychainStartStakingStateProtocol, + delegate: StakingSelectPoolDelegate, + selectedPoolId: NominationPools.PoolId? + ) -> NominationPoolSearchViewProtocol? { + guard + let interactor = createInteractor(for: state), + let currencyManager = CurrencyManager.shared else { + return nil + } + + let wireframe = NominationPoolSearchWireframe() + + let viewModelFactory = StakingSelectPoolViewModelFactory( + apyFormatter: NumberFormatter.percentAPY.localizableResource(), + membersFormatter: NumberFormatter.quantity.localizableResource(), + poolIconFactory: NominationPoolsIconFactory() + ) + + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: state.chainAsset.assetDisplayInfo, + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager) + ) + + let dataValidatingFactory = NominationPoolDataValidatorFactory( + presentable: wireframe, + balanceFactory: balanceViewModelFactory + ) + + let presenter = NominationPoolSearchPresenter( + interactor: interactor, + wireframe: wireframe, + selectedPoolId: selectedPoolId, + dataValidatingFactory: dataValidatingFactory, + viewModelFactory: viewModelFactory, + chainAsset: state.chainAsset, + delegate: delegate, + operationQueue: OperationManagerFacade.sharedDefaultQueue, + localizationManager: LocalizationManager.shared, + logger: Logger.shared + ) + + let view = NominationPoolSearchViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared, + keyboardAppearanceStrategy: EventDrivenKeyboardStrategy(events: [.viewDidAppear]) + ) + + presenter.view = view + interactor.presenter = presenter + dataValidatingFactory.view = view + + return view + } + + static func createInteractor(for state: RelaychainStartStakingStateProtocol) -> NominationPoolSearchInteractor? { + let chainId = state.chainAsset.chain.chainId + let chainRegistry = ChainRegistryFacade.sharedRegistry + + guard + let runtimeService = chainRegistry.getRuntimeProvider(for: chainId), + let connection = chainRegistry.getConnection(for: chainId), + let activePoolService = state.activePoolsService else { + return nil + } + + let queue = OperationManagerFacade.sharedDefaultQueue + let poolsOperationFactory = NominationPoolsOperationFactory(operationQueue: queue) + let rewardCalculationFactory = NPoolsRewardEngineFactory(operationFactory: poolsOperationFactory) + + let interactor = NominationPoolSearchInteractor( + chainAsset: state.chainAsset, + poolsOperationFactory: poolsOperationFactory, + npoolsLocalSubscriptionFactory: state.npLocalSubscriptionFactory, + rewardEngineOperationFactory: rewardCalculationFactory, + eraNominationPoolsService: activePoolService, + validatorRewardService: state.relaychainRewardCalculatorService, + connection: connection, + runtimeService: runtimeService, + searchOperationFactory: NominationPoolSearchOperationFactory(), + operationQueue: queue + ) + + return interactor + } +} diff --git a/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchViewLayout.swift b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchViewLayout.swift new file mode 100644 index 0000000000..801aec1b68 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchViewLayout.swift @@ -0,0 +1,24 @@ +import UIKit + +final class NominationPoolSearchViewLayout: BaseTableSearchViewLayout { + let loadingView: ListLoadingView = .create { + $0.isHidden = true + } + + override init(frame: CGRect) { + super.init(frame: frame) + + tableView.estimatedRowHeight = 44 + tableView.separatorStyle = .none + } + + override func setupLayout() { + super.setupLayout() + + addSubview(loadingView) + loadingView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(16) + make.centerY.equalToSuperview() + } + } +} diff --git a/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchWireframe.swift b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchWireframe.swift new file mode 100644 index 0000000000..3ff52f0570 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPoolSearch/NominationPoolSearchWireframe.swift @@ -0,0 +1,12 @@ +import Foundation + +final class NominationPoolSearchWireframe: NominationPoolSearchWireframeProtocol { + func complete(from view: ControllerBackedProtocol?) { + if let amountView: StakingSetupAmountViewProtocol = view?.controller.navigationController?.findTopView() { + view?.controller.navigationController?.popToViewController( + amountView.controller, + animated: true + ) + } + } +} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseInteractor.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseInteractor.swift new file mode 100644 index 0000000000..3755eb093f --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseInteractor.swift @@ -0,0 +1,347 @@ +import UIKit +import RobinHood +import BigInt + +class NominationPoolBondMoreBaseInteractor: AnyProviderAutoCleaning, AnyCancellableCleaning, + NominationPoolsDataProviding { + weak var basePresenter: NominationPoolBondMoreBaseInteractorOutputProtocol? + let chainAsset: ChainAsset + let selectedAccount: MetaChainAccountResponse + let feeProxy: ExtrinsicFeeProxyProtocol + let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol + let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol + let callFactory: SubstrateCallFactoryProtocol + let extrinsicService: ExtrinsicServiceProtocol + let npoolsOperationFactory: NominationPoolsOperationFactoryProtocol + let runtimeService: RuntimeCodingServiceProtocol + let npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol + let stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol + let assetStorageInfoFactory: AssetStorageInfoOperationFactoryProtocol + + private var operationQueue: OperationQueue + private lazy var operationManager = OperationManager(operationQueue: operationQueue) + + private var priceProvider: StreamableProvider? + private var balanceProvider: StreamableProvider? + private var poolMemberProvider: AnyDataProvider? + private var bondedPoolProvider: AnyDataProvider? + private var claimableRewardProvider: AnySingleValueProvider? + private var rewardPoolProvider: AnyDataProvider? + + private var bondedAccountIdCancellable: CancellableCall? + private var assetExistenceCancellable: CancellableCall? + + private var accountId: AccountId { selectedAccount.chainAccount.accountId } + private var currentPoolId: NominationPools.PoolId? + private var currentPoolRewardCounter: BigUInt? + private var currentMemberRewardCounter: BigUInt? + private var poolAccountId: AccountId? + + var chainId: ChainModel.Id { chainAsset.chain.chainId } + + init( + chainAsset: ChainAsset, + selectedAccount: MetaChainAccountResponse, + runtimeService: RuntimeCodingServiceProtocol, + feeProxy: ExtrinsicFeeProxyProtocol, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + callFactory: SubstrateCallFactoryProtocol, + extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, + npoolsOperationFactory: NominationPoolsOperationFactoryProtocol, + npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, + stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol, + assetStorageInfoFactory: AssetStorageInfoOperationFactoryProtocol, + operationQueue: OperationQueue, + currencyManager: CurrencyManagerProtocol + ) { + self.chainAsset = chainAsset + self.selectedAccount = selectedAccount + self.feeProxy = feeProxy + self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory + self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory + self.operationQueue = operationQueue + self.callFactory = callFactory + self.npoolsOperationFactory = npoolsOperationFactory + self.runtimeService = runtimeService + self.npoolsLocalSubscriptionFactory = npoolsLocalSubscriptionFactory + self.stakingLocalSubscriptionFactory = stakingLocalSubscriptionFactory + self.assetStorageInfoFactory = assetStorageInfoFactory + + extrinsicService = extrinsicServiceFactory.createService( + account: selectedAccount.chainAccount, + chain: chainAsset.chain + ) + + self.currencyManager = currencyManager + } + + func subscribeAccountBalance() { + clear(streamableProvider: &balanceProvider) + + balanceProvider = subscribeToAssetBalanceProvider( + for: selectedAccount.chainAccount.accountId, + chainId: chainAsset.chain.chainId, + assetId: chainAsset.asset.assetId + ) + } + + func subscribePrice() { + clear(streamableProvider: &priceProvider) + + if let priceId = chainAsset.asset.priceId { + priceProvider = subscribeToPrice(for: priceId, currency: selectedCurrency) + } else { + basePresenter?.didReceive(price: nil) + } + } + + func createExtrinsicClosure(for points: BigUInt) -> ExtrinsicBuilderClosure { + { builder in + let call = NominationPools.BondExtraCall(extra: .freeBalance(points)) + return try builder.adding(call: call.runtimeCall()) + } + } + + func subscribePoolProviders() { + guard let poolId = currentPoolId else { + return + } + + bondedPoolProvider = subscribeBondedPool(for: poolId, chainId: chainId) + rewardPoolProvider = subscribeRewardPool(for: poolId, chainId: chainId) + + subscribeClaimableRewardsProvider() + } + + func subscribeClaimableRewardsProvider() { + guard let poolId = currentPoolId else { + return + } + + claimableRewardProvider = subscribeClaimableRewards( + for: chainId, + poolId: poolId, + accountId: accountId + ) + + if claimableRewardProvider == nil { + basePresenter?.didReceive(error: .claimableRewards(CommonError.dataCorruption)) + } + } + + func subscribePoolMember() { + clear(dataProvider: &poolMemberProvider) + poolMemberProvider = subscribePoolMember(for: accountId, chainId: chainId) + } + + func provideAssetExistence() { + let assetInfoWrapper = assetStorageInfoFactory.createStorageInfoWrapper( + from: chainAsset.asset, + runtimeProvider: runtimeService + ) + + let assetBalanceExistenceWrapper: CompoundOperationWrapper = + OperationCombiningService.compoundWrapper(operationManager: operationManager) { [weak self] in + guard let self = self else { + return nil + } + let assetInfo = try assetInfoWrapper.targetOperation.extractNoCancellableResultData() + + return self.assetStorageInfoFactory.createAssetBalanceExistenceOperation( + for: assetInfo, + chainId: self.chainAsset.chain.chainId, + asset: self.chainAsset.asset + ) + } + assetBalanceExistenceWrapper.addDependency(wrapper: assetInfoWrapper) + + let wrapper = CompoundOperationWrapper( + targetOperation: assetBalanceExistenceWrapper.targetOperation, + dependencies: assetInfoWrapper.allOperations + assetBalanceExistenceWrapper.dependencies + ) + + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard self?.assetExistenceCancellable === wrapper else { + return + } + self?.assetExistenceCancellable = nil + + do { + let assetExistence = try wrapper.targetOperation.extractNoCancellableResultData() + self?.basePresenter?.didReceive(assetBalanceExistance: assetExistence) + } catch { + self?.basePresenter?.didReceive(error: .assetExistance(error)) + } + } + } + + assetExistenceCancellable = wrapper + + operationQueue.addOperations( + wrapper.allOperations, + waitUntilFinished: false + ) + } +} + +extension NominationPoolBondMoreBaseInteractor: NominationPoolBondMoreBaseInteractorInputProtocol { + func setup() { + feeProxy.delegate = self + + subscribeAccountBalance() + subscribePoolMember() + subscribePrice() + provideAssetExistence() + } + + func estimateFee(for amount: BigUInt) { + let reuseIdentifier = String(amount) + feeProxy.estimateFee( + using: extrinsicService, + reuseIdentifier: reuseIdentifier, + setupBy: createExtrinsicClosure(for: amount) + ) + } + + func retrySubscriptions() { + subscribeAccountBalance() + subscribePoolMember() + subscribePrice() + } + + func retryClaimableRewards() { + subscribeClaimableRewardsProvider() + } + + func retryAssetExistance() { + provideAssetExistence() + } +} + +extension NominationPoolBondMoreBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { + func handleAssetBalance( + result: Result, + accountId _: AccountId, + chainId _: ChainModel.Id, + assetId _: AssetModel.Id + ) { + switch result { + case let .success(balance): + basePresenter?.didReceive(assetBalance: balance) + case let .failure(error): + basePresenter?.didReceive(error: .subscription(error, "asset balance")) + } + } +} + +extension NominationPoolBondMoreBaseInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { + func handlePrice( + result: Result, + priceId _: AssetModel.PriceId + ) { + switch result { + case let .success(priceData): + basePresenter?.didReceive(price: priceData) + case let .failure(error): + basePresenter?.didReceive(error: .subscription(error, "price")) + } + } +} + +extension NominationPoolBondMoreBaseInteractor: ExtrinsicFeeProxyDelegate { + func didReceiveFee(result: Result, for _: TransactionFeeId) { + switch result { + case let .success(dispatchInfo): + let fee = BigUInt(dispatchInfo.fee) + basePresenter?.didReceive(fee: fee) + case let .failure(error): + basePresenter?.didReceive(error: .fetchFeeFailed(error)) + } + } +} + +extension NominationPoolBondMoreBaseInteractor: SelectedCurrencyDepending { + func applyCurrency() { + guard basePresenter != nil, + let priceId = chainAsset.asset.priceId else { + return + } + + priceProvider = subscribeToPrice(for: priceId, currency: selectedCurrency) + } +} + +extension NominationPoolBondMoreBaseInteractor: NPoolsLocalStorageSubscriber, NPoolsLocalSubscriptionHandler { + func handlePoolMember( + result: Result, + accountId _: AccountId, chainId _: ChainModel.Id + ) { + switch result { + case let .success(poolMember): + if currentPoolId != poolMember?.poolId { + currentPoolId = poolMember?.poolId + + subscribePoolProviders() + } + + if currentMemberRewardCounter != poolMember?.lastRecordedRewardCounter { + currentMemberRewardCounter = poolMember?.lastRecordedRewardCounter + + claimableRewardProvider?.refresh() + } + + basePresenter?.didReceive(poolMember: poolMember) + case let .failure(error): + basePresenter?.didReceive(error: .subscription(error, "pool member")) + } + } + + func handleRewardPool( + result: Result, + poolId: NominationPools.PoolId, + chainId _: ChainModel.Id + ) { + guard currentPoolId == poolId else { + return + } + + if case let .success(rewardPool) = result, rewardPool?.lastRecordedRewardCounter != currentPoolRewardCounter { + self.currentPoolRewardCounter = rewardPool?.lastRecordedRewardCounter + + claimableRewardProvider?.refresh() + } + } + + func handleBondedPool( + result: Result, + poolId _: NominationPools.PoolId, + chainId _: ChainModel.Id + ) { + switch result { + case let .success(bondedPool): + basePresenter?.didReceive(bondedPool: bondedPool) + case let .failure(error): + basePresenter?.didReceive(error: .subscription(error, "bonded pool")) + } + } + + func handleClaimableRewards( + result: Result, + chainId _: ChainModel.Id, + poolId: NominationPools.PoolId, + accountId _: AccountId + ) { + guard currentPoolId == poolId else { + return + } + + switch result { + case let .success(rewards): + basePresenter?.didReceive(claimableRewards: rewards) + case let .failure(error): + basePresenter?.didReceive(error: .subscription(error, "claimable rewards")) + } + } +} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBasePresenter.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBasePresenter.swift new file mode 100644 index 0000000000..b6bba7eff7 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBasePresenter.swift @@ -0,0 +1,189 @@ +import Foundation +import BigInt +import SoraFoundation + +class NominationPoolBondMoreBasePresenter: NominationPoolBondMoreBaseInteractorOutputProtocol { + weak var baseView: NominationPoolBondMoreBaseViewProtocol? + let baseWireframe: NominationPoolBondMoreBaseWireframeProtocol + let baseInteractor: NominationPoolBondMoreBaseInteractorInputProtocol + + let chainAsset: ChainAsset + let hintsViewModelFactory: NominationPoolsBondMoreHintsFactoryProtocol + let logger: LoggerProtocol + let balanceViewModelFactory: BalanceViewModelFactoryProtocol + let dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol + + var assetBalance: AssetBalance? + var poolMember: NominationPools.PoolMember? + var bondedPool: NominationPools.BondedPool? + var price: PriceData? + var fee: BigUInt? + var claimableRewards: BigUInt? + var assetBalanceExistance: AssetBalanceExistence? + + init( + interactor: NominationPoolBondMoreBaseInteractorInputProtocol, + wireframe: NominationPoolBondMoreBaseWireframeProtocol, + chainAsset: ChainAsset, + hintsViewModelFactory: NominationPoolsBondMoreHintsFactoryProtocol, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol, + localizationManager: LocalizationManagerProtocol, + logger: LoggerProtocol + ) { + baseInteractor = interactor + baseWireframe = wireframe + self.logger = logger + self.chainAsset = chainAsset + self.hintsViewModelFactory = hintsViewModelFactory + self.balanceViewModelFactory = balanceViewModelFactory + self.dataValidatorFactory = dataValidatorFactory + self.localizationManager = localizationManager + } + + func updateView() { + fatalError("Must be overriden by subsclass") + } + + func provideFee() { + fatalError("Must be overriden by subsclass") + } + + func getInputAmount() -> Decimal? { + fatalError("Must be overriden by subsclass") + } + + func getInputAmountInPlank() -> BigUInt? { + fatalError("Must be overriden by subsclass") + } + + func provideHints() { + let hints = hintsViewModelFactory.createHints( + rewards: claimableRewards, + locale: selectedLocale + ) + + baseView?.didReceiveHints(viewModel: hints) + } + + func refreshFee() { + let inputAmount = getInputAmountInPlank() ?? 0 + + fee = nil + + provideFee() + + baseInteractor.estimateFee(for: inputAmount) + } + + func getSpendingAmountInPlank() -> BigUInt? { + guard let inputAmount = getInputAmountInPlank(), + let fee = fee else { + return nil + } + return inputAmount + fee + } + + func getValidations() -> [DataValidating] { + let baseValidators = [ + dataValidatorFactory.hasInPlank( + fee: fee, + locale: selectedLocale, + precision: chainAsset.assetDisplayInfo.assetPrecision + ) { [weak self] in + self?.refreshFee() + }, + dataValidatorFactory.canSpendAmountInPlank( + balance: assetBalance?.transferable, + spendingAmount: getInputAmount(), + asset: chainAsset.assetDisplayInfo, + locale: selectedLocale + ), + dataValidatorFactory.canPayFeeSpendingAmountInPlank( + balance: assetBalance?.transferable, + fee: fee, + spendingAmount: getInputAmount(), + asset: chainAsset.assetDisplayInfo, + locale: selectedLocale + ) + ] + + let poolValidators = [ + dataValidatorFactory.nominationPoolIsNotDestroing( + pool: bondedPool, + locale: selectedLocale + ), + dataValidatorFactory.nominationPoolIsNotFullyUnbonding( + poolMember: poolMember, + locale: selectedLocale + ) + ] + + return baseValidators + poolValidators + } + + // MARK: - NominationPoolBondMoreBaseInteractorOutputProtocol + + func didReceive(assetBalance: AssetBalance?) { + self.assetBalance = assetBalance + } + + func didReceive(poolMember: NominationPools.PoolMember?) { + self.poolMember = poolMember + } + + func didReceive(bondedPool: NominationPools.BondedPool?) { + self.bondedPool = bondedPool + } + + func didReceive(price: PriceData?) { + self.price = price + } + + func didReceive(fee: BigUInt?) { + self.fee = fee + + provideFee() + } + + func didReceive(claimableRewards: BigUInt?) { + self.claimableRewards = claimableRewards + + provideHints() + } + + func didReceive(assetBalanceExistance: AssetBalanceExistence?) { + self.assetBalanceExistance = assetBalanceExistance + } + + func didReceive(error: NominationPoolBondMoreError) { + logger.error(error.localizedDescription) + + switch error { + case .fetchFeeFailed: + baseWireframe.presentFeeStatus(on: baseView, locale: selectedLocale) { [weak self] in + self?.refreshFee() + } + case .subscription: + baseWireframe.presentRequestStatus(on: baseView, locale: selectedLocale) { [weak self] in + self?.baseInteractor.retrySubscriptions() + } + case .claimableRewards: + baseWireframe.presentRequestStatus(on: baseView, locale: selectedLocale) { [weak self] in + self?.baseInteractor.retryClaimableRewards() + } + case .assetExistance: + baseWireframe.presentRequestStatus(on: baseView, locale: selectedLocale) { [weak self] in + self?.baseInteractor.retryAssetExistance() + } + } + } +} + +extension NominationPoolBondMoreBasePresenter: Localizable { + func applyLocalization() { + if baseView?.isSetup == true { + updateView() + } + } +} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseProtocols.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseProtocols.swift new file mode 100644 index 0000000000..0901638f41 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseProtocols.swift @@ -0,0 +1,34 @@ +import BigInt + +protocol NominationPoolBondMoreBaseViewProtocol: ControllerBackedProtocol { + func didReceiveHints(viewModel: [String]) +} + +protocol NominationPoolBondMoreBaseInteractorInputProtocol: AnyObject { + func setup() + func estimateFee(for amount: BigUInt) + func retrySubscriptions() + func retryClaimableRewards() + func retryAssetExistance() +} + +protocol NominationPoolBondMoreBaseInteractorOutputProtocol: AnyObject { + func didReceive(price: PriceData?) + func didReceive(assetBalance: AssetBalance?) + func didReceive(fee: BigUInt?) + func didReceive(error: NominationPoolBondMoreError) + func didReceive(poolMember: NominationPools.PoolMember?) + func didReceive(bondedPool: NominationPools.BondedPool?) + func didReceive(claimableRewards: BigUInt?) + func didReceive(assetBalanceExistance: AssetBalanceExistence?) +} + +protocol NominationPoolBondMoreBaseWireframeProtocol: ErrorPresentable, AlertPresentable, CommonRetryable, + FeeRetryable, NominationPoolErrorPresentable {} + +enum NominationPoolBondMoreError: Error { + case fetchFeeFailed(Error) + case subscription(Error, String) + case claimableRewards(Error) + case assetExistance(Error) +} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseWireframe.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseWireframe.swift new file mode 100644 index 0000000000..7ae7003303 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseWireframe.swift @@ -0,0 +1,3 @@ +import Foundation + +class NominationPoolBondMoreBaseWireframe: NominationPoolBondMoreBaseWireframeProtocol {} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolsBondMoreHintsFactory.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolsBondMoreHintsFactory.swift new file mode 100644 index 0000000000..be81c94abf --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolsBondMoreHintsFactory.swift @@ -0,0 +1,42 @@ +import Foundation +import BigInt + +protocol NominationPoolsBondMoreHintsFactoryProtocol { + func createHints( + rewards: BigUInt?, + locale: Locale + ) -> [String] +} + +final class NominationPoolsBondMoreHintsFactory { + let chainAsset: ChainAsset + let balanceViewModelFactory: BalanceViewModelFactoryProtocol + + init( + chainAsset: ChainAsset, + balanceViewModelFactory: BalanceViewModelFactoryProtocol + ) { + self.chainAsset = chainAsset + self.balanceViewModelFactory = balanceViewModelFactory + } +} + +extension NominationPoolsBondMoreHintsFactory: NominationPoolsBondMoreHintsFactoryProtocol { + func createHints( + rewards: BigUInt?, + locale: Locale + ) -> [String] { + let eraHint = R.string.localizable.stakingHintRewardBondMore_v2_2_0(preferredLanguages: locale.rLanguages) + + var hints: [String] = [eraHint] + + if let rewards = rewards, rewards > 0 { + let decimalAmount = rewards.decimal(precision: chainAsset.asset.precision) + let amount = balanceViewModelFactory.amountFromValue(decimalAmount).value(for: locale) + let hint = R.string.localizable.stakingPoolRewardsClaimHint(amount, preferredLanguages: locale.rLanguages) + hints.append(hint) + } + + return hints + } +} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmInteractor.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmInteractor.swift new file mode 100644 index 0000000000..1976b4885d --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmInteractor.swift @@ -0,0 +1,58 @@ +import UIKit +import BigInt + +final class NominationPoolBondMoreConfirmInteractor: NominationPoolBondMoreBaseInteractor { + weak var presenter: NominationPoolBondMoreConfirmInteractorOutputProtocol? { + basePresenter as? NominationPoolBondMoreConfirmInteractorOutputProtocol + } + + let signingWrapper: SigningWrapperProtocol + + init( + chainAsset: ChainAsset, + selectedAccount: MetaChainAccountResponse, + runtimeService: RuntimeCodingServiceProtocol, + feeProxy: ExtrinsicFeeProxyProtocol, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + callFactory: SubstrateCallFactoryProtocol, + extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, + npoolsOperationFactory: NominationPoolsOperationFactoryProtocol, + npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, + stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol, + assetStorageInfoFactory: AssetStorageInfoOperationFactoryProtocol, + operationQueue: OperationQueue, + currencyManager: CurrencyManagerProtocol, + signingWrapper: SigningWrapperProtocol + ) { + self.signingWrapper = signingWrapper + super.init( + chainAsset: chainAsset, + selectedAccount: selectedAccount, + runtimeService: runtimeService, + feeProxy: feeProxy, + walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, + priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, + callFactory: callFactory, + extrinsicServiceFactory: extrinsicServiceFactory, + npoolsOperationFactory: npoolsOperationFactory, + npoolsLocalSubscriptionFactory: npoolsLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: stakingLocalSubscriptionFactory, + assetStorageInfoFactory: assetStorageInfoFactory, + operationQueue: operationQueue, + currencyManager: currencyManager + ) + } +} + +extension NominationPoolBondMoreConfirmInteractor: NominationPoolBondMoreConfirmInteractorInputProtocol { + func submit(amount: BigUInt) { + extrinsicService.submit( + createExtrinsicClosure(for: amount), + signer: signingWrapper, + runningIn: .main + ) { [weak self] result in + self?.presenter?.didReceive(submissionResult: result) + } + } +} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmPresenter.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmPresenter.swift new file mode 100644 index 0000000000..05f767bacc --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmPresenter.swift @@ -0,0 +1,172 @@ +import Foundation +import SoraFoundation +import BigInt + +final class NominationPoolBondMoreConfirmPresenter: NominationPoolBondMoreBasePresenter { + weak var view: NominationPoolBondMoreConfirmViewProtocol? { + baseView as? NominationPoolBondMoreConfirmViewProtocol + } + + var wireframe: NominationPoolBondMoreConfirmWireframeProtocol? { + baseWireframe as? NominationPoolBondMoreConfirmWireframeProtocol + } + + var interactor: NominationPoolBondMoreConfirmInteractorInputProtocol? { + baseInteractor as? NominationPoolBondMoreConfirmInteractorInputProtocol + } + + let selectedAccount: MetaChainAccountResponse + let amount: Decimal + + private lazy var walletViewModelFactory = WalletAccountViewModelFactory() + private lazy var displayAddressViewModelFactory = DisplayAddressViewModelFactory() + + init( + interactor: NominationPoolBondMoreConfirmInteractorInputProtocol, + wireframe: NominationPoolBondMoreConfirmWireframeProtocol, + amount: Decimal, + selectedAccount: MetaChainAccountResponse, + chainAsset: ChainAsset, + hintsViewModelFactory: NominationPoolsBondMoreHintsFactoryProtocol, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol, + localizationManager: LocalizationManagerProtocol, + logger: LoggerProtocol + ) { + self.selectedAccount = selectedAccount + self.amount = amount + super.init( + interactor: interactor, + wireframe: wireframe, + chainAsset: chainAsset, + hintsViewModelFactory: hintsViewModelFactory, + balanceViewModelFactory: balanceViewModelFactory, + dataValidatorFactory: dataValidatorFactory, + localizationManager: localizationManager, + logger: logger + ) + } + + private func provideAmountViewModel() { + let viewModel = balanceViewModelFactory.balanceFromPrice( + amount, + priceData: price + ).value(for: selectedLocale) + + view?.didReceiveAmount(viewModel: viewModel) + } + + private func provideWalletViewModel() { + do { + let viewModel = try walletViewModelFactory.createDisplayViewModel(from: selectedAccount) + view?.didReceiveWallet(viewModel: viewModel) + } catch { + logger.error("Did receive error: \(error)") + } + } + + private func provideAccountViewModel() { + do { + let viewModel = try walletViewModelFactory.createViewModel(from: selectedAccount) + view?.didReceiveAccount(viewModel: viewModel.rawDisplayAddress()) + } catch { + logger.error("Did receive error: \(error)") + } + } + + override func updateView() { + provideAmountViewModel() + provideWalletViewModel() + provideAccountViewModel() + provideFee() + provideHints() + } + + override func provideFee() { + let viewModel: BalanceViewModelProtocol? = fee.flatMap { amount in + guard let amountDecimal = Decimal.fromSubstrateAmount( + amount, + precision: chainAsset.assetDisplayInfo.assetPrecision + ) else { + return nil + } + + return balanceViewModelFactory.balanceFromPrice( + amountDecimal, + priceData: price + ).value(for: selectedLocale) + } + + view?.didReceiveFee(viewModel: viewModel) + } + + override func getInputAmount() -> Decimal? { + amount + } + + override func getInputAmountInPlank() -> BigUInt? { + amount.toSubstrateAmount(precision: chainAsset.assetDisplayInfo.assetPrecision) + } + + override func didReceive(price: PriceData?) { + super.didReceive(price: price) + + provideAmountViewModel() + provideFee() + } +} + +extension NominationPoolBondMoreConfirmPresenter: NominationPoolBondMoreConfirmPresenterProtocol { + func setup() { + refreshFee() + updateView() + interactor?.setup() + } + + func proceed() { + let validators = getValidations() + + DataValidationRunner( + validators: validators + ).runValidation { [weak self] in + guard let amount = self?.getInputAmountInPlank() else { + return + } + + self?.view?.didStartLoading() + self?.interactor?.submit(amount: amount) + } + } + + func selectAccount() { + guard + let address = selectedAccount.chainAccount.toAddress(), + let view = view else { + return + } + + wireframe?.presentAccountOptions( + from: view, + address: address, + chain: chainAsset.chain, + locale: selectedLocale + ) + } +} + +extension NominationPoolBondMoreConfirmPresenter: NominationPoolBondMoreConfirmInteractorOutputProtocol { + func didReceive(submissionResult: SubmitExtrinsicResult) { + view?.didStopLoading() + + switch submissionResult { + case .success: + wireframe?.presentExtrinsicSubmission(from: view, completionAction: .dismiss, locale: selectedLocale) + case let .failure(error): + if error.isWatchOnlySigning { + wireframe?.presentDismissingNoSigningView(from: view) + } else { + _ = wireframe?.present(error: error, from: view, locale: selectedLocale) + } + } + } +} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmProtocols.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmProtocols.swift new file mode 100644 index 0000000000..6175f996a6 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmProtocols.swift @@ -0,0 +1,27 @@ +import BigInt + +protocol NominationPoolBondMoreConfirmViewProtocol: NominationPoolBondMoreBaseViewProtocol { + func didReceiveAmount(viewModel: BalanceViewModelProtocol) + func didReceiveWallet(viewModel: DisplayWalletViewModel) + func didReceiveAccount(viewModel: DisplayAddressViewModel) + func didReceiveFee(viewModel: BalanceViewModelProtocol?) + func didStartLoading() + func didStopLoading() +} + +protocol NominationPoolBondMoreConfirmPresenterProtocol: AnyObject { + func setup() + func proceed() + func selectAccount() +} + +protocol NominationPoolBondMoreConfirmInteractorInputProtocol: NominationPoolBondMoreBaseInteractorInputProtocol { + func submit(amount: BigUInt) +} + +protocol NominationPoolBondMoreConfirmInteractorOutputProtocol: NominationPoolBondMoreBaseInteractorOutputProtocol { + func didReceive(submissionResult: SubmitExtrinsicResult) +} + +protocol NominationPoolBondMoreConfirmWireframeProtocol: NominationPoolBondMoreBaseWireframeProtocol, + AddressOptionsPresentable, MessageSheetPresentable, ExtrinsicSubmissionPresenting {} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewController.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewController.swift new file mode 100644 index 0000000000..3668c7eb6e --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewController.swift @@ -0,0 +1,117 @@ +import UIKit +import SoraFoundation + +final class NominationPoolBondMoreConfirmViewController: UIViewController, ViewHolder { + typealias RootViewType = NominationPoolBondMoreConfirmViewLayout + + let presenter: NominationPoolBondMoreConfirmPresenterProtocol + + init( + presenter: NominationPoolBondMoreConfirmPresenterProtocol, + localizationManager: LocalizationManagerProtocol + ) { + self.presenter = presenter + super.init(nibName: nil, bundle: nil) + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = NominationPoolBondMoreConfirmViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupLocalization() + setupHandlers() + presenter.setup() + } + + private func setupLocalization() { + let languages = selectedLocale.rLanguages + + title = R.string.localizable.stakingBondMore_v190( + preferredLanguages: languages + ) + + rootView.actionButton.imageWithTitleView?.title = R.string.localizable + .commonConfirm(preferredLanguages: languages) + + rootView.walletCell.titleLabel.text = R.string.localizable.commonWallet( + preferredLanguages: languages + ) + + rootView.accountCell.titleLabel.text = R.string.localizable.commonAccount( + preferredLanguages: languages + ) + + rootView.networkFeeCell.rowContentView.locale = selectedLocale + } + + private func setupHandlers() { + rootView.actionButton.addTarget( + self, + action: #selector(actionConfirm), + for: .touchUpInside + ) + + rootView.accountCell.addTarget( + self, + action: #selector(actionSelectAccount), + for: .touchUpInside + ) + } + + @objc private func actionConfirm() { + presenter.proceed() + } + + @objc private func actionSelectAccount() { + presenter.selectAccount() + } +} + +extension NominationPoolBondMoreConfirmViewController: NominationPoolBondMoreConfirmViewProtocol { + func didReceiveAmount(viewModel: BalanceViewModelProtocol) { + rootView.amountView.bind(viewModel: viewModel) + } + + func didReceiveWallet(viewModel: DisplayWalletViewModel) { + rootView.walletCell.bind(viewModel: viewModel.cellViewModel) + } + + func didReceiveAccount(viewModel: DisplayAddressViewModel) { + rootView.accountCell.bind(viewModel: viewModel.cellViewModel) + } + + func didReceiveFee(viewModel: BalanceViewModelProtocol?) { + rootView.networkFeeCell.rowContentView.bind(viewModel: viewModel) + } + + func didReceiveHints(viewModel: [String]) { + rootView.hintListView.bind(texts: viewModel) + } + + func didStartLoading() { + rootView.loadingView.startLoading() + } + + func didStopLoading() { + rootView.loadingView.stopLoading() + } +} + +extension NominationPoolBondMoreConfirmViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} + +extension NominationPoolBondMoreConfirmViewController: ImportantViewProtocol {} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewFactory.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewFactory.swift new file mode 100644 index 0000000000..7cfd1d59f2 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewFactory.swift @@ -0,0 +1,101 @@ +import Foundation +import SoraFoundation + +struct NominationPoolBondMoreConfirmViewFactory { + static func createView( + state: NPoolsStakingSharedStateProtocol, + amount: Decimal + ) -> NominationPoolBondMoreConfirmViewProtocol? { + guard let interactor = createInteractor(state: state), + let currencyManager = CurrencyManager.shared, + let wallet = SelectedWalletSettings.shared.value, + let selectedAccount = wallet.fetchMetaChainAccount(for: state.chainAsset.chain.accountRequest()) else { + return nil + } + let wireframe = NominationPoolBondMoreConfirmWireframe() + let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: state.chainAsset.assetDisplayInfo, + priceAssetInfoFactory: priceAssetInfoFactory + ) + let hintsViewModelFactory = NominationPoolsBondMoreHintsFactory( + chainAsset: state.chainAsset, + balanceViewModelFactory: balanceViewModelFactory + ) + let localizationManager = LocalizationManager.shared + let dataValidatorFactory = NominationPoolDataValidatorFactory( + presentable: wireframe, + balanceFactory: balanceViewModelFactory + ) + + let presenter = NominationPoolBondMoreConfirmPresenter( + interactor: interactor, + wireframe: wireframe, + amount: amount, + selectedAccount: selectedAccount, + chainAsset: state.chainAsset, + hintsViewModelFactory: hintsViewModelFactory, + balanceViewModelFactory: balanceViewModelFactory, + dataValidatorFactory: dataValidatorFactory, + localizationManager: localizationManager, + logger: Logger.shared + ) + + let view = NominationPoolBondMoreConfirmViewController( + presenter: presenter, + localizationManager: localizationManager + ) + + presenter.baseView = view + interactor.basePresenter = presenter + dataValidatorFactory.view = view + + return view + } + + static func createInteractor(state: NPoolsStakingSharedStateProtocol) -> NominationPoolBondMoreConfirmInteractor? { + let chainAsset = state.chainAsset + + guard + let selectedMetaAccount = SelectedWalletSettings.shared.value, + let currencyManager = CurrencyManager.shared, + let selectedAccount = selectedMetaAccount.fetchMetaChainAccount( + for: chainAsset.chain.accountRequest() + ) else { + return nil + } + + let chainRegistry = ChainRegistryFacade.sharedRegistry + + guard + let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), + let runtimeRegistry = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { + return nil + } + let extrinsicServiceFactory = ExtrinsicServiceFactory( + runtimeRegistry: runtimeRegistry, + engine: connection, + operationManager: OperationManagerFacade.sharedManager + ) + let operationQueue = OperationManagerFacade.sharedDefaultQueue + let signingWrapper = SigningWrapperFactory.createSigner(from: selectedAccount) + + return .init( + chainAsset: chainAsset, + selectedAccount: selectedAccount, + runtimeService: runtimeRegistry, + feeProxy: ExtrinsicFeeProxy(), + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + priceLocalSubscriptionFactory: PriceProviderFactory.shared, + callFactory: SubstrateCallFactory(), + extrinsicServiceFactory: extrinsicServiceFactory, + npoolsOperationFactory: NominationPoolsOperationFactory(operationQueue: operationQueue), + npoolsLocalSubscriptionFactory: state.npLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.relaychainLocalSubscriptionFactory, + assetStorageInfoFactory: AssetStorageInfoOperationFactory(), + operationQueue: operationQueue, + currencyManager: currencyManager, + signingWrapper: signingWrapper + ) + } +} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewLayout.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewLayout.swift new file mode 100644 index 0000000000..ca042c2bb2 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewLayout.swift @@ -0,0 +1,39 @@ +import UIKit + +final class NominationPoolBondMoreConfirmViewLayout: SCLoadableActionLayoutView { + let amountView = MultilineBalanceView() + + let walletTableView = StackTableView() + + let walletCell = StackTableCell() + + let accountCell: StackInfoTableCell = .create { + $0.detailsLabel.lineBreakMode = .byTruncatingMiddle + } + + let networkFeeCell = StackNetworkFeeCell() + + var actionButton: TriangularedButton { + genericActionView.actionButton + } + + var loadingView: LoadableActionView { + genericActionView + } + + let hintListView = HintListView() + + override func setupLayout() { + super.setupLayout() + + addArrangedSubview(amountView, spacingAfter: 24) + + addArrangedSubview(walletTableView, spacingAfter: 16) + + walletTableView.addArrangedSubview(walletCell) + walletTableView.addArrangedSubview(accountCell) + walletTableView.addArrangedSubview(networkFeeCell) + + addArrangedSubview(hintListView) + } +} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmWireframe.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmWireframe.swift new file mode 100644 index 0000000000..ce28f2d984 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmWireframe.swift @@ -0,0 +1,4 @@ +import Foundation + +final class NominationPoolBondMoreConfirmWireframe: NominationPoolBondMoreConfirmWireframeProtocol, + ModalAlertPresenting {} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupInteractor.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupInteractor.swift new file mode 100644 index 0000000000..90e4d21f30 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupInteractor.swift @@ -0,0 +1,7 @@ +import UIKit +import RobinHood +import BigInt + +final class NominationPoolBondMoreSetupInteractor: NominationPoolBondMoreBaseInteractor {} + +extension NominationPoolBondMoreSetupInteractor: NominationPoolBondMoreSetupInteractorInputProtocol {} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupPresenter.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupPresenter.swift new file mode 100644 index 0000000000..d0b2601683 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupPresenter.swift @@ -0,0 +1,188 @@ +import BigInt +import SoraFoundation + +final class NominationPoolBondMoreSetupPresenter: NominationPoolBondMoreBasePresenter { + weak var view: NominationPoolBondMoreSetupViewProtocol? { + baseView as? NominationPoolBondMoreSetupViewProtocol + } + + var wireframe: NominationPoolBondMoreSetupWireframeProtocol? { + baseWireframe as? NominationPoolBondMoreSetupWireframeProtocol + } + + var interactor: NominationPoolBondMoreSetupInteractorInputProtocol? { + baseInteractor as? NominationPoolBondMoreSetupInteractorInputProtocol + } + + private var inputResult: AmountInputResult? + + init( + interactor: NominationPoolBondMoreSetupInteractorInputProtocol, + wireframe: NominationPoolBondMoreSetupWireframeProtocol, + chainAsset: ChainAsset, + hintsViewModelFactory: NominationPoolsBondMoreHintsFactoryProtocol, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol, + localizationManager: LocalizationManagerProtocol, + logger: LoggerProtocol + + ) { + super.init( + interactor: interactor, + wireframe: wireframe, + chainAsset: chainAsset, + hintsViewModelFactory: hintsViewModelFactory, + balanceViewModelFactory: balanceViewModelFactory, + dataValidatorFactory: dataValidatorFactory, + localizationManager: localizationManager, + logger: logger + ) + } + + func provideTransferrableBalance() { + guard let balance = assetBalance?.transferable.decimal(precision: chainAsset.asset.precision) else { + view?.didReceiveTransferable(viewModel: nil) + return + } + let viewModel = balanceViewModelFactory.amountFromValue(balance).value(for: selectedLocale) + view?.didReceiveTransferable(viewModel: viewModel) + } + + func provideAmountInputViewModel() { + let inputAmount = getInputAmount() + let viewModel = balanceViewModelFactory.createBalanceInputViewModel( + inputAmount + ).value(for: selectedLocale) + + view?.didReceiveInput(viewModel: viewModel) + } + + func provideAssetViewModel() { + let balance = assetBalance?.transferable.decimal(precision: chainAsset.asset.precision) ?? 0 + let inputAmount = getInputAmount() ?? 0 + let viewModel = balanceViewModelFactory.createAssetBalanceViewModel( + inputAmount, + balance: balance, + priceData: price + ).value(for: selectedLocale) + + view?.didReceiveAssetBalance(viewModel: viewModel) + } + + override func getInputAmount() -> Decimal? { + guard let inputResult = inputResult else { + return nil + } + let fee = fee?.decimal(precision: chainAsset.asset.precision) ?? 0 + let transferable = assetBalance?.transferable.decimal(precision: chainAsset.asset.precision) ?? 0 + let total = assetBalance?.totalInPlank.decimal(precision: chainAsset.asset.precision) ?? 0 + let existentialDeposit = assetBalanceExistance?.minBalance.decimal(precision: chainAsset.asset.precision) ?? 0 + let value = max(min(transferable - fee, total - fee - existentialDeposit), 0) + return inputResult.absoluteValue(from: value) + } + + override func updateView() { + provideAmountInputViewModel() + provideAssetViewModel() + provideTransferrableBalance() + provideFee() + provideHints() + } + + override func provideFee() { + guard let fee = fee?.decimal(precision: chainAsset.asset.precision) else { + view?.didReceiveFee(viewModel: nil) + return + } + + let viewModel = balanceViewModelFactory.balanceFromPrice(fee, priceData: price).value(for: selectedLocale) + + view?.didReceiveFee(viewModel: viewModel) + } + + override func getInputAmountInPlank() -> BigUInt? { + guard let decimalAmount = getInputAmount() else { + return nil + } + + return decimalAmount.toSubstrateAmount( + precision: chainAsset.assetDisplayInfo.assetPrecision + ) + } + + override func didReceive(assetBalance: AssetBalance?) { + super.didReceive(assetBalance: assetBalance) + + provideAssetViewModel() + provideTransferrableBalance() + provideAmountInputViewModel() + } + + override func didReceive(price: PriceData?) { + super.didReceive(price: price) + + provideAmountInputViewModel() + provideAssetViewModel() + provideTransferrableBalance() + provideFee() + } + + override func didReceive(assetBalanceExistance: AssetBalanceExistence?) { + super.didReceive(assetBalanceExistance: assetBalanceExistance) + + provideAmountInputViewModel() + } +} + +extension NominationPoolBondMoreSetupPresenter: NominationPoolBondMoreSetupPresenterProtocol { + func setup() { + interactor?.setup() + refreshFee() + } + + func selectAmountPercentage(_ percentage: Float) { + inputResult = .rate(Decimal(Double(percentage))) + + provideAmountInputViewModel() + provideAssetViewModel() + refreshFee() + } + + func updateAmount(_ newValue: Decimal?) { + inputResult = newValue.map { AmountInputResult.absolute($0) } + + provideAssetViewModel() + refreshFee() + } + + func proceed() { + let baseValidators = getValidations() + + var currentInputAmount = getInputAmount() + + let setupValidators: [DataValidating] = [ + dataValidatorFactory.poolStakingNotViolatingExistentialDeposit( + for: .init( + stakingAmount: getInputAmount(), + assetBalance: assetBalance, + fee: fee, + existentialDeposit: assetBalanceExistance?.minBalance, + amountUpdateClosure: { newAmount in + currentInputAmount = newAmount + } + ), + chainAsset: chainAsset, + locale: selectedLocale + ) + ] + + DataValidationRunner( + validators: baseValidators + setupValidators + ).runValidation { [weak self] in + guard let amount = currentInputAmount else { + return + } + self?.wireframe?.showConfirm(from: self?.view, amount: amount) + } + } +} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupProtocols.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupProtocols.swift new file mode 100644 index 0000000000..72a790dbd7 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupProtocols.swift @@ -0,0 +1,23 @@ +import BigInt + +protocol NominationPoolBondMoreSetupViewProtocol: NominationPoolBondMoreBaseViewProtocol { + func didReceiveInput(viewModel: AmountInputViewModelProtocol) + func didReceiveFee(viewModel: BalanceViewModelProtocol?) + func didReceiveTransferable(viewModel: String?) + func didReceiveAssetBalance(viewModel: AssetBalanceViewModelProtocol) +} + +protocol NominationPoolBondMoreSetupPresenterProtocol: AnyObject { + func setup() + func selectAmountPercentage(_ percentage: Float) + func updateAmount(_ newValue: Decimal?) + func proceed() +} + +protocol NominationPoolBondMoreSetupInteractorInputProtocol: NominationPoolBondMoreBaseInteractorInputProtocol {} + +protocol NominationPoolBondMoreSetupInteractorOutputProtocol: NominationPoolBondMoreBaseInteractorOutputProtocol {} + +protocol NominationPoolBondMoreSetupWireframeProtocol: NominationPoolBondMoreBaseWireframeProtocol { + func showConfirm(from view: ControllerBackedProtocol?, amount: Decimal) +} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupViewController.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupViewController.swift new file mode 100644 index 0000000000..7dbaf3e463 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupViewController.swift @@ -0,0 +1,168 @@ +import UIKit +import SoraFoundation + +final class NominationPoolBondMoreSetupViewController: UIViewController, ViewHolder { + typealias RootViewType = NominationPoolBondMoreSetupViewLayout + + let presenter: NominationPoolBondMoreSetupPresenterProtocol + private var amountInputViewModel: AmountInputViewModelProtocol? + + init( + presenter: NominationPoolBondMoreSetupPresenterProtocol, + localizationManager: LocalizationManagerProtocol + ) { + self.presenter = presenter + super.init(nibName: nil, bundle: nil) + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = NominationPoolBondMoreSetupViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupHandlers() + setupLocalization() + presenter.setup() + } + + private func setupLocalization() { + let languages = selectedLocale.rLanguages + + title = R.string.localizable.stakingBondMore_v190( + preferredLanguages: languages + ) + + rootView.amountView.titleView.text = R.string.localizable.walletSendAmountTitle( + preferredLanguages: languages + ) + + rootView.amountView.detailsTitleLabel.text = R.string.localizable.commonAvailablePrefix( + preferredLanguages: languages + ) + + rootView.actionButton.imageWithTitleView?.title = R.string.localizable.commonContinue( + preferredLanguages: languages + ) + + rootView.networkFeeView.locale = selectedLocale + + setupAmountInputAccessoryView() + updateProceedButtonState() + } + + private func setupAmountInputAccessoryView() { + let accessoryView = UIFactory.default.createAmountAccessoryView( + for: self, + locale: selectedLocale + ) + + rootView.amountInputView.textField.inputAccessoryView = accessoryView + } + + private func setupHandlers() { + rootView.amountInputView.addTarget( + self, + action: #selector(actionAmountChange), + for: .editingChanged + ) + rootView.actionButton.addTarget( + self, + action: #selector(actionProceed), + for: .touchUpInside + ) + } + + @objc func actionAmountChange() { + let amount = rootView.amountInputView.inputViewModel?.decimalAmount + presenter.updateAmount(amount) + + updateProceedButtonState() + } + + @objc private func actionProceed() { + presenter.proceed() + } + + private func updateProceedButtonState() { + if !rootView.amountInputView.completed { + rootView.actionButton.applyDisabledStyle() + rootView.actionButton.isUserInteractionEnabled = false + + rootView.actionButton.imageWithTitleView?.title = R.string.localizable + .transferSetupEnterAmount(preferredLanguages: selectedLocale.rLanguages) + rootView.actionButton.invalidateLayout() + + return + } + + rootView.actionButton.applyEnabledStyle() + rootView.actionButton.isUserInteractionEnabled = true + + rootView.actionButton.imageWithTitleView?.title = R.string.localizable.commonContinue( + preferredLanguages: selectedLocale.rLanguages + ) + rootView.actionButton.invalidateLayout() + } +} + +extension NominationPoolBondMoreSetupViewController: NominationPoolBondMoreSetupViewProtocol { + func didReceiveInput(viewModel: AmountInputViewModelProtocol) { + rootView.amountInputView.bind(inputViewModel: viewModel) + updateProceedButtonState() + } + + func didReceiveFee(viewModel: BalanceViewModelProtocol?) { + rootView.networkFeeView.bind(viewModel: viewModel) + updateProceedButtonState() + } + + func didReceiveTransferable(viewModel: String?) { + rootView.amountView.detailsValueLabel.text = viewModel + } + + func didReceiveHints(viewModel: [String]) { + rootView.hintListView.bind(texts: viewModel) + } + + func didReceiveAssetBalance(viewModel: AssetBalanceViewModelProtocol) { + let assetViewModel = AssetViewModel( + symbol: viewModel.symbol, + imageViewModel: viewModel.iconViewModel + ) + + rootView.amountInputView.bind(assetViewModel: assetViewModel) + rootView.amountInputView.bind(priceViewModel: viewModel.price) + + rootView.amountView.detailsValueLabel.text = viewModel.balance + } +} + +extension NominationPoolBondMoreSetupViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} + +extension NominationPoolBondMoreSetupViewController: AmountInputAccessoryViewDelegate { + func didSelect(on _: AmountInputAccessoryView, percentage: Float) { + rootView.amountInputView.textField.resignFirstResponder() + + presenter.selectAmountPercentage(percentage) + } + + func didSelectDone(on _: AmountInputAccessoryView) { + rootView.amountInputView.textField.resignFirstResponder() + } +} + +extension NominationPoolBondMoreSetupViewController: ImportantViewProtocol {} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupViewFactory.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupViewFactory.swift new file mode 100644 index 0000000000..54c541a0bb --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupViewFactory.swift @@ -0,0 +1,91 @@ +import Foundation +import SoraFoundation + +struct NominationPoolBondMoreSetupViewFactory { + static func createView(state: NPoolsStakingSharedStateProtocol) -> NominationPoolBondMoreSetupViewProtocol? { + guard let currencyManager = CurrencyManager.shared, + let interactor = createInteractor(state: state) else { + return nil + } + let wireframe = NominationPoolBondMoreSetupWireframe(state: state) + let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: state.chainAsset.assetDisplayInfo, + priceAssetInfoFactory: priceAssetInfoFactory + ) + let hintsViewModelFactory = NominationPoolsBondMoreHintsFactory( + chainAsset: state.chainAsset, + balanceViewModelFactory: balanceViewModelFactory + ) + let localizationManager = LocalizationManager.shared + let dataValidatorFactory = NominationPoolDataValidatorFactory( + presentable: wireframe, + balanceFactory: balanceViewModelFactory + ) + + let presenter = NominationPoolBondMoreSetupPresenter( + interactor: interactor, + wireframe: wireframe, + chainAsset: state.chainAsset, + hintsViewModelFactory: hintsViewModelFactory, + balanceViewModelFactory: balanceViewModelFactory, + dataValidatorFactory: dataValidatorFactory, + localizationManager: localizationManager, + logger: Logger.shared + ) + + let view = NominationPoolBondMoreSetupViewController( + presenter: presenter, + localizationManager: localizationManager + ) + + presenter.baseView = view + interactor.basePresenter = presenter + dataValidatorFactory.view = view + return view + } + + static func createInteractor(state: NPoolsStakingSharedStateProtocol) -> NominationPoolBondMoreSetupInteractor? { + let chainAsset = state.chainAsset + + guard + let selectedMetaAccount = SelectedWalletSettings.shared.value, + let currencyManager = CurrencyManager.shared, + let selectedAccount = selectedMetaAccount.fetchMetaChainAccount( + for: chainAsset.chain.accountRequest() + ) else { + return nil + } + + let chainRegistry = ChainRegistryFacade.sharedRegistry + + guard + let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), + let runtimeRegistry = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { + return nil + } + let extrinsicServiceFactory = ExtrinsicServiceFactory( + runtimeRegistry: runtimeRegistry, + engine: connection, + operationManager: OperationManagerFacade.sharedManager + ) + let operationQueue = OperationManagerFacade.sharedDefaultQueue + + return .init( + chainAsset: chainAsset, + selectedAccount: selectedAccount, + runtimeService: runtimeRegistry, + feeProxy: ExtrinsicFeeProxy(), + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + priceLocalSubscriptionFactory: PriceProviderFactory.shared, + callFactory: SubstrateCallFactory(), + extrinsicServiceFactory: extrinsicServiceFactory, + npoolsOperationFactory: NominationPoolsOperationFactory(operationQueue: operationQueue), + npoolsLocalSubscriptionFactory: state.npLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.relaychainLocalSubscriptionFactory, + assetStorageInfoFactory: AssetStorageInfoOperationFactory(), + operationQueue: operationQueue, + currencyManager: currencyManager + ) + } +} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupViewLayout.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupViewLayout.swift new file mode 100644 index 0000000000..aa4f2f2741 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupViewLayout.swift @@ -0,0 +1,45 @@ +import UIKit + +final class NominationPoolBondMoreSetupViewLayout: SCSingleActionLayoutView { + let amountView = TitleHorizontalMultiValueView() + + let amountInputView = NewAmountInputView() + + let networkFeeView = UIFactory.default.createNetworkFeeView() + + let hintListView = HintListView() + + var actionButton: TriangularedButton { + genericActionView + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = R.color.colorSecondaryScreenBackground() + actionButton.applyDefaultStyle() + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func setupLayout() { + super.setupLayout() + + addArrangedSubview(amountView, spacingAfter: 8) + amountView.snp.makeConstraints { make in + make.height.equalTo(34) + } + + addArrangedSubview(amountInputView, spacingAfter: 16) + amountInputView.snp.makeConstraints { make in + make.height.equalTo(64) + } + + addArrangedSubview(networkFeeView, spacingAfter: 16) + addArrangedSubview(hintListView) + } +} diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupWireframe.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupWireframe.swift new file mode 100644 index 0000000000..5685b58abf --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupWireframe.swift @@ -0,0 +1,18 @@ +import Foundation + +final class NominationPoolBondMoreSetupWireframe: NominationPoolBondMoreBaseWireframe { + let state: NPoolsStakingSharedStateProtocol + + init(state: NPoolsStakingSharedStateProtocol) { + self.state = state + } +} + +extension NominationPoolBondMoreSetupWireframe: NominationPoolBondMoreSetupWireframeProtocol { + func showConfirm(from view: ControllerBackedProtocol?, amount: Decimal) { + guard let confirmView = NominationPoolBondMoreConfirmViewFactory.createView(state: state, amount: amount) else { + return + } + view?.controller.navigationController?.pushViewController(confirmView.controller, animated: true) + } +} diff --git a/novawallet/Modules/Staking/NominationPools/ClaimRewards/Model/NPoolsClaimRewardsError.swift b/novawallet/Modules/Staking/NominationPools/ClaimRewards/Model/NPoolsClaimRewardsError.swift new file mode 100644 index 0000000000..c69494dceb --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/ClaimRewards/Model/NPoolsClaimRewardsError.swift @@ -0,0 +1,6 @@ +import Foundation + +enum NPoolsClaimRewardsError: Error { + case subscription(Error, String) + case fee(Error) +} diff --git a/novawallet/Modules/Staking/NominationPools/ClaimRewards/Model/NPoolsClaimRewardsStrategy.swift b/novawallet/Modules/Staking/NominationPools/ClaimRewards/Model/NPoolsClaimRewardsStrategy.swift new file mode 100644 index 0000000000..ff8f74f4eb --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/ClaimRewards/Model/NPoolsClaimRewardsStrategy.swift @@ -0,0 +1,8 @@ +import Foundation + +extension NominationPools { + enum ClaimRewardsStrategy: String { + case restake + case freeBalance + } +} diff --git a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsInteractor.swift b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsInteractor.swift new file mode 100644 index 0000000000..8f8ae7fd66 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsInteractor.swift @@ -0,0 +1,246 @@ +import UIKit +import RobinHood +import SubstrateSdk +import BigInt + +final class NPoolsClaimRewardsInteractor { + weak var presenter: NPoolsClaimRewardsInteractorOutputProtocol? + + let selectedAccount: MetaChainAccountResponse + let chainAsset: ChainAsset + let extrinsicService: ExtrinsicServiceProtocol + let feeProxy: ExtrinsicFeeProxyProtocol + let signingWrapper: SigningWrapperProtocol + + let npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol + let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol + let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol + + var accountId: AccountId { selectedAccount.chainAccount.accountId } + var chainId: ChainModel.Id { chainAsset.chain.chainId } + var asset: AssetModel { chainAsset.asset } + var assetId: AssetModel.Id { asset.assetId } + + private var poolMemberProvider: AnyDataProvider? + private var balanceProvider: StreamableProvider? + private var priceProvider: StreamableProvider? + private var rewardPoolProvider: AnyDataProvider? + private var claimableRewardProvider: AnySingleValueProvider? + + private var currentPoolId: NominationPools.PoolId? + private var currentPoolRewardCounter: BigUInt? + private var currentMemberRewardCounter: BigUInt? + + init( + selectedAccount: MetaChainAccountResponse, + chainAsset: ChainAsset, + extrinsicService: ExtrinsicServiceProtocol, + feeProxy: ExtrinsicFeeProxyProtocol, + signingWrapper: SigningWrapperProtocol, + npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + currencyManager: CurrencyManagerProtocol + ) { + self.selectedAccount = selectedAccount + self.chainAsset = chainAsset + self.extrinsicService = extrinsicService + self.feeProxy = feeProxy + self.signingWrapper = signingWrapper + self.npoolsLocalSubscriptionFactory = npoolsLocalSubscriptionFactory + self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory + self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory + self.currencyManager = currencyManager + } + + func setupPoolProviders() { + guard let poolId = currentPoolId else { + return + } + + rewardPoolProvider = subscribeRewardPool(for: poolId, chainId: chainId) + + setupClaimableRewardsProvider() + } + + func setupClaimableRewardsProvider() { + guard let poolId = currentPoolId else { + return + } + + claimableRewardProvider = subscribeClaimableRewards( + for: chainId, + poolId: poolId, + accountId: accountId + ) + + if claimableRewardProvider == nil { + presenter?.didReceive(error: .subscription(CommonError.dataCorruption, "rewards")) + } + } + + func setupCurrencyProvider() { + guard let priceId = asset.priceId else { + presenter?.didReceive(price: nil) + return + } + + priceProvider = subscribeToPrice(for: priceId, currency: selectedCurrency) + } + + func setupBaseProviders() { + rewardPoolProvider = nil + claimableRewardProvider = nil + + poolMemberProvider = subscribePoolMember(for: accountId, chainId: chainId) + balanceProvider = subscribeToAssetBalanceProvider(for: accountId, chainId: chainId, assetId: assetId) + + setupCurrencyProvider() + } + + func createExtrinsicBuilderClosure( + for strategy: NominationPools.ClaimRewardsStrategy + ) -> ExtrinsicBuilderClosure { + { builder in + switch strategy { + case .restake: + let bondExtra = NominationPools.BondExtraCall(extra: .rewards) + return try builder.adding(call: bondExtra.runtimeCall()) + case .freeBalance: + let claimRewards = NominationPools.ClaimRewardsCall() + return try builder.adding(call: claimRewards.runtimeCall()) + } + } + } +} + +extension NPoolsClaimRewardsInteractor: NPoolsClaimRewardsInteractorInputProtocol { + func setup() { + feeProxy.delegate = self + + setupBaseProviders() + } + + func remakeSubscriptions() { + setupBaseProviders() + } + + func estimateFee(for strategy: NominationPools.ClaimRewardsStrategy) { + feeProxy.estimateFee( + using: extrinsicService, + reuseIdentifier: strategy.rawValue, + setupBy: createExtrinsicBuilderClosure(for: strategy) + ) + } + + func submit(for strategy: NominationPools.ClaimRewardsStrategy) { + extrinsicService.submit( + createExtrinsicBuilderClosure(for: strategy), + signer: signingWrapper, + runningIn: .main + ) { [weak self] result in + self?.presenter?.didReceive(submissionResult: result) + } + } +} + +extension NPoolsClaimRewardsInteractor: ExtrinsicFeeProxyDelegate { + func didReceiveFee(result: Result, for _: TransactionFeeId) { + switch result { + case let .success(dispatchInfo): + presenter?.didReceive(fee: BigUInt(dispatchInfo.fee)) + case let .failure(error): + presenter?.didReceive(error: .fee(error)) + } + } +} + +extension NPoolsClaimRewardsInteractor: NPoolsLocalStorageSubscriber, NPoolsLocalSubscriptionHandler { + func handlePoolMember( + result: Result, + accountId _: AccountId, chainId _: ChainModel.Id + ) { + switch result { + case let .success(optPoolMember): + if currentPoolId != optPoolMember?.poolId { + currentPoolId = optPoolMember?.poolId + + setupPoolProviders() + } + + if currentMemberRewardCounter != optPoolMember?.lastRecordedRewardCounter { + currentMemberRewardCounter = optPoolMember?.lastRecordedRewardCounter + + claimableRewardProvider?.refresh() + } + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "pool member")) + } + } + + func handleClaimableRewards( + result: Result, + chainId _: ChainModel.Id, + poolId _: NominationPools.PoolId, + accountId _: AccountId + ) { + switch result { + case let .success(rewards): + presenter?.didReceive(claimableRewards: rewards) + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "rewards")) + } + } + + func handleRewardPool( + result: Result, + poolId: NominationPools.PoolId, + chainId _: ChainModel.Id + ) { + guard currentPoolId == poolId else { + return + } + + if case let .success(rewardPool) = result, rewardPool?.lastRecordedRewardCounter != currentPoolRewardCounter { + self.currentPoolRewardCounter = rewardPool?.lastRecordedRewardCounter + + claimableRewardProvider?.refresh() + } + } +} + +extension NPoolsClaimRewardsInteractor: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { + func handleAssetBalance( + result: Result, + accountId _: AccountId, chainId _: ChainModel.Id, + assetId _: AssetModel.Id + ) { + switch result { + case let .success(assetBalance): + presenter?.didReceive(assetBalance: assetBalance) + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "balance")) + } + } +} + +extension NPoolsClaimRewardsInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { + func handlePrice(result: Result, priceId _: AssetModel.PriceId) { + switch result { + case let .success(priceData): + presenter?.didReceive(price: priceData) + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "price")) + } + } +} + +extension NPoolsClaimRewardsInteractor: SelectedCurrencyDepending { + func applyCurrency() { + guard presenter != nil else { + return + } + + setupCurrencyProvider() + } +} diff --git a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsPresenter.swift b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsPresenter.swift new file mode 100644 index 0000000000..c5ca7f61f7 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsPresenter.swift @@ -0,0 +1,255 @@ +import Foundation +import SoraFoundation +import BigInt + +final class NPoolsClaimRewardsPresenter { + weak var view: NPoolsClaimRewardsViewProtocol? + let wireframe: NPoolsClaimRewardsWireframeProtocol + let interactor: NPoolsClaimRewardsInteractorInputProtocol + let chainAsset: ChainAsset + let dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol + let balanceViewModelFactory: BalanceViewModelFactoryProtocol + let selectedAccount: MetaChainAccountResponse + let logger: LoggerProtocol + + var claimRewardsStrategy: NominationPools.ClaimRewardsStrategy = .freeBalance + + private lazy var walletViewModelFactory = WalletAccountViewModelFactory() + private lazy var displayAddressViewModelFactory = DisplayAddressViewModelFactory() + + var assetBalance: AssetBalance? + var claimableRewards: BigUInt? + var price: PriceData? + var fee: BigUInt? + + init( + interactor: NPoolsClaimRewardsInteractorInputProtocol, + wireframe: NPoolsClaimRewardsWireframeProtocol, + selectedAccount: MetaChainAccountResponse, + chainAsset: ChainAsset, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol, + localizationManager: LocalizationManagerProtocol, + logger: LoggerProtocol + ) { + self.interactor = interactor + self.wireframe = wireframe + self.selectedAccount = selectedAccount + self.chainAsset = chainAsset + self.balanceViewModelFactory = balanceViewModelFactory + self.dataValidatorFactory = dataValidatorFactory + self.logger = logger + self.localizationManager = localizationManager + } + + private func provideAmountViewModel() { + guard let claimableRewards = claimableRewards?.decimal(precision: chainAsset.asset.precision) else { + return + } + + let viewModel = balanceViewModelFactory.balanceFromPrice( + claimableRewards, + priceData: price + ).value(for: selectedLocale) + + view?.didReceiveAmount(viewModel: viewModel) + } + + private func provideWalletViewModel() { + do { + let viewModel = try walletViewModelFactory.createDisplayViewModel(from: selectedAccount) + view?.didReceiveWallet(viewModel: viewModel) + } catch { + logger.error("Did receive error: \(error)") + } + } + + private func provideAccountViewModel() { + do { + let viewModel = try walletViewModelFactory.createViewModel(from: selectedAccount) + view?.didReceiveAccount(viewModel: viewModel.rawDisplayAddress()) + } catch { + logger.error("Did receive error: \(error)") + } + } + + private func provideFee() { + let viewModel: BalanceViewModelProtocol? = fee.flatMap { amount in + guard let amountDecimal = Decimal.fromSubstrateAmount( + amount, + precision: chainAsset.assetDisplayInfo.assetPrecision + ) else { + return nil + } + + return balanceViewModelFactory.balanceFromPrice( + amountDecimal, + priceData: price + ).value(for: selectedLocale) + } + + view?.didReceiveFee(viewModel: viewModel) + } + + private func provideClaimStrategy() { + view?.didReceiveClaimStrategy(viewModel: claimRewardsStrategy) + } + + private func updateView() { + provideAmountViewModel() + provideWalletViewModel() + provideAccountViewModel() + provideFee() + provideClaimStrategy() + } + + private func refreshFee() { + fee = nil + provideFee() + + interactor.estimateFee(for: claimRewardsStrategy) + } +} + +extension NPoolsClaimRewardsPresenter: NPoolsClaimRewardsPresenterProtocol { + func setup() { + updateView() + + interactor.setup() + + refreshFee() + } + + func confirm() { + DataValidationRunner(validators: [ + dataValidatorFactory.hasInPlank( + fee: fee, + locale: selectedLocale, + precision: chainAsset.assetDisplayInfo.assetPrecision + ) { [weak self] in + self?.refreshFee() + }, + dataValidatorFactory.canPayFeeInPlank( + balance: assetBalance?.transferable, + fee: fee, + asset: chainAsset.assetDisplayInfo, + locale: selectedLocale + ), + dataValidatorFactory.hasProfitAfterClaim( + rewards: claimableRewards, + fee: fee, + chainAsset: chainAsset, + locale: selectedLocale + ) + ]).runValidation { [weak self] in + guard let claimRewardsStrategy = self?.claimRewardsStrategy else { + return + } + + self?.view?.didStartLoading() + + self?.interactor.submit(for: claimRewardsStrategy) + } + } + + func selectAccount() { + guard + let address = selectedAccount.chainAccount.toAddress(), + let view = view else { + return + } + + wireframe.presentAccountOptions( + from: view, + address: address, + chain: chainAsset.chain, + locale: selectedLocale + ) + } + + func toggleClaimStrategy() { + switch claimRewardsStrategy { + case .freeBalance: + claimRewardsStrategy = .restake + case .restake: + claimRewardsStrategy = .freeBalance + } + + refreshFee() + } +} + +extension NPoolsClaimRewardsPresenter: NPoolsClaimRewardsInteractorOutputProtocol { + func didReceive(assetBalance: AssetBalance?) { + logger.debug("Asset balance: \(String(describing: assetBalance))") + + self.assetBalance = assetBalance + } + + func didReceive(claimableRewards: BigUInt?) { + logger.debug("Claimable rewards: \(String(describing: claimableRewards))") + + self.claimableRewards = claimableRewards + + provideAmountViewModel() + } + + func didReceive(price: PriceData?) { + logger.debug("Price: \(String(describing: price))") + + self.price = price + + provideAmountViewModel() + provideFee() + } + + func didReceive(fee: BigUInt?) { + logger.debug("Fee: \(String(describing: fee))") + + self.fee = fee + + provideFee() + } + + func didReceive(submissionResult: Result) { + view?.didStopLoading() + + switch submissionResult { + case .success: + wireframe.presentExtrinsicSubmission( + from: view, + completionAction: .dismiss, + locale: selectedLocale + ) + case let .failure(error): + if error.isWatchOnlySigning { + wireframe.presentDismissingNoSigningView(from: view) + } else { + _ = wireframe.present(error: error, from: view, locale: selectedLocale) + } + } + } + + func didReceive(error: NPoolsClaimRewardsError) { + logger.error("Error: \(error)") + + switch error { + case .subscription: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.remakeSubscriptions() + } + case .fee: + wireframe.presentFeeStatus(on: view, locale: selectedLocale) { [weak self] in + self?.refreshFee() + } + } + } +} + +extension NPoolsClaimRewardsPresenter: Localizable { + func applyLocalization() { + if let view = view, view.isSetup { + updateView() + } + } +} diff --git a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsProtocols.swift b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsProtocols.swift new file mode 100644 index 0000000000..555567899e --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsProtocols.swift @@ -0,0 +1,36 @@ +import BigInt + +protocol NPoolsClaimRewardsViewProtocol: ControllerBackedProtocol, LoadableViewProtocol { + func didReceiveAmount(viewModel: BalanceViewModelProtocol) + func didReceiveWallet(viewModel: DisplayWalletViewModel) + func didReceiveAccount(viewModel: DisplayAddressViewModel) + func didReceiveFee(viewModel: BalanceViewModelProtocol?) + func didReceiveClaimStrategy(viewModel: NominationPools.ClaimRewardsStrategy) +} + +protocol NPoolsClaimRewardsPresenterProtocol: AnyObject { + func setup() + func confirm() + func selectAccount() + func toggleClaimStrategy() +} + +protocol NPoolsClaimRewardsInteractorInputProtocol: AnyObject { + func setup() + func remakeSubscriptions() + func estimateFee(for strategy: NominationPools.ClaimRewardsStrategy) + func submit(for strategy: NominationPools.ClaimRewardsStrategy) +} + +protocol NPoolsClaimRewardsInteractorOutputProtocol: AnyObject { + func didReceive(assetBalance: AssetBalance?) + func didReceive(claimableRewards: BigUInt?) + func didReceive(price: PriceData?) + func didReceive(fee: BigUInt?) + func didReceive(submissionResult: Result) + func didReceive(error: NPoolsClaimRewardsError) +} + +protocol NPoolsClaimRewardsWireframeProtocol: AlertPresentable, ErrorPresentable, CommonRetryable, FeeRetryable, + AddressOptionsPresentable, MessageSheetPresentable, + ExtrinsicSubmissionPresenting, NominationPoolErrorPresentable {} diff --git a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewController.swift b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewController.swift new file mode 100644 index 0000000000..afdf8b499c --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewController.swift @@ -0,0 +1,132 @@ +import UIKit +import SoraFoundation + +final class NPoolsClaimRewardsViewController: UIViewController, ViewHolder { + typealias RootViewType = NPoolsClaimRewardsViewLayout + + let presenter: NPoolsClaimRewardsPresenterProtocol + + init(presenter: NPoolsClaimRewardsPresenterProtocol, localizationManager: LocalizationManagerProtocol) { + self.presenter = presenter + + super.init(nibName: nil, bundle: nil) + + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = NPoolsClaimRewardsViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupHandlers() + setupLocalization() + + presenter.setup() + } + + private func setupLocalization() { + let languages = selectedLocale.rLanguages + + title = R.string.localizable.stakingClaimRewards(preferredLanguages: languages) + + rootView.actionButton.imageWithTitleView?.title = R.string.localizable + .commonConfirm(preferredLanguages: selectedLocale.rLanguages) + + rootView.walletCell.titleLabel.text = R.string.localizable.commonWallet( + preferredLanguages: selectedLocale.rLanguages + ) + + rootView.accountCell.titleLabel.text = R.string.localizable.commonAccount( + preferredLanguages: selectedLocale.rLanguages + ) + + rootView.networkFeeCell.rowContentView.locale = selectedLocale + + rootView.restakeCell.titleLabel.text = R.string.localizable.stakingRestakeTitle_v2_2_0( + preferredLanguages: languages + ) + + rootView.restakeCell.subtitleLabel.text = R.string.localizable.stakingRestakeMessage( + preferredLanguages: languages + ) + } + + private func setupHandlers() { + rootView.actionButton.addTarget( + self, + action: #selector(actionConfirm), + for: .touchUpInside + ) + + rootView.accountCell.addTarget( + self, + action: #selector(actionSelectAccount), + for: .touchUpInside + ) + + rootView.restakeCell.switchControl.addTarget( + self, + action: #selector(actionToggleClaimStrategy), + for: .valueChanged + ) + } + + @objc private func actionConfirm() { + presenter.confirm() + } + + @objc private func actionSelectAccount() { + presenter.selectAccount() + } + + @objc private func actionToggleClaimStrategy() { + presenter.toggleClaimStrategy() + } +} + +extension NPoolsClaimRewardsViewController: NPoolsClaimRewardsViewProtocol { + func didReceiveAmount(viewModel: BalanceViewModelProtocol) { + rootView.amountView.bind(viewModel: viewModel) + } + + func didReceiveWallet(viewModel: DisplayWalletViewModel) { + rootView.walletCell.bind(viewModel: viewModel.cellViewModel) + } + + func didReceiveAccount(viewModel: DisplayAddressViewModel) { + rootView.accountCell.bind(viewModel: viewModel.cellViewModel) + } + + func didReceiveFee(viewModel: BalanceViewModelProtocol?) { + rootView.networkFeeCell.rowContentView.bind(viewModel: viewModel) + } + + func didReceiveClaimStrategy(viewModel: NominationPools.ClaimRewardsStrategy) { + let shouldRestake = viewModel == .restake + rootView.restakeCell.switchControl.setOn(shouldRestake, animated: false) + } + + func didStartLoading() { + rootView.loadingView.startLoading() + } + + func didStopLoading() { + rootView.loadingView.stopLoading() + } +} + +extension NPoolsClaimRewardsViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} diff --git a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewFactory.swift b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewFactory.swift new file mode 100644 index 0000000000..701b3606c1 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewFactory.swift @@ -0,0 +1,88 @@ +import Foundation +import RobinHood +import SoraFoundation + +struct NPoolsClaimRewardsViewFactory { + static func createView(for state: NPoolsStakingSharedStateProtocol) -> NPoolsClaimRewardsViewProtocol? { + guard + let interactor = createInteractor(for: state), + let wallet = SelectedWalletSettings.shared.value, + let selectedAccount = wallet.fetchMetaChainAccount(for: state.chainAsset.chain.accountRequest()), + let currencyManager = CurrencyManager.shared else { + return nil + } + + let wireframe = NPoolsClaimRewardsWireframe() + + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: state.chainAsset.assetDisplayInfo, + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager) + ) + + let dataValidatingFactory = NominationPoolDataValidatorFactory( + presentable: wireframe, + balanceFactory: balanceViewModelFactory + ) + + let presenter = NPoolsClaimRewardsPresenter( + interactor: interactor, + wireframe: wireframe, + selectedAccount: selectedAccount, + chainAsset: state.chainAsset, + balanceViewModelFactory: balanceViewModelFactory, + dataValidatorFactory: dataValidatingFactory, + localizationManager: LocalizationManager.shared, + logger: Logger.shared + ) + + let view = NPoolsClaimRewardsViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + interactor.presenter = presenter + dataValidatingFactory.view = view + + return view + } + + private static func createInteractor( + for state: NPoolsStakingSharedStateProtocol + ) -> NPoolsClaimRewardsInteractor? { + let chainAsset = state.chainAsset + let chainRegistry = ChainRegistryFacade.sharedRegistry + + guard + let selectedAccount = SelectedWalletSettings.shared.value?.fetchMetaChainAccount( + for: chainAsset.chain.accountRequest() + ), + let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), + let currencyManager = CurrencyManager.shared else { + return nil + } + + let operationQueue = OperationManagerFacade.sharedDefaultQueue + + let extrinsicService = ExtrinsicServiceFactory( + runtimeRegistry: runtimeService, + engine: connection, + operationManager: OperationManager(operationQueue: operationQueue) + ).createService(account: selectedAccount.chainAccount, chain: chainAsset.chain) + + let signingWrapper = SigningWrapperFactory.createSigner(from: selectedAccount) + + return NPoolsClaimRewardsInteractor( + selectedAccount: selectedAccount, + chainAsset: chainAsset, + extrinsicService: extrinsicService, + feeProxy: ExtrinsicFeeProxy(), + signingWrapper: signingWrapper, + npoolsLocalSubscriptionFactory: state.npLocalSubscriptionFactory, + priceLocalSubscriptionFactory: PriceProviderFactory.shared, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + currencyManager: currencyManager + ) + } +} diff --git a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewLayout.swift b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewLayout.swift new file mode 100644 index 0000000000..dbce2313b8 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewLayout.swift @@ -0,0 +1,47 @@ +import UIKit + +final class NPoolsClaimRewardsViewLayout: SCLoadableActionLayoutView { + let amountView = MultilineBalanceView() + + let walletTableView = StackTableView() + + let walletCell = StackTableCell() + + let accountCell: StackInfoTableCell = { + let cell = StackInfoTableCell() + cell.detailsLabel.lineBreakMode = .byTruncatingMiddle + return cell + }() + + let networkFeeCell = StackNetworkFeeCell() + + let settingsTableView: StackTableView = .create { view in + view.cellHeight = 36 + view.contentInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16) + } + + let restakeCell = StackSwitchCell() + + var actionButton: TriangularedButton { + genericActionView.actionButton + } + + var loadingView: LoadableActionView { + genericActionView + } + + override func setupLayout() { + super.setupLayout() + + addArrangedSubview(amountView, spacingAfter: 24) + + addArrangedSubview(walletTableView, spacingAfter: 12) + + walletTableView.addArrangedSubview(walletCell) + walletTableView.addArrangedSubview(accountCell) + walletTableView.addArrangedSubview(networkFeeCell) + + addArrangedSubview(settingsTableView) + settingsTableView.addArrangedSubview(restakeCell) + } +} diff --git a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsWireframe.swift b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsWireframe.swift new file mode 100644 index 0000000000..ec61436ad5 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsWireframe.swift @@ -0,0 +1,3 @@ +import Foundation + +final class NPoolsClaimRewardsWireframe: NPoolsClaimRewardsWireframeProtocol, ModalAlertPresenting {} diff --git a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemError.swift b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemError.swift new file mode 100644 index 0000000000..097951653e --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemError.swift @@ -0,0 +1,6 @@ +import Foundation + +enum NPoolsRedeemError: Error { + case subscription(Error, String) + case fee(Error) +} diff --git a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemInteractor.swift b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemInteractor.swift new file mode 100644 index 0000000000..2799d6f0e5 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemInteractor.swift @@ -0,0 +1,308 @@ +import UIKit +import RobinHood +import SubstrateSdk +import BigInt + +final class NPoolsRedeemInteractor { + weak var presenter: NPoolsRedeemInteractorOutputProtocol? + + let selectedAccount: MetaChainAccountResponse + let chainAsset: ChainAsset + let extrinsicService: ExtrinsicServiceProtocol + let feeProxy: ExtrinsicFeeProxyProtocol + let slashesOperationFactory: SlashesOperationFactoryProtocol + let signingWrapper: SigningWrapperProtocol + let operationQueue: OperationQueue + + let npoolsOperationFactory: NominationPoolsOperationFactoryProtocol + let npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol + let stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol + let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol + let connection: JSONRPCEngine + let runtimeService: RuntimeCodingServiceProtocol + let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol + + var accountId: AccountId { selectedAccount.chainAccount.accountId } + var chainId: ChainModel.Id { chainAsset.chain.chainId } + var asset: AssetModel { chainAsset.asset } + var assetId: AssetModel.Id { asset.assetId } + + private var poolMemberProvider: AnyDataProvider? + private var subPoolsProvider: AnyDataProvider? + private var balanceProvider: StreamableProvider? + private var priceProvider: StreamableProvider? + private var activeEraProvider: AnyDataProvider? + + private var currentPoolId: NominationPools.PoolId? + + init( + selectedAccount: MetaChainAccountResponse, + chainAsset: ChainAsset, + extrinsicService: ExtrinsicServiceProtocol, + feeProxy: ExtrinsicFeeProxyProtocol, + signingWrapper: SigningWrapperProtocol, + slashesOperationFactory: SlashesOperationFactoryProtocol, + npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, + stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + npoolsOperationFactory: NominationPoolsOperationFactoryProtocol, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol, + currencyManager: CurrencyManagerProtocol, + operationQueue: OperationQueue + ) { + self.selectedAccount = selectedAccount + self.chainAsset = chainAsset + self.extrinsicService = extrinsicService + self.feeProxy = feeProxy + self.signingWrapper = signingWrapper + self.slashesOperationFactory = slashesOperationFactory + self.npoolsLocalSubscriptionFactory = npoolsLocalSubscriptionFactory + self.stakingLocalSubscriptionFactory = stakingLocalSubscriptionFactory + self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory + self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory + self.npoolsOperationFactory = npoolsOperationFactory + self.connection = connection + self.runtimeService = runtimeService + self.operationQueue = operationQueue + self.currencyManager = currencyManager + } + + func setupPoolProviders() { + guard let poolId = currentPoolId else { + return + } + + subPoolsProvider = subscribeSubPools(for: poolId, chainId: chainId) + } + + func setupCurrencyProvider() { + guard let priceId = asset.priceId else { + presenter?.didReceive(price: nil) + return + } + + priceProvider = subscribeToPrice(for: priceId, currency: selectedCurrency) + } + + func setupBaseProviders() { + subPoolsProvider = nil + + poolMemberProvider = subscribePoolMember(for: accountId, chainId: chainId) + balanceProvider = subscribeToAssetBalanceProvider(for: accountId, chainId: chainId, assetId: assetId) + activeEraProvider = subscribeActiveEra(for: chainId) + + setupCurrencyProvider() + } + + private func fetchSlashingSpansForStash( + poolId: NominationPools.PoolId, + completionClosure: @escaping (Result) -> Void + ) { + let bondedAccountWrapper = npoolsOperationFactory.createBondedAccountsWrapper( + for: { [poolId] }, + runtimeService: runtimeService + ) + + let accountIdClosure: () throws -> AccountId = { + let accounts = try bondedAccountWrapper.targetOperation.extractNoCancellableResultData() + + if let accountId = accounts[poolId] { + return accountId + } else { + throw CommonError.dataCorruption + } + } + + let slashingWrapper = slashesOperationFactory.createSlashingSpansOperationForStash( + accountIdClosure, + engine: connection, + runtimeService: runtimeService + ) + + slashingWrapper.addDependency(wrapper: bondedAccountWrapper) + + slashingWrapper.targetOperation.completionBlock = { + DispatchQueue.main.async { + if let result = slashingWrapper.targetOperation.result { + completionClosure(result) + } else { + completionClosure(.failure(BaseOperationError.unexpectedDependentResult)) + } + } + } + + let allOperations = bondedAccountWrapper.allOperations + slashingWrapper.allOperations + + operationQueue.addOperations(allOperations, waitUntilFinished: false) + } + + func createExtrinsicBuilderClosure( + for accountId: AccountId, + numOfSlashingSpans: UInt32 + ) -> ExtrinsicBuilderClosure { + { builder in + let redeemCall = NominationPools.RedeemCall( + memberAccount: .accoundId(accountId), + numberOfSlashingSpans: numOfSlashingSpans + ) + + return try builder.adding(call: redeemCall.runtimeCall()) + } + } + + func estimateFee(for numOfSlashingSpans: UInt32) { + feeProxy.estimateFee( + using: extrinsicService, + reuseIdentifier: TransactionFeeId(numOfSlashingSpans), + setupBy: createExtrinsicBuilderClosure(for: accountId, numOfSlashingSpans: numOfSlashingSpans) + ) + } + + func submit(for numberOfSlashingSpans: UInt32) { + extrinsicService.submit( + createExtrinsicBuilderClosure(for: accountId, numOfSlashingSpans: numberOfSlashingSpans), + signer: signingWrapper, + runningIn: .main + ) { [weak self] result in + self?.presenter?.didReceive(submissionResult: result) + } + } +} + +extension NPoolsRedeemInteractor: NPoolsRedeemInteractorInputProtocol { + func setup() { + feeProxy.delegate = self + + setupBaseProviders() + } + + func remakeSubscriptions() { + setupBaseProviders() + } + + func estimateFee() { + guard let poolId = currentPoolId else { + return + } + + fetchSlashingSpansForStash(poolId: poolId) { [weak self] result in + switch result { + case let .success(optSlashingSpans): + self?.estimateFee(for: optSlashingSpans?.numOfSlashingSpans ?? 0) + case let .failure(error): + self?.presenter?.didReceive(error: .fee(error)) + } + } + } + + func submit() { + guard let poolId = currentPoolId else { + presenter?.didReceive(submissionResult: .failure(CommonError.dataCorruption)) + return + } + + fetchSlashingSpansForStash(poolId: poolId) { [weak self] result in + switch result { + case let .success(optSlashingSpans): + self?.submit(for: optSlashingSpans?.numOfSlashingSpans ?? 0) + case let .failure(error): + self?.presenter?.didReceive(submissionResult: .failure(error)) + } + } + } +} + +extension NPoolsRedeemInteractor: ExtrinsicFeeProxyDelegate { + func didReceiveFee(result: Result, for _: TransactionFeeId) { + switch result { + case let .success(dispatchInfo): + presenter?.didReceive(fee: BigUInt(dispatchInfo.fee)) + case let .failure(error): + presenter?.didReceive(error: .fee(error)) + } + } +} + +extension NPoolsRedeemInteractor: NPoolsLocalStorageSubscriber, NPoolsLocalSubscriptionHandler { + func handlePoolMember( + result: Result, + accountId _: AccountId, chainId _: ChainModel.Id + ) { + switch result { + case let .success(optPoolMember): + if currentPoolId != optPoolMember?.poolId { + currentPoolId = optPoolMember?.poolId + + setupPoolProviders() + + estimateFee() + } + + presenter?.didReceive(poolMember: optPoolMember) + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "pool member")) + } + } + + func handleSubPools( + result: Result, + poolId _: NominationPools.PoolId, + chainId _: ChainModel.Id + ) { + switch result { + case let .success(subPools): + presenter?.didReceive(subPools: subPools) + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "sub pools")) + } + } +} + +extension NPoolsRedeemInteractor: StakingLocalStorageSubscriber, StakingLocalSubscriptionHandler { + func handleActiveEra(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(activeEra): + presenter?.didReceive(activeEra: activeEra) + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "active era")) + } + } +} + +extension NPoolsRedeemInteractor: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { + func handleAssetBalance( + result: Result, + accountId _: AccountId, chainId _: ChainModel.Id, + assetId _: AssetModel.Id + ) { + switch result { + case let .success(assetBalance): + presenter?.didReceive(assetBalance: assetBalance) + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "balance")) + } + } +} + +extension NPoolsRedeemInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { + func handlePrice(result: Result, priceId _: AssetModel.PriceId) { + switch result { + case let .success(priceData): + presenter?.didReceive(price: priceData) + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "price")) + } + } +} + +extension NPoolsRedeemInteractor: SelectedCurrencyDepending { + func applyCurrency() { + guard presenter != nil else { + return + } + + setupCurrencyProvider() + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemPresenter.swift b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemPresenter.swift new file mode 100644 index 0000000000..42c2b591a1 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemPresenter.swift @@ -0,0 +1,250 @@ +import Foundation +import SoraFoundation +import BigInt + +final class NPoolsRedeemPresenter { + weak var view: NPoolsRedeemViewProtocol? + let wireframe: NPoolsRedeemWireframeProtocol + let interactor: NPoolsRedeemInteractorInputProtocol + let chainAsset: ChainAsset + let dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol + let balanceViewModelFactory: BalanceViewModelFactoryProtocol + let selectedAccount: MetaChainAccountResponse + let logger: LoggerProtocol + + private lazy var walletViewModelFactory = WalletAccountViewModelFactory() + private lazy var displayAddressViewModelFactory = DisplayAddressViewModelFactory() + + var assetBalance: AssetBalance? + var poolMember: NominationPools.PoolMember? + var subPools: NominationPools.SubPools? + var activeEra: ActiveEraInfo? + var price: PriceData? + var fee: BigUInt? + + init( + interactor: NPoolsRedeemInteractorInputProtocol, + wireframe: NPoolsRedeemWireframeProtocol, + selectedAccount: MetaChainAccountResponse, + chainAsset: ChainAsset, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol, + localizationManager: LocalizationManagerProtocol, + logger: LoggerProtocol + ) { + self.interactor = interactor + self.wireframe = wireframe + self.selectedAccount = selectedAccount + self.chainAsset = chainAsset + self.balanceViewModelFactory = balanceViewModelFactory + self.dataValidatorFactory = dataValidatorFactory + self.logger = logger + self.localizationManager = localizationManager + } + + private func getRedeemableAmount() -> BigUInt? { + guard + let subPools = subPools, + let poolMember = poolMember, + let era = activeEra?.index else { + return nil + } + + return subPools.redeemableBalance(for: poolMember, in: era) + } + + private func provideAmountViewModel() { + guard let amount = getRedeemableAmount()?.decimal(precision: chainAsset.asset.precision) else { + return + } + + let viewModel = balanceViewModelFactory.balanceFromPrice( + amount, + priceData: price + ).value(for: selectedLocale) + + view?.didReceiveAmount(viewModel: viewModel) + } + + private func provideWalletViewModel() { + do { + let viewModel = try walletViewModelFactory.createDisplayViewModel(from: selectedAccount) + view?.didReceiveWallet(viewModel: viewModel) + } catch { + logger.error("Did receive error: \(error)") + } + } + + private func provideAccountViewModel() { + do { + let viewModel = try walletViewModelFactory.createViewModel(from: selectedAccount) + view?.didReceiveAccount(viewModel: viewModel.rawDisplayAddress()) + } catch { + logger.error("Did receive error: \(error)") + } + } + + private func provideFee() { + let viewModel: BalanceViewModelProtocol? = fee.flatMap { amount in + guard let amountDecimal = Decimal.fromSubstrateAmount( + amount, + precision: chainAsset.assetDisplayInfo.assetPrecision + ) else { + return nil + } + + return balanceViewModelFactory.balanceFromPrice( + amountDecimal, + priceData: price + ).value(for: selectedLocale) + } + + view?.didReceiveFee(viewModel: viewModel) + } + + func updateView() { + provideAmountViewModel() + provideWalletViewModel() + provideAccountViewModel() + provideFee() + } +} + +extension NPoolsRedeemPresenter: NPoolsRedeemPresenterProtocol { + func setup() { + updateView() + + interactor.setup() + } + + func confirm() { + DataValidationRunner(validators: [ + dataValidatorFactory.hasInPlank( + fee: fee, + locale: selectedLocale, + precision: chainAsset.assetDisplayInfo.assetPrecision + ) { [weak self] in + self?.interactor.estimateFee() + }, + dataValidatorFactory.canPayFeeInPlank( + balance: assetBalance?.transferable, + fee: fee, + asset: chainAsset.assetDisplayInfo, + locale: selectedLocale + ) + ]).runValidation { [weak self] in + self?.view?.didStartLoading() + self?.interactor.submit() + } + } + + func selectAccount() { + guard + let address = selectedAccount.chainAccount.toAddress(), + let view = view else { + return + } + + wireframe.presentAccountOptions( + from: view, + address: address, + chain: chainAsset.chain, + locale: selectedLocale + ) + } +} + +extension NPoolsRedeemPresenter: NPoolsRedeemInteractorOutputProtocol { + func didReceive(assetBalance: AssetBalance?) { + logger.debug("Asset balance: \(String(describing: assetBalance))") + + self.assetBalance = assetBalance + } + + func didReceive(poolMember: NominationPools.PoolMember?) { + logger.debug("Pool member: \(String(describing: assetBalance))") + + self.poolMember = poolMember + + provideAmountViewModel() + } + + func didReceive(subPools: NominationPools.SubPools?) { + logger.debug("SubPools: \(String(describing: assetBalance))") + + self.subPools = subPools + + provideAmountViewModel() + } + + func didReceive(activeEra: ActiveEraInfo?) { + logger.debug("Active era: \(String(describing: assetBalance))") + + self.activeEra = activeEra + + provideAmountViewModel() + } + + func didReceive(price: PriceData?) { + logger.debug("Price: \(String(describing: assetBalance))") + + self.price = price + + provideAmountViewModel() + provideFee() + } + + func didReceive(fee: BigUInt?) { + logger.debug("Fee: \(String(describing: assetBalance))") + + self.fee = fee + + provideFee() + } + + func didReceive(submissionResult: Result) { + view?.didStopLoading() + + switch submissionResult { + case .success: + let totalPoints = poolMember?.points ?? 0 + let willHaveStaking = totalPoints > 0 || (poolMember?.unbondingEras ?? []).count > 1 + let action: ExtrinsicSubmissionPresentingAction = willHaveStaking ? .dismiss : .popBaseAndDismiss + + wireframe.presentExtrinsicSubmission( + from: view, + completionAction: action, + locale: selectedLocale + ) + case let .failure(error): + if error.isWatchOnlySigning { + wireframe.presentDismissingNoSigningView(from: view) + } else { + _ = wireframe.present(error: error, from: view, locale: selectedLocale) + } + } + } + + func didReceive(error: NPoolsRedeemError) { + logger.error("Error: \(error)") + + switch error { + case .subscription: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.remakeSubscriptions() + } + case .fee: + wireframe.presentFeeStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.estimateFee() + } + } + } +} + +extension NPoolsRedeemPresenter: Localizable { + func applyLocalization() { + if let view = view, view.isSetup { + updateView() + } + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemProtocols.swift b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemProtocols.swift new file mode 100644 index 0000000000..d9003624e4 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemProtocols.swift @@ -0,0 +1,36 @@ +import BigInt + +protocol NPoolsRedeemViewProtocol: ControllerBackedProtocol, LoadableViewProtocol { + func didReceiveAmount(viewModel: BalanceViewModelProtocol) + func didReceiveWallet(viewModel: DisplayWalletViewModel) + func didReceiveAccount(viewModel: DisplayAddressViewModel) + func didReceiveFee(viewModel: BalanceViewModelProtocol?) +} + +protocol NPoolsRedeemPresenterProtocol: AnyObject { + func setup() + func confirm() + func selectAccount() +} + +protocol NPoolsRedeemInteractorInputProtocol: AnyObject { + func setup() + func remakeSubscriptions() + func estimateFee() + func submit() +} + +protocol NPoolsRedeemInteractorOutputProtocol: AnyObject { + func didReceive(assetBalance: AssetBalance?) + func didReceive(poolMember: NominationPools.PoolMember?) + func didReceive(subPools: NominationPools.SubPools?) + func didReceive(activeEra: ActiveEraInfo?) + func didReceive(price: PriceData?) + func didReceive(fee: BigUInt?) + func didReceive(submissionResult: Result) + func didReceive(error: NPoolsRedeemError) +} + +protocol NPoolsRedeemWireframeProtocol: ErrorPresentable, AlertPresentable, CommonRetryable, FeeRetryable, + AddressOptionsPresentable, MessageSheetPresentable, + NominationPoolErrorPresentable, ExtrinsicSubmissionPresenting {} diff --git a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewController.swift b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewController.swift new file mode 100644 index 0000000000..904eaa4f6e --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewController.swift @@ -0,0 +1,108 @@ +import UIKit +import SoraFoundation + +final class NPoolsRedeemViewController: UIViewController, ViewHolder { + typealias RootViewType = NPoolsRedeemViewLayout + + let presenter: NPoolsRedeemPresenterProtocol + + init(presenter: NPoolsRedeemPresenterProtocol, localizationManager: LocalizationManagerProtocol) { + self.presenter = presenter + super.init(nibName: nil, bundle: nil) + + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = NPoolsRedeemViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupHandlers() + setupLocalization() + + presenter.setup() + } + + private func setupLocalization() { + let languages = selectedLocale.rLanguages + + title = R.string.localizable.stakingRedeem(preferredLanguages: languages) + + rootView.actionButton.imageWithTitleView?.title = R.string.localizable + .commonConfirm(preferredLanguages: selectedLocale.rLanguages) + + rootView.walletCell.titleLabel.text = R.string.localizable.commonWallet( + preferredLanguages: selectedLocale.rLanguages + ) + + rootView.accountCell.titleLabel.text = R.string.localizable.commonAccount( + preferredLanguages: selectedLocale.rLanguages + ) + + rootView.networkFeeCell.rowContentView.locale = selectedLocale + } + + private func setupHandlers() { + rootView.actionButton.addTarget( + self, + action: #selector(actionConfirm), + for: .touchUpInside + ) + + rootView.accountCell.addTarget( + self, + action: #selector(actionSelectAccount), + for: .touchUpInside + ) + } + + @objc private func actionConfirm() { + presenter.confirm() + } + + @objc private func actionSelectAccount() { + presenter.selectAccount() + } +} + +extension NPoolsRedeemViewController: NPoolsRedeemViewProtocol { + func didReceiveAmount(viewModel: BalanceViewModelProtocol) { + rootView.amountView.bind(viewModel: viewModel) + } + + func didReceiveWallet(viewModel: DisplayWalletViewModel) { + rootView.walletCell.bind(viewModel: viewModel.cellViewModel) + } + + func didReceiveAccount(viewModel: DisplayAddressViewModel) { + rootView.accountCell.bind(viewModel: viewModel.cellViewModel) + } + + func didReceiveFee(viewModel: BalanceViewModelProtocol?) { + rootView.networkFeeCell.rowContentView.bind(viewModel: viewModel) + } + + func didStartLoading() { + rootView.loadingView.startLoading() + } + + func didStopLoading() { + rootView.loadingView.stopLoading() + } +} + +extension NPoolsRedeemViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewFactory.swift b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewFactory.swift new file mode 100644 index 0000000000..d5723c5a40 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewFactory.swift @@ -0,0 +1,103 @@ +import Foundation +import SubstrateSdk +import RobinHood +import SoraFoundation + +struct NPoolsRedeemViewFactory { + static func createView(for state: NPoolsStakingSharedStateProtocol) -> NPoolsRedeemViewProtocol? { + guard + let interactor = createInteractor(for: state), + let wallet = SelectedWalletSettings.shared.value, + let selectedAccount = wallet.fetchMetaChainAccount(for: state.chainAsset.chain.accountRequest()), + let currencyManager = CurrencyManager.shared else { + return nil + } + + let wireframe = NPoolsRedeemWireframe() + + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: state.chainAsset.assetDisplayInfo, + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager) + ) + + let dataValidatingFactory = NominationPoolDataValidatorFactory( + presentable: wireframe, + balanceFactory: balanceViewModelFactory + ) + + let presenter = NPoolsRedeemPresenter( + interactor: interactor, + wireframe: wireframe, + selectedAccount: selectedAccount, + chainAsset: state.chainAsset, + balanceViewModelFactory: balanceViewModelFactory, + dataValidatorFactory: dataValidatingFactory, + localizationManager: LocalizationManager.shared, + logger: Logger.shared + ) + + let view = NPoolsRedeemViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + interactor.presenter = presenter + dataValidatingFactory.view = view + + return view + } + + private static func createInteractor( + for state: NPoolsStakingSharedStateProtocol + ) -> NPoolsRedeemInteractor? { + let chainAsset = state.chainAsset + let chainRegistry = ChainRegistryFacade.sharedRegistry + + guard + let selectedAccount = SelectedWalletSettings.shared.value?.fetchMetaChainAccount( + for: chainAsset.chain.accountRequest() + ), + let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), + let currencyManager = CurrencyManager.shared else { + return nil + } + + let operationQueue = OperationManagerFacade.sharedDefaultQueue + + let extrinsicService = ExtrinsicServiceFactory( + runtimeRegistry: runtimeService, + engine: connection, + operationManager: OperationManager(operationQueue: operationQueue) + ).createService(account: selectedAccount.chainAccount, chain: chainAsset.chain) + + let signingWrapper = SigningWrapperFactory.createSigner(from: selectedAccount) + + let storageRequestFactory = StorageRequestFactory( + remoteFactory: StorageKeyFactory(), + operationManager: OperationManager(operationQueue: operationQueue) + ) + + let slashesOperationFactory = SlashesOperationFactory(storageRequestFactory: storageRequestFactory) + let npoolsOperationFactory = NominationPoolsOperationFactory(operationQueue: operationQueue) + + return NPoolsRedeemInteractor( + selectedAccount: selectedAccount, + chainAsset: chainAsset, + extrinsicService: extrinsicService, + feeProxy: ExtrinsicFeeProxy(), + signingWrapper: signingWrapper, + slashesOperationFactory: slashesOperationFactory, + npoolsLocalSubscriptionFactory: state.npLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.relaychainLocalSubscriptionFactory, + priceLocalSubscriptionFactory: PriceProviderFactory.shared, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + npoolsOperationFactory: npoolsOperationFactory, + connection: connection, + runtimeService: runtimeService, + currencyManager: currencyManager, + operationQueue: operationQueue + ) + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewLayout.swift b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewLayout.swift new file mode 100644 index 0000000000..d5106197f6 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewLayout.swift @@ -0,0 +1,37 @@ +import UIKit + +final class NPoolsRedeemViewLayout: SCLoadableActionLayoutView { + let amountView = MultilineBalanceView() + + let walletTableView = StackTableView() + + let walletCell = StackTableCell() + + let accountCell: StackInfoTableCell = { + let cell = StackInfoTableCell() + cell.detailsLabel.lineBreakMode = .byTruncatingMiddle + return cell + }() + + let networkFeeCell = StackNetworkFeeCell() + + var actionButton: TriangularedButton { + genericActionView.actionButton + } + + var loadingView: LoadableActionView { + genericActionView + } + + override func setupLayout() { + super.setupLayout() + + addArrangedSubview(amountView, spacingAfter: 24) + + addArrangedSubview(walletTableView, spacingAfter: 12) + + walletTableView.addArrangedSubview(walletCell) + walletTableView.addArrangedSubview(accountCell) + walletTableView.addArrangedSubview(networkFeeCell) + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemWireframe.swift b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemWireframe.swift new file mode 100644 index 0000000000..8e7e1ed053 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemWireframe.swift @@ -0,0 +1,3 @@ +import Foundation + +final class NPoolsRedeemWireframe: NPoolsRedeemWireframeProtocol, ModalAlertPresenting {} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseError.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseError.swift new file mode 100644 index 0000000000..df6e1d6021 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseError.swift @@ -0,0 +1,10 @@ +import Foundation + +enum NPoolsUnstakeBaseError: Error { + case subscription(Error, String) + case stakingDuration(Error) + case eraCountdown(Error) + case claimableRewards(Error) + case unstakeLimits(Error) + case fee(Error) +} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseInteractor.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseInteractor.swift new file mode 100644 index 0000000000..ec2de4c91b --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseInteractor.swift @@ -0,0 +1,473 @@ +import Foundation +import RobinHood +import BigInt +import SubstrateSdk + +class NPoolsUnstakeBaseInteractor: AnyCancellableCleaning, NominationPoolsDataProviding { + weak var basePresenter: NPoolsUnstakeBaseInteractorOutputProtocol? + + let selectedAccount: MetaChainAccountResponse + let chainAsset: ChainAsset + let extrinsicService: ExtrinsicServiceProtocol + let feeProxy: ExtrinsicFeeProxyProtocol + + let npoolsOperationFactory: NominationPoolsOperationFactoryProtocol + let npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol + let stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol + let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol + let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol + let eraCountdownOperationFactory: EraCountdownOperationFactoryProtocol + let unstakeLimitsFactory: NPoolsUnstakeOperationFactoryProtocol + let durationFactory: StakingDurationOperationFactoryProtocol + let connection: JSONRPCEngine + let runtimeService: RuntimeCodingServiceProtocol + let eventCenter: EventCenterProtocol + let operationQueue: OperationQueue + + var accountId: AccountId { selectedAccount.chainAccount.accountId } + var chainId: ChainModel.Id { chainAsset.chain.chainId } + var asset: AssetModel { chainAsset.asset } + var assetId: AssetModel.Id { asset.assetId } + + private var poolMemberProvider: AnyDataProvider? + private var balanceProvider: StreamableProvider? + private var priceProvider: StreamableProvider? + private var bondedPoolProvider: AnyDataProvider? + private var poolLedgerProvider: AnyDataProvider? + private var rewardPoolProvider: AnyDataProvider? + private var claimableRewardProvider: AnySingleValueProvider? + private var minStakeProvider: AnyDataProvider? + + private var bondedAccountIdCancellable: CancellableCall? + private var eraCountdownCancellable: CancellableCall? + private var durationCancellable: CancellableCall? + private var unstakeLimitsCancellable: CancellableCall? + + private var currentPoolId: NominationPools.PoolId? + private var currentPoolRewardCounter: BigUInt? + private var currentMemberRewardCounter: BigUInt? + private var poolAccountId: AccountId? + + init( + selectedAccount: MetaChainAccountResponse, + chainAsset: ChainAsset, + extrinsicService: ExtrinsicServiceProtocol, + feeProxy: ExtrinsicFeeProxyProtocol, + npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, + stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol, + eraCountdownOperationFactory: EraCountdownOperationFactoryProtocol, + durationFactory: StakingDurationOperationFactoryProtocol, + npoolsOperationFactory: NominationPoolsOperationFactoryProtocol, + unstakeLimitsFactory: NPoolsUnstakeOperationFactoryProtocol, + eventCenter: EventCenterProtocol, + currencyManager: CurrencyManagerProtocol, + operationQueue: OperationQueue + ) { + self.selectedAccount = selectedAccount + self.chainAsset = chainAsset + self.extrinsicService = extrinsicService + self.feeProxy = feeProxy + self.npoolsLocalSubscriptionFactory = npoolsLocalSubscriptionFactory + self.stakingLocalSubscriptionFactory = stakingLocalSubscriptionFactory + self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory + self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory + self.npoolsOperationFactory = npoolsOperationFactory + self.connection = connection + self.runtimeService = runtimeService + self.eraCountdownOperationFactory = eraCountdownOperationFactory + self.durationFactory = durationFactory + self.unstakeLimitsFactory = unstakeLimitsFactory + self.operationQueue = operationQueue + self.eventCenter = eventCenter + self.currencyManager = currencyManager + } + + deinit { + clearCancellable() + } + + func clearCancellable() { + clear(cancellable: &bondedAccountIdCancellable) + clear(cancellable: &eraCountdownCancellable) + clear(cancellable: &durationCancellable) + clear(cancellable: &unstakeLimitsCancellable) + } + + func provideBondedAccountId() { + clear(cancellable: &bondedAccountIdCancellable) + + guard let poolId = currentPoolId else { + return + } + + bondedAccountIdCancellable = fetchBondedAccounts( + for: npoolsOperationFactory, + poolIds: { [poolId] }, + runtimeService: runtimeService, + operationQueue: operationQueue, + completion: { [weak self] result in + self?.bondedAccountIdCancellable = nil + + switch result { + case let .success(accountIds): + if let accountId = accountIds[poolId] { + self?.poolAccountId = accountId + self?.setupBondedAccountProviders() + } + case let .failure(error): + self?.basePresenter?.didReceive(error: .subscription(error, "bondedAccountId")) + } + } + ) + } + + func setupPoolProviders() { + guard let poolId = currentPoolId else { + return + } + + bondedPoolProvider = subscribeBondedPool(for: poolId, chainId: chainId) + rewardPoolProvider = subscribeRewardPool(for: poolId, chainId: chainId) + + setupClaimableRewardsProvider() + } + + func setupBondedAccountProviders() { + poolLedgerProvider = nil + + guard let accountId = poolAccountId else { + return + } + + poolLedgerProvider = subscribeLedgerInfo(for: accountId, chainId: chainId) + } + + func setupClaimableRewardsProvider() { + guard let poolId = currentPoolId else { + return + } + + claimableRewardProvider = subscribeClaimableRewards( + for: chainId, + poolId: poolId, + accountId: accountId + ) + + if claimableRewardProvider == nil { + basePresenter?.didReceive(error: .claimableRewards(CommonError.dataCorruption)) + } + } + + func setupCurrencyProvider() { + guard let priceId = asset.priceId else { + basePresenter?.didReceive(price: nil) + return + } + + priceProvider = subscribeToPrice(for: priceId, currency: selectedCurrency) + } + + func setupBaseProviders() { + bondedPoolProvider = nil + poolLedgerProvider = nil + rewardPoolProvider = nil + claimableRewardProvider = nil + + poolMemberProvider = subscribePoolMember(for: accountId, chainId: chainId) + balanceProvider = subscribeToAssetBalanceProvider(for: accountId, chainId: chainId, assetId: assetId) + minStakeProvider = subscribeMinJoinBond(for: chainId) + + setupCurrencyProvider() + } + + func provideEraCountdown() { + clear(cancellable: &eraCountdownCancellable) + + let wrapper = eraCountdownOperationFactory.fetchCountdownOperationWrapper( + for: connection, + runtimeService: runtimeService + ) + + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard wrapper === self?.eraCountdownCancellable else { + return + } + + self?.eraCountdownCancellable = nil + + do { + let eraCountdown = try wrapper.targetOperation.extractNoCancellableResultData() + self?.basePresenter?.didReceive(eraCountdown: eraCountdown) + } catch { + self?.basePresenter?.didReceive(error: .eraCountdown(error)) + } + } + } + + eraCountdownCancellable = wrapper + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } + + func provideStakingDuration() { + clear(cancellable: &durationCancellable) + + let wrapper = durationFactory.createDurationOperation(from: runtimeService) + + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard wrapper === self?.durationCancellable else { + return + } + + self?.durationCancellable = nil + + do { + let duration = try wrapper.targetOperation.extractNoCancellableResultData() + self?.basePresenter?.didReceive(stakingDuration: duration) + } catch { + self?.basePresenter?.didReceive(error: .stakingDuration(error)) + } + } + } + + durationCancellable = wrapper + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } + + func provideUnstakingLimits() { + clear(cancellable: &unstakeLimitsCancellable) + + let wrapper = unstakeLimitsFactory.createLimitsWrapper( + for: chainAsset.chain, + runtimeService: runtimeService + ) + + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard self?.unstakeLimitsCancellable === wrapper else { + return + } + + self?.unstakeLimitsCancellable = nil + + do { + let limits = try wrapper.targetOperation.extractNoCancellableResultData() + self?.basePresenter?.didReceive(unstakingLimits: limits) + } catch { + self?.basePresenter?.didReceive(error: .unstakeLimits(error)) + } + } + } + + unstakeLimitsCancellable = wrapper + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } + + func createExtrinsicClosure(for points: BigUInt, accountId: AccountId) -> ExtrinsicBuilderClosure { + { builder in + let call = NominationPools.UnbondCall( + memberAccount: .accoundId(accountId), + unbondingPoints: points + ) + return try builder.adding(call: call.runtimeCall()) + } + } +} + +extension NPoolsUnstakeBaseInteractor: NPoolsUnstakeBaseInteractorInputProtocol { + func setup() { + setupBaseProviders() + provideEraCountdown() + provideStakingDuration() + provideUnstakingLimits() + + feeProxy.delegate = self + eventCenter.add(observer: self, dispatchIn: .main) + } + + func retrySubscriptions() { + setupBaseProviders() + } + + func retryStakingDuration() { + provideStakingDuration() + } + + func retryEraCountdown() { + provideEraCountdown() + } + + func retryClaimableRewards() { + setupClaimableRewardsProvider() + } + + func retryUnstakeLimits() { + provideUnstakingLimits() + } + + func estimateFee(for points: BigUInt) { + let identifier = String(points) + + feeProxy.estimateFee( + using: extrinsicService, + reuseIdentifier: identifier, + setupBy: createExtrinsicClosure(for: points, accountId: accountId) + ) + } +} + +extension NPoolsUnstakeBaseInteractor: ExtrinsicFeeProxyDelegate { + func didReceiveFee(result: Result, for _: TransactionFeeId) { + switch result { + case let .success(dispatchInfo): + basePresenter?.didReceive(fee: BigUInt(dispatchInfo.fee)) + case let .failure(error): + basePresenter?.didReceive(error: .fee(error)) + } + } +} + +extension NPoolsUnstakeBaseInteractor: NPoolsLocalStorageSubscriber, NPoolsLocalSubscriptionHandler { + func handlePoolMember( + result: Result, + accountId _: AccountId, chainId _: ChainModel.Id + ) { + switch result { + case let .success(optPoolMember): + if currentPoolId != optPoolMember?.poolId { + currentPoolId = optPoolMember?.poolId + + setupPoolProviders() + provideBondedAccountId() + } + + if currentMemberRewardCounter != optPoolMember?.lastRecordedRewardCounter { + currentMemberRewardCounter = optPoolMember?.lastRecordedRewardCounter + + claimableRewardProvider?.refresh() + } + + basePresenter?.didReceive(poolMember: optPoolMember) + case let .failure(error): + basePresenter?.didReceive(error: .subscription(error, "pool member")) + } + } + + func handleBondedPool( + result: Result, + poolId _: NominationPools.PoolId, + chainId _: ChainModel.Id + ) { + switch result { + case let .success(bondedPool): + basePresenter?.didReceive(bondedPool: bondedPool) + case let .failure(error): + basePresenter?.didReceive(error: .subscription(error, "bonded pool")) + } + } + + func handleClaimableRewards( + result: Result, + chainId _: ChainModel.Id, + poolId _: NominationPools.PoolId, + accountId _: AccountId + ) { + switch result { + case let .success(rewards): + basePresenter?.didReceive(claimableRewards: rewards) + case let .failure(error): + basePresenter?.didReceive(error: .claimableRewards(error)) + } + } + + func handleRewardPool( + result: Result, + poolId: NominationPools.PoolId, + chainId _: ChainModel.Id + ) { + guard currentPoolId == poolId else { + return + } + + if case let .success(rewardPool) = result, rewardPool?.lastRecordedRewardCounter != currentPoolRewardCounter { + self.currentPoolRewardCounter = rewardPool?.lastRecordedRewardCounter + + claimableRewardProvider?.refresh() + } + } + + func handleMinJoinBond(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(minStake): + basePresenter?.didReceive(minStake: minStake) + case let .failure(error): + basePresenter?.didReceive(error: .subscription(error, "min stake")) + } + } +} + +extension NPoolsUnstakeBaseInteractor: StakingLocalStorageSubscriber, StakingLocalSubscriptionHandler { + func handleLedgerInfo( + result: Result, + accountId _: AccountId, + chainId _: ChainModel.Id + ) { + switch result { + case let .success(ledger): + basePresenter?.didReceive(stakingLedger: ledger) + case let .failure(error): + basePresenter?.didReceive(error: .subscription(error, "ledger")) + } + } +} + +extension NPoolsUnstakeBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { + func handleAssetBalance( + result: Result, + accountId _: AccountId, chainId _: ChainModel.Id, + assetId _: AssetModel.Id + ) { + switch result { + case let .success(assetBalance): + basePresenter?.didReceive(assetBalance: assetBalance) + case let .failure(error): + basePresenter?.didReceive(error: .subscription(error, "balance")) + } + } +} + +extension NPoolsUnstakeBaseInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { + func handlePrice(result: Result, priceId _: AssetModel.PriceId) { + switch result { + case let .success(priceData): + basePresenter?.didReceive(price: priceData) + case let .failure(error): + basePresenter?.didReceive(error: .subscription(error, "price")) + } + } +} + +extension NPoolsUnstakeBaseInteractor: EventVisitorProtocol { + func processBlockTimeChanged(event _: BlockTimeChanged) { + provideEraCountdown() + provideStakingDuration() + } +} + +extension NPoolsUnstakeBaseInteractor: SelectedCurrencyDepending { + func applyCurrency() { + guard basePresenter != nil else { + return + } + + setupCurrencyProvider() + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBasePresenter.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBasePresenter.swift new file mode 100644 index 0000000000..eaa71421c0 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBasePresenter.swift @@ -0,0 +1,284 @@ +import Foundation +import BigInt +import SoraFoundation + +class NPoolsUnstakeBasePresenter: NPoolsUnstakeBaseInteractorOutputProtocol { + weak var baseView: ControllerBackedProtocol? + + let baseWireframe: NPoolsUnstakeBaseWireframeProtocol + let baseInteractor: NPoolsUnstakeBaseInteractorInputProtocol + let chainAsset: ChainAsset + let hintsViewModelFactory: NPoolsUnstakeHintsFactoryProtocol + let dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol + let balanceViewModelFactory: BalanceViewModelFactoryProtocol + let logger: LoggerProtocol + + var assetBalance: AssetBalance? + var poolMember: NominationPools.PoolMember? + var bondedPool: NominationPools.BondedPool? + var stakingLedger: StakingLedger? + var stakingDuration: StakingDuration? + var eraCountdown: EraCountdown? + var claimableRewards: BigUInt? + var minStake: BigUInt? + var price: PriceData? + var unstakingLimits: NominationPools.UnstakeLimits? + var fee: BigUInt? + + init( + baseInteractor: NPoolsUnstakeBaseInteractorInputProtocol, + baseWireframe: NPoolsUnstakeBaseWireframeProtocol, + chainAsset: ChainAsset, + hintsViewModelFactory: NPoolsUnstakeHintsFactoryProtocol, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol, + localizationManager: LocalizationManagerProtocol, + logger: LoggerProtocol + ) { + self.baseInteractor = baseInteractor + self.baseWireframe = baseWireframe + self.chainAsset = chainAsset + self.hintsViewModelFactory = hintsViewModelFactory + self.balanceViewModelFactory = balanceViewModelFactory + self.dataValidatorFactory = dataValidatorFactory + self.logger = logger + self.localizationManager = localizationManager + } + + func updateView() { + fatalError("Must be overriden by subsclass") + } + + func provideHints() { + fatalError("Must be overriden by subsclass") + } + + func provideFee() { + fatalError("Must be overriden by subsclass") + } + + func getInputAmount() -> Decimal? { + fatalError("Must be overriden by subsclass") + } + + func getInputAmountInPlank() -> BigUInt? { + fatalError("Must be overriden by subsclass") + } + + func getStakedAmountInPlank() -> BigUInt? { + guard + let stakingLedger = stakingLedger, + let bondedPool = bondedPool, + let poolMember = poolMember else { + return nil + } + + return NominationPools.pointsToBalance( + for: poolMember.points, + totalPoints: bondedPool.points, + poolBalance: stakingLedger.active + ) + } + + func getStakedAmount() -> Decimal? { + getStakedAmountInPlank()?.decimal(precision: chainAsset.asset.precision) + } + + func getValidations() -> [DataValidating] { + [ + dataValidatorFactory.hasInPlank( + fee: fee, + locale: selectedLocale, + precision: chainAsset.assetDisplayInfo.assetPrecision + ) { [weak self] in + self?.refreshFee() + }, + dataValidatorFactory.canUnstake( + for: getInputAmount() ?? 0, + stakedAmountInPlank: getStakedAmountInPlank(), + chainAsset: chainAsset, + locale: selectedLocale + ), + dataValidatorFactory.canPayFeeInPlank( + balance: assetBalance?.transferable, + fee: fee, + asset: chainAsset.assetDisplayInfo, + locale: selectedLocale + ), + dataValidatorFactory.hasLedgerUnstakeSpace( + for: stakingLedger, + limits: unstakingLimits, + eraCountdown: eraCountdown, + locale: selectedLocale + ), + dataValidatorFactory.hasPoolMemberUnstakeSpace( + for: poolMember, + limits: unstakingLimits, + eraCountdown: eraCountdown, + locale: selectedLocale + ) + ] + } + + func getUnstakingPoints() -> BigUInt? { + guard + let stakingLedger = stakingLedger, + let bondedPool = bondedPool, + let poolMember = poolMember else { + return nil + } + + let inputAmount = getInputAmountInPlank() ?? 0 + + return NominationPools.unstakingBalanceToPoints( + for: inputAmount, + totalPoints: bondedPool.points, + poolBalance: stakingLedger.active, + memberStakedPoints: poolMember.points + ) + } + + func refreshFee() { + guard let unstakingPoints = getUnstakingPoints() else { + return + } + + fee = nil + + provideFee() + + baseInteractor.estimateFee(for: unstakingPoints) + } + + // MARK: Unstake Base Interactor Output + + func didReceive(assetBalance: AssetBalance?) { + logger.debug("Asset balance: \(String(describing: assetBalance))") + + self.assetBalance = assetBalance + } + + func didReceive(poolMember: NominationPools.PoolMember?) { + logger.debug("Pool member: \(String(describing: poolMember))") + + let shouldRefreshFee = poolMember?.points != self.poolMember?.points + + self.poolMember = poolMember + provideHints() + + if shouldRefreshFee { + refreshFee() + } + } + + func didReceive(bondedPool: NominationPools.BondedPool?) { + logger.debug("Bonded pool: \(String(describing: bondedPool))") + + let shouldRefreshFee = bondedPool?.points != self.bondedPool?.points + + self.bondedPool = bondedPool + + if shouldRefreshFee { + refreshFee() + } + } + + func didReceive(stakingLedger: StakingLedger?) { + logger.debug("Staking ledger: \(String(describing: stakingLedger))") + + let shouldRefreshFee = stakingLedger?.active != self.stakingLedger?.active + + self.stakingLedger = stakingLedger + + if shouldRefreshFee { + refreshFee() + } + } + + func didReceive(stakingDuration: StakingDuration) { + logger.debug("Staking duration: \(stakingDuration)") + + self.stakingDuration = stakingDuration + + provideHints() + } + + func didReceive(eraCountdown: EraCountdown) { + logger.debug("Era countdown: \(eraCountdown)") + + self.eraCountdown = eraCountdown + } + + func didReceive(claimableRewards: BigUInt?) { + logger.debug("Claimable rewards: \(String(describing: claimableRewards))") + + self.claimableRewards = claimableRewards + + provideHints() + } + + func didReceive(minStake: BigUInt?) { + logger.debug("Min stake: \(String(describing: minStake))") + + self.minStake = minStake + } + + func didReceive(price: PriceData?) { + logger.debug("Price: \(String(describing: price))") + + self.price = price + } + + func didReceive(unstakingLimits: NominationPools.UnstakeLimits) { + logger.debug("Unstaking limits: \(unstakingLimits)") + + self.unstakingLimits = unstakingLimits + } + + func didReceive(fee: BigUInt?) { + logger.debug("Fee: \(String(describing: fee))") + + self.fee = fee + + provideFee() + } + + func didReceive(error: NPoolsUnstakeBaseError) { + logger.error("Error: \(error)") + + switch error { + case .subscription: + baseWireframe.presentRequestStatus(on: baseView, locale: selectedLocale) { [weak self] in + self?.baseInteractor.retrySubscriptions() + } + case .stakingDuration: + baseWireframe.presentRequestStatus(on: baseView, locale: selectedLocale) { [weak self] in + self?.baseInteractor.retryStakingDuration() + } + case .eraCountdown: + baseWireframe.presentRequestStatus(on: baseView, locale: selectedLocale) { [weak self] in + self?.baseInteractor.retryEraCountdown() + } + case .claimableRewards: + baseWireframe.presentRequestStatus(on: baseView, locale: selectedLocale) { [weak self] in + self?.baseInteractor.retryClaimableRewards() + } + case .unstakeLimits: + baseWireframe.presentRequestStatus(on: baseView, locale: selectedLocale) { [weak self] in + self?.baseInteractor.retryUnstakeLimits() + } + case .fee: + baseWireframe.presentFeeStatus(on: baseView, locale: selectedLocale) { [weak self] in + self?.refreshFee() + } + } + } +} + +extension NPoolsUnstakeBasePresenter: Localizable { + func applyLocalization() { + if let view = baseView, view.isSetup { + updateView() + } + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseProtocols.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseProtocols.swift new file mode 100644 index 0000000000..ffc75392f8 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseProtocols.swift @@ -0,0 +1,29 @@ +import BigInt + +protocol NPoolsUnstakeBaseInteractorInputProtocol: AnyObject { + func setup() + func retrySubscriptions() + func retryStakingDuration() + func retryEraCountdown() + func retryClaimableRewards() + func retryUnstakeLimits() + func estimateFee(for points: BigUInt) +} + +protocol NPoolsUnstakeBaseInteractorOutputProtocol: AnyObject { + func didReceive(assetBalance: AssetBalance?) + func didReceive(poolMember: NominationPools.PoolMember?) + func didReceive(bondedPool: NominationPools.BondedPool?) + func didReceive(stakingLedger: StakingLedger?) + func didReceive(stakingDuration: StakingDuration) + func didReceive(eraCountdown: EraCountdown) + func didReceive(claimableRewards: BigUInt?) + func didReceive(minStake: BigUInt?) + func didReceive(price: PriceData?) + func didReceive(unstakingLimits: NominationPools.UnstakeLimits) + func didReceive(fee: BigUInt?) + func didReceive(error: NPoolsUnstakeBaseError) +} + +protocol NPoolsUnstakeBaseWireframeProtocol: ErrorPresentable, AlertPresentable, CommonRetryable, FeeRetryable, + NominationPoolErrorPresentable {} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmInteractor.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmInteractor.swift new file mode 100644 index 0000000000..978db3de0d --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmInteractor.swift @@ -0,0 +1,72 @@ +import UIKit +import SubstrateSdk +import BigInt + +final class NPoolsUnstakeConfirmInteractor: NPoolsUnstakeBaseInteractor { + var presenter: NPoolsUnstakeConfirmInteractorOutputProtocol? { + get { + basePresenter as? NPoolsUnstakeConfirmInteractorOutputProtocol + } + + set { + basePresenter = newValue + } + } + + let signingWrapper: SigningWrapperProtocol + + init( + selectedAccount: MetaChainAccountResponse, + chainAsset: ChainAsset, + signingWrapper: SigningWrapperProtocol, + extrinsicService: ExtrinsicServiceProtocol, + feeProxy: ExtrinsicFeeProxyProtocol, + npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, + stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol, + eraCountdownOperationFactory: EraCountdownOperationFactoryProtocol, + durationFactory: StakingDurationOperationFactoryProtocol, + npoolsOperationFactory: NominationPoolsOperationFactoryProtocol, + unstakeLimitsFactory: NPoolsUnstakeOperationFactoryProtocol, + eventCenter: EventCenterProtocol, + currencyManager: CurrencyManagerProtocol, + operationQueue: OperationQueue + ) { + self.signingWrapper = signingWrapper + + super.init( + selectedAccount: selectedAccount, + chainAsset: chainAsset, + extrinsicService: extrinsicService, + feeProxy: feeProxy, + npoolsLocalSubscriptionFactory: npoolsLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: stakingLocalSubscriptionFactory, + priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, + walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, + connection: connection, + runtimeService: runtimeService, + eraCountdownOperationFactory: eraCountdownOperationFactory, + durationFactory: durationFactory, + npoolsOperationFactory: npoolsOperationFactory, + unstakeLimitsFactory: unstakeLimitsFactory, + eventCenter: eventCenter, + currencyManager: currencyManager, + operationQueue: operationQueue + ) + } +} + +extension NPoolsUnstakeConfirmInteractor: NPoolsUnstakeConfirmInteractorInputProtocol { + func submit(unstakingPoints: BigUInt) { + extrinsicService.submit( + createExtrinsicClosure(for: unstakingPoints, accountId: accountId), + signer: signingWrapper, + runningIn: .main + ) { [weak self] result in + self?.presenter?.didReceive(submissionResult: result) + } + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmPresenter.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmPresenter.swift new file mode 100644 index 0000000000..53fec54352 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmPresenter.swift @@ -0,0 +1,185 @@ +import Foundation +import SoraFoundation +import BigInt + +final class NPoolsUnstakeConfirmPresenter: NPoolsUnstakeBasePresenter { + weak var view: NPoolsUnstakeConfirmViewProtocol? + + var wireframe: NPoolsUnstakeConfirmWireframeProtocol? { + baseWireframe as? NPoolsUnstakeConfirmWireframeProtocol + } + + var interactor: NPoolsUnstakeConfirmInteractorInputProtocol? { + baseInteractor as? NPoolsUnstakeConfirmInteractorInputProtocol + } + + let selectedAccount: MetaChainAccountResponse + let unstakingAmount: Decimal + + private lazy var walletViewModelFactory = WalletAccountViewModelFactory() + private lazy var displayAddressViewModelFactory = DisplayAddressViewModelFactory() + + init( + interactor: NPoolsUnstakeConfirmInteractorInputProtocol, + wireframe: NPoolsUnstakeConfirmWireframeProtocol, + unstakingAmount: Decimal, + selectedAccount: MetaChainAccountResponse, + chainAsset: ChainAsset, + hintsViewModelFactory: NPoolsUnstakeHintsFactoryProtocol, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol, + localizationManager: LocalizationManagerProtocol, + logger: LoggerProtocol + ) { + self.selectedAccount = selectedAccount + self.unstakingAmount = unstakingAmount + + super.init( + baseInteractor: interactor, + baseWireframe: wireframe, + chainAsset: chainAsset, + hintsViewModelFactory: hintsViewModelFactory, + balanceViewModelFactory: balanceViewModelFactory, + dataValidatorFactory: dataValidatorFactory, + localizationManager: localizationManager, + logger: logger + ) + } + + private func provideAmountViewModel() { + let viewModel = balanceViewModelFactory.balanceFromPrice( + unstakingAmount, + priceData: price + ).value(for: selectedLocale) + + view?.didReceiveAmount(viewModel: viewModel) + } + + private func provideWalletViewModel() { + do { + let viewModel = try walletViewModelFactory.createDisplayViewModel(from: selectedAccount) + view?.didReceiveWallet(viewModel: viewModel) + } catch { + logger.error("Did receive error: \(error)") + } + } + + private func provideAccountViewModel() { + do { + let viewModel = try walletViewModelFactory.createViewModel(from: selectedAccount) + view?.didReceiveAccount(viewModel: viewModel.rawDisplayAddress()) + } catch { + logger.error("Did receive error: \(error)") + } + } + + // MARK: Subsclass + + override func updateView() { + provideAmountViewModel() + provideWalletViewModel() + provideAccountViewModel() + provideFee() + provideHints() + } + + override func getInputAmount() -> Decimal? { + unstakingAmount + } + + override func getInputAmountInPlank() -> BigUInt? { + unstakingAmount.toSubstrateAmount(precision: chainAsset.assetDisplayInfo.assetPrecision) + } + + override func provideFee() { + let viewModel: BalanceViewModelProtocol? = fee.flatMap { amount in + guard let amountDecimal = Decimal.fromSubstrateAmount( + amount, + precision: chainAsset.assetDisplayInfo.assetPrecision + ) else { + return nil + } + + return balanceViewModelFactory.balanceFromPrice( + amountDecimal, + priceData: price + ).value(for: selectedLocale) + } + + view?.didReceiveFee(viewModel: viewModel) + } + + override func provideHints() { + let hints = hintsViewModelFactory.createHints( + stakingDuration: stakingDuration, + rewards: claimableRewards, + locale: selectedLocale + ) + + view?.didReceiveHints(viewModel: hints) + } + + override func didReceive(price: PriceData?) { + super.didReceive(price: price) + + provideAmountViewModel() + provideFee() + } +} + +extension NPoolsUnstakeConfirmPresenter: NPoolsUnstakeConfirmPresenterProtocol { + func setup() { + updateView() + + interactor?.setup() + } + + func proceed() { + let validators = getValidations() + + DataValidationRunner( + validators: validators + ).runValidation { [weak self] in + guard let unstakingPoints = self?.getUnstakingPoints() else { + return + } + + self?.view?.didStartLoading() + self?.interactor?.submit(unstakingPoints: unstakingPoints) + } + } + + func selectAccount() { + guard + let address = selectedAccount.chainAccount.toAddress(), + let view = view else { + return + } + + wireframe?.presentAccountOptions( + from: view, + address: address, + chain: chainAsset.chain, + locale: selectedLocale + ) + } +} + +extension NPoolsUnstakeConfirmPresenter: NPoolsUnstakeConfirmInteractorOutputProtocol { + func didReceive(submissionResult: Result) { + logger.debug("Submission result: \(submissionResult)") + + view?.didStopLoading() + + switch submissionResult { + case .success: + wireframe?.presentExtrinsicSubmission(from: view, completionAction: .dismiss, locale: selectedLocale) + case let .failure(error): + if error.isWatchOnlySigning { + wireframe?.presentDismissingNoSigningView(from: view) + } else { + _ = wireframe?.present(error: error, from: view, locale: selectedLocale) + } + } + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmProtocols.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmProtocols.swift new file mode 100644 index 0000000000..9dc33fdf08 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmProtocols.swift @@ -0,0 +1,26 @@ +import BigInt + +protocol NPoolsUnstakeConfirmViewProtocol: ControllerBackedProtocol, LoadableViewProtocol { + func didReceiveAmount(viewModel: BalanceViewModelProtocol) + func didReceiveWallet(viewModel: DisplayWalletViewModel) + func didReceiveAccount(viewModel: DisplayAddressViewModel) + func didReceiveFee(viewModel: BalanceViewModelProtocol?) + func didReceiveHints(viewModel: [String]) +} + +protocol NPoolsUnstakeConfirmPresenterProtocol: AnyObject { + func setup() + func proceed() + func selectAccount() +} + +protocol NPoolsUnstakeConfirmInteractorInputProtocol: NPoolsUnstakeBaseInteractorInputProtocol { + func submit(unstakingPoints: BigUInt) +} + +protocol NPoolsUnstakeConfirmInteractorOutputProtocol: NPoolsUnstakeBaseInteractorOutputProtocol { + func didReceive(submissionResult: Result) +} + +protocol NPoolsUnstakeConfirmWireframeProtocol: NPoolsUnstakeBaseWireframeProtocol, AddressOptionsPresentable, + MessageSheetPresentable, ExtrinsicSubmissionPresenting {} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewController.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewController.swift new file mode 100644 index 0000000000..5d4105e879 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewController.swift @@ -0,0 +1,112 @@ +import UIKit +import SoraFoundation + +final class NPoolsUnstakeConfirmViewController: UIViewController, ViewHolder { + typealias RootViewType = NPoolsUnstakeConfirmViewLayout + + let presenter: NPoolsUnstakeConfirmPresenterProtocol + + init(presenter: NPoolsUnstakeConfirmPresenterProtocol, localizationManager: LocalizationManagerProtocol) { + self.presenter = presenter + super.init(nibName: nil, bundle: nil) + + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = NPoolsUnstakeConfirmViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupHandlers() + setupLocalization() + + presenter.setup() + } + + private func setupLocalization() { + let languages = selectedLocale.rLanguages + + title = R.string.localizable.stakingUnbond_v190(preferredLanguages: languages) + + rootView.actionButton.imageWithTitleView?.title = R.string.localizable + .commonConfirm(preferredLanguages: selectedLocale.rLanguages) + + rootView.walletCell.titleLabel.text = R.string.localizable.commonWallet( + preferredLanguages: selectedLocale.rLanguages + ) + + rootView.accountCell.titleLabel.text = R.string.localizable.commonAccount( + preferredLanguages: selectedLocale.rLanguages + ) + + rootView.networkFeeCell.rowContentView.locale = selectedLocale + } + + private func setupHandlers() { + rootView.actionButton.addTarget( + self, + action: #selector(actionConfirm), + for: .touchUpInside + ) + + rootView.accountCell.addTarget( + self, + action: #selector(actionSelectAccount), + for: .touchUpInside + ) + } + + @objc private func actionConfirm() { + presenter.proceed() + } + + @objc private func actionSelectAccount() { + presenter.selectAccount() + } +} + +extension NPoolsUnstakeConfirmViewController: NPoolsUnstakeConfirmViewProtocol { + func didReceiveAmount(viewModel: BalanceViewModelProtocol) { + rootView.amountView.bind(viewModel: viewModel) + } + + func didReceiveWallet(viewModel: DisplayWalletViewModel) { + rootView.walletCell.bind(viewModel: viewModel.cellViewModel) + } + + func didReceiveAccount(viewModel: DisplayAddressViewModel) { + rootView.accountCell.bind(viewModel: viewModel.cellViewModel) + } + + func didReceiveFee(viewModel: BalanceViewModelProtocol?) { + rootView.networkFeeCell.rowContentView.bind(viewModel: viewModel) + } + + func didReceiveHints(viewModel: [String]) { + rootView.hintListView.bind(texts: viewModel) + } + + func didStartLoading() { + rootView.loadingView.startLoading() + } + + func didStopLoading() { + rootView.loadingView.stopLoading() + } +} + +extension NPoolsUnstakeConfirmViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewFactory.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewFactory.swift new file mode 100644 index 0000000000..ef2202bb3b --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewFactory.swift @@ -0,0 +1,110 @@ +import Foundation +import SoraFoundation +import RobinHood + +struct NPoolsUnstakeConfirmViewFactory { + static func createView( + for amount: Decimal, + state: NPoolsStakingSharedStateProtocol + ) -> NPoolsUnstakeConfirmViewProtocol? { + guard + let interactor = createInteractor(for: state), + let currencyManager = CurrencyManager.shared, + let wallet = SelectedWalletSettings.shared.value, + let selectedAccount = wallet.fetchMetaChainAccount(for: state.chainAsset.chain.accountRequest()) else { + return nil + } + + let wireframe = NPoolsUnstakeConfirmWireframe() + + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: state.chainAsset.assetDisplayInfo, + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager) + ) + + let hintsViewModelFactory = NPoolsUnstakeHintsFactory( + chainAsset: state.chainAsset, + balanceViewModelFactory: balanceViewModelFactory + ) + + let dataValidatingFactory = NominationPoolDataValidatorFactory( + presentable: wireframe, + balanceFactory: balanceViewModelFactory + ) + + let presenter = NPoolsUnstakeConfirmPresenter( + interactor: interactor, + wireframe: wireframe, + unstakingAmount: amount, + selectedAccount: selectedAccount, + chainAsset: state.chainAsset, + hintsViewModelFactory: hintsViewModelFactory, + balanceViewModelFactory: balanceViewModelFactory, + dataValidatorFactory: dataValidatingFactory, + localizationManager: LocalizationManager.shared, + logger: Logger.shared + ) + + let view = NPoolsUnstakeConfirmViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + interactor.presenter = presenter + dataValidatingFactory.view = view + + return view + } + + static func createInteractor(for state: NPoolsStakingSharedStateProtocol) -> NPoolsUnstakeConfirmInteractor? { + let chainAsset = state.chainAsset + let chainRegistry = ChainRegistryFacade.sharedRegistry + + guard + let selectedAccount = SelectedWalletSettings.shared.value?.fetchMetaChainAccount( + for: chainAsset.chain.accountRequest() + ), + let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), + let currencyManager = CurrencyManager.shared else { + return nil + } + + let operationQueue = OperationManagerFacade.sharedDefaultQueue + + let extrinsicService = ExtrinsicServiceFactory( + runtimeRegistry: runtimeService, + engine: connection, + operationManager: OperationManager(operationQueue: operationQueue) + ).createService(account: selectedAccount.chainAccount, chain: chainAsset.chain) + + let eraCountdownOperationFactory = state.createEraCountdownOperationFactory(for: operationQueue) + let durationOperationFactory = state.createStakingDurationOperationFactory() + + let npoolsOperationFactory = NominationPoolsOperationFactory(operationQueue: operationQueue) + + let signingWrapper = SigningWrapperFactory.createSigner(from: selectedAccount) + + return NPoolsUnstakeConfirmInteractor( + selectedAccount: selectedAccount, + chainAsset: chainAsset, + signingWrapper: signingWrapper, + extrinsicService: extrinsicService, + feeProxy: ExtrinsicFeeProxy(), + npoolsLocalSubscriptionFactory: state.npLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.relaychainLocalSubscriptionFactory, + priceLocalSubscriptionFactory: PriceProviderFactory.shared, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + connection: connection, + runtimeService: runtimeService, + eraCountdownOperationFactory: eraCountdownOperationFactory, + durationFactory: durationOperationFactory, + npoolsOperationFactory: npoolsOperationFactory, + unstakeLimitsFactory: NPoolsUnstakeOperationFactory(), + eventCenter: EventCenter.shared, + currencyManager: currencyManager, + operationQueue: operationQueue + ) + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewLayout.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewLayout.swift new file mode 100644 index 0000000000..346a66ab24 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewLayout.swift @@ -0,0 +1,41 @@ +import UIKit + +final class NPoolsUnstakeConfirmViewLayout: SCLoadableActionLayoutView { + let amountView = MultilineBalanceView() + + let walletTableView = StackTableView() + + let walletCell = StackTableCell() + + let accountCell: StackInfoTableCell = { + let cell = StackInfoTableCell() + cell.detailsLabel.lineBreakMode = .byTruncatingMiddle + return cell + }() + + let networkFeeCell = StackNetworkFeeCell() + + var actionButton: TriangularedButton { + genericActionView.actionButton + } + + var loadingView: LoadableActionView { + genericActionView + } + + let hintListView = HintListView() + + override func setupLayout() { + super.setupLayout() + + addArrangedSubview(amountView, spacingAfter: 24) + + addArrangedSubview(walletTableView, spacingAfter: 16) + + walletTableView.addArrangedSubview(walletCell) + walletTableView.addArrangedSubview(accountCell) + walletTableView.addArrangedSubview(networkFeeCell) + + addArrangedSubview(hintListView) + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmWireframe.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmWireframe.swift new file mode 100644 index 0000000000..d2d84e6126 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmWireframe.swift @@ -0,0 +1,3 @@ +import Foundation + +final class NPoolsUnstakeConfirmWireframe: NPoolsUnstakeConfirmWireframeProtocol, ModalAlertPresenting {} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Model/NominationPoolsUnstakeLimits.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Model/NominationPoolsUnstakeLimits.swift new file mode 100644 index 0000000000..1701195071 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Model/NominationPoolsUnstakeLimits.swift @@ -0,0 +1,9 @@ +import Foundation + +extension NominationPools { + struct UnstakeLimits { + let globalMaxUnlockings: UInt32 + let poolMemberMaxUnlockings: UInt32 + let bondingDuration: UInt32 + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Operation/NominationPoolsUnstakeOperationFactory.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Operation/NominationPoolsUnstakeOperationFactory.swift new file mode 100644 index 0000000000..b679ab320c --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Operation/NominationPoolsUnstakeOperationFactory.swift @@ -0,0 +1,89 @@ +import Foundation +import RobinHood + +protocol NPoolsUnstakeOperationFactoryProtocol { + func createLimitsWrapper( + for chain: ChainModel, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper +} + +final class NPoolsUnstakeOperationFactory { + private func createKnownPoolMemberChunksLimit(for chainId: ChainModel.Id) -> BaseOperation { + ClosureOperation { + switch chainId { + case KnowChainId.alephZero: + return 8 + default: + return nil + } + } + } +} + +extension NPoolsUnstakeOperationFactory: NPoolsUnstakeOperationFactoryProtocol { + func createLimitsWrapper( + for chain: ChainModel, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper { + let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() + + let maxUnlockingsOperation: PrimitiveConstantOperation = PrimitiveConstantOperation( + path: Staking.maxUnlockingChunksConstantPath, + fallbackValue: StakingConstants.maxUnlockingChunks + ) + + maxUnlockingsOperation.configurationBlock = { + do { + maxUnlockingsOperation.codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + } catch { + maxUnlockingsOperation.result = .failure(error) + } + } + + let unlockingOperation: PrimitiveConstantOperation = PrimitiveConstantOperation( + path: Staking.maxUnlockingChunksConstantPath + ) + + unlockingOperation.configurationBlock = { + do { + unlockingOperation.codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + } catch { + unlockingOperation.result = .failure(error) + } + } + + maxUnlockingsOperation.addDependency(codingFactoryOperation) + unlockingOperation.addDependency(codingFactoryOperation) + + let knownPoolMemberChunksOperation = createKnownPoolMemberChunksLimit(for: chain.chainId) + + let mapOperation = ClosureOperation { + let maxUnlockChunks = try maxUnlockingsOperation.extractNoCancellableResultData() + let maxMemberChunks = try knownPoolMemberChunksOperation.extractNoCancellableResultData() + let unlockingDuration = try unlockingOperation.extractNoCancellableResultData() + + return .init( + globalMaxUnlockings: maxUnlockChunks, + poolMemberMaxUnlockings: maxMemberChunks ?? maxUnlockChunks, + bondingDuration: unlockingDuration + ) + } + + let dependencies = [ + codingFactoryOperation, + maxUnlockingsOperation, + unlockingOperation, + knownPoolMemberChunksOperation + ] + + dependencies.forEach { operation in + mapOperation.addDependency(operation) + } + + return CompoundOperationWrapper( + targetOperation: mapOperation, + dependencies: dependencies + ) + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupInteractor.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupInteractor.swift new file mode 100644 index 0000000000..6d128b457d --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupInteractor.swift @@ -0,0 +1,15 @@ +import UIKit + +final class NPoolsUnstakeSetupInteractor: NPoolsUnstakeBaseInteractor { + var presenter: NPoolsUnstakeSetupInteractorOutputProtocol? { + get { + basePresenter as? NPoolsUnstakeSetupInteractorOutputProtocol + } + + set { + basePresenter = newValue + } + } +} + +extension NPoolsUnstakeSetupInteractor: NPoolsUnstakeSetupInteractorInputProtocol {} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupPresenter.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupPresenter.swift new file mode 100644 index 0000000000..dc23049a85 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupPresenter.swift @@ -0,0 +1,217 @@ +import Foundation +import SoraFoundation +import BigInt + +final class NPoolsUnstakeSetupPresenter: NPoolsUnstakeBasePresenter { + weak var view: NPoolsUnstakeSetupViewProtocol? + + var wireframe: NPoolsUnstakeSetupWireframeProtocol? { + baseWireframe as? NPoolsUnstakeSetupWireframeProtocol + } + + var interactor: NPoolsUnstakeSetupInteractorInputProtocol? { + baseInteractor as? NPoolsUnstakeSetupInteractorInputProtocol + } + + private var inputResult: AmountInputResult? + + init( + interactor: NPoolsUnstakeSetupInteractorInputProtocol, + wireframe: NPoolsUnstakeSetupWireframeProtocol, + chainAsset: ChainAsset, + hintsViewModelFactory: NPoolsUnstakeHintsFactoryProtocol, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol, + localizationManager: LocalizationManagerProtocol, + logger: LoggerProtocol + ) { + super.init( + baseInteractor: interactor, + baseWireframe: wireframe, + chainAsset: chainAsset, + hintsViewModelFactory: hintsViewModelFactory, + balanceViewModelFactory: balanceViewModelFactory, + dataValidatorFactory: dataValidatorFactory, + localizationManager: localizationManager, + logger: logger + ) + } + + func provideAmountInputViewModel() { + let stakedDecimal = getStakedAmount() ?? 0 + let inputAmount = inputResult?.absoluteValue(from: stakedDecimal) + + let viewModel = balanceViewModelFactory.createBalanceInputViewModel( + inputAmount + ).value(for: selectedLocale) + + view?.didReceiveInput(viewModel: viewModel) + } + + func provideAssetViewModel() { + let stakedAmount = getStakedAmount() ?? 0 + let inputAmount = inputResult?.absoluteValue(from: stakedAmount) ?? 0 + let viewModel = balanceViewModelFactory.createAssetBalanceViewModel( + inputAmount, + balance: stakedAmount, + priceData: price + ).value(for: selectedLocale) + + view?.didReceiveAssetBalance(viewModel: viewModel) + } + + func provideTransferrableBalance() { + guard let balance = assetBalance?.transferable.decimal(precision: chainAsset.asset.precision) else { + view?.didReceiveTransferable(viewModel: nil) + return + } + + let viewModel = balanceViewModelFactory.balanceFromPrice( + balance, + priceData: price + ).value(for: selectedLocale) + + view?.didReceiveTransferable(viewModel: viewModel) + } + + func updateAfterStakeAmountChange() { + provideAssetViewModel() + + if case .rate = inputResult { + provideAmountInputViewModel() + } + } + + // MARK: Subsclass + + override func updateView() { + provideAmountInputViewModel() + provideAssetViewModel() + provideFee() + provideHints() + } + + override func provideHints() { + let hints = hintsViewModelFactory.createHints( + stakingDuration: stakingDuration, + rewards: claimableRewards, + locale: selectedLocale + ) + + view?.didReceiveHints(viewModel: hints) + } + + override func provideFee() { + guard let fee = fee?.decimal(precision: chainAsset.asset.precision) else { + view?.didReceiveFee(viewModel: nil) + return + } + + let viewModel = balanceViewModelFactory.balanceFromPrice(fee, priceData: price).value(for: selectedLocale) + + view?.didReceiveFee(viewModel: viewModel) + } + + override func getInputAmount() -> Decimal? { + let stakedDecimal = getStakedAmount() ?? 0 + return inputResult?.absoluteValue(from: stakedDecimal) + } + + override func getInputAmountInPlank() -> BigUInt? { + guard let decimalAmount = getInputAmount() else { + return nil + } + + return decimalAmount.toSubstrateAmount( + precision: chainAsset.assetDisplayInfo.assetPrecision + ) + } + + override func didReceive(assetBalance: AssetBalance?) { + super.didReceive(assetBalance: assetBalance) + + provideTransferrableBalance() + } + + override func didReceive(poolMember: NominationPools.PoolMember?) { + super.didReceive(poolMember: poolMember) + + updateAfterStakeAmountChange() + } + + override func didReceive(bondedPool: NominationPools.BondedPool?) { + super.didReceive(bondedPool: bondedPool) + + updateAfterStakeAmountChange() + } + + override func didReceive(stakingLedger: StakingLedger?) { + super.didReceive(stakingLedger: stakingLedger) + + updateAfterStakeAmountChange() + } + + override func didReceive(price: PriceData?) { + super.didReceive(price: price) + + provideAssetViewModel() + provideTransferrableBalance() + provideFee() + } +} + +extension NPoolsUnstakeSetupPresenter: NPoolsUnstakeSetupPresenterProtocol { + func setup() { + updateView() + + interactor?.setup() + } + + func selectAmountPercentage(_ percentage: Float) { + inputResult = .rate(Decimal(Double(percentage))) + + provideAmountInputViewModel() + + provideAssetViewModel() + refreshFee() + } + + func updateAmount(_ newValue: Decimal?) { + inputResult = newValue.map { AmountInputResult.absolute($0) } + + provideAssetViewModel() + refreshFee() + } + + func proceed() { + let baseValidations = getValidations() + + var optUnstakingAmount = getInputAmount() + + let minStakeValidationParams = MinStakeCrossedParams( + stakedAmountInPlank: getStakedAmountInPlank(), + minStake: minStake + ) { [weak self] in + optUnstakingAmount = self?.getStakedAmount() + } + + let minStakeValidation = dataValidatorFactory.minStakeNotCrossed( + for: getInputAmount() ?? 0, + params: minStakeValidationParams, + chainAsset: chainAsset, + locale: selectedLocale + ) + + DataValidationRunner( + validators: baseValidations + [minStakeValidation] + ).runValidation { [weak self] in + guard let unstakingAmount = optUnstakingAmount else { + return + } + + self?.wireframe?.showConfirm(from: self?.view, amount: unstakingAmount) + } + } +} + +extension NPoolsUnstakeSetupPresenter: NPoolsUnstakeSetupInteractorOutputProtocol {} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupProtocols.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupProtocols.swift new file mode 100644 index 0000000000..390c35d1eb --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupProtocols.swift @@ -0,0 +1,24 @@ +import Foundation + +protocol NPoolsUnstakeSetupViewProtocol: ControllerBackedProtocol { + func didReceiveAssetBalance(viewModel: AssetBalanceViewModelProtocol) + func didReceiveInput(viewModel: AmountInputViewModelProtocol) + func didReceiveFee(viewModel: BalanceViewModelProtocol?) + func didReceiveTransferable(viewModel: BalanceViewModelProtocol?) + func didReceiveHints(viewModel: [String]) +} + +protocol NPoolsUnstakeSetupPresenterProtocol: AnyObject { + func setup() + func selectAmountPercentage(_ percentage: Float) + func updateAmount(_ newValue: Decimal?) + func proceed() +} + +protocol NPoolsUnstakeSetupInteractorInputProtocol: NPoolsUnstakeBaseInteractorInputProtocol {} + +protocol NPoolsUnstakeSetupInteractorOutputProtocol: NPoolsUnstakeBaseInteractorOutputProtocol {} + +protocol NPoolsUnstakeSetupWireframeProtocol: NPoolsUnstakeBaseWireframeProtocol { + func showConfirm(from view: NPoolsUnstakeSetupViewProtocol?, amount: Decimal) +} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewController.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewController.swift new file mode 100644 index 0000000000..f4b20e7e9f --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewController.swift @@ -0,0 +1,165 @@ +import UIKit +import SoraFoundation + +final class NPoolsUnstakeSetupViewController: UIViewController, ViewHolder { + typealias RootViewType = NPoolsUnstakeSetupViewLayout + + let presenter: NPoolsUnstakeSetupPresenterProtocol + + init(presenter: NPoolsUnstakeSetupPresenterProtocol, localizationManager: LocalizationManagerProtocol) { + self.presenter = presenter + super.init(nibName: nil, bundle: nil) + + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = NPoolsUnstakeSetupViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupHandlers() + setupLocalization() + + presenter.setup() + } + + private func setupLocalization() { + let languages = selectedLocale.rLanguages + + title = R.string.localizable.stakingUnbond_v190(preferredLanguages: languages) + + rootView.amountView.titleView.text = R.string.localizable.walletSendAmountTitle( + preferredLanguages: languages + ) + + rootView.amountView.detailsTitleLabel.text = R.string.localizable.commonStakedPrefix( + preferredLanguages: languages + ) + + rootView.transferableView.titleLabel.text = R.string.localizable.walletBalanceAvailable( + preferredLanguages: languages + ) + + rootView.networkFeeView.locale = selectedLocale + + setupAmountInputAccessoryView() + updateActionButtonState() + } + + private func updateActionButtonState() { + if !rootView.amountInputView.completed { + rootView.actionButton.applyDisabledStyle() + rootView.actionButton.isUserInteractionEnabled = false + + rootView.actionButton.imageWithTitleView?.title = R.string.localizable + .transferSetupEnterAmount(preferredLanguages: selectedLocale.rLanguages) + rootView.actionButton.invalidateLayout() + + return + } + + rootView.actionButton.applyEnabledStyle() + rootView.actionButton.isUserInteractionEnabled = true + + rootView.actionButton.imageWithTitleView?.title = R.string.localizable.commonContinue( + preferredLanguages: selectedLocale.rLanguages + ) + rootView.actionButton.invalidateLayout() + } + + private func setupAmountInputAccessoryView() { + let accessoryView = UIFactory.default.createAmountAccessoryView( + for: self, + locale: selectedLocale + ) + + rootView.amountInputView.textField.inputAccessoryView = accessoryView + } + + private func setupHandlers() { + rootView.amountInputView.addTarget( + self, + action: #selector(actionAmountChange), + for: .editingChanged + ) + + rootView.actionButton.addTarget( + self, + action: #selector(actionProceed), + for: .touchUpInside + ) + } + + @objc func actionAmountChange() { + let amount = rootView.amountInputView.inputViewModel?.decimalAmount + presenter.updateAmount(amount) + + updateActionButtonState() + } + + @objc func actionProceed() { + presenter.proceed() + } +} + +extension NPoolsUnstakeSetupViewController: NPoolsUnstakeSetupViewProtocol { + func didReceiveAssetBalance(viewModel: AssetBalanceViewModelProtocol) { + let assetViewModel = AssetViewModel( + symbol: viewModel.symbol, + imageViewModel: viewModel.iconViewModel + ) + + rootView.amountInputView.bind(assetViewModel: assetViewModel) + rootView.amountInputView.bind(priceViewModel: viewModel.price) + + rootView.amountView.detailsValueLabel.text = viewModel.balance + } + + func didReceiveInput(viewModel: AmountInputViewModelProtocol) { + rootView.amountInputView.bind(inputViewModel: viewModel) + + updateActionButtonState() + } + + func didReceiveFee(viewModel: BalanceViewModelProtocol?) { + rootView.networkFeeView.bind(viewModel: viewModel) + } + + func didReceiveTransferable(viewModel: BalanceViewModelProtocol?) { + rootView.transferableView.bind(viewModel: viewModel) + } + + func didReceiveHints(viewModel: [String]) { + rootView.hintListView.bind(texts: viewModel) + } +} + +extension NPoolsUnstakeSetupViewController: AmountInputAccessoryViewDelegate { + func didSelect(on _: AmountInputAccessoryView, percentage: Float) { + rootView.amountInputView.textField.resignFirstResponder() + + presenter.selectAmountPercentage(percentage) + } + + func didSelectDone(on _: AmountInputAccessoryView) { + rootView.amountInputView.textField.resignFirstResponder() + } +} + +extension NPoolsUnstakeSetupViewController: ImportantViewProtocol {} + +extension NPoolsUnstakeSetupViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewFactory.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewFactory.swift new file mode 100644 index 0000000000..ca8e48dedc --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewFactory.swift @@ -0,0 +1,102 @@ +import Foundation +import RobinHood +import SoraFoundation + +struct NPoolsUnstakeSetupViewFactory { + static func createView(for state: NPoolsStakingSharedStateProtocol) -> NPoolsUnstakeSetupViewProtocol? { + guard + let interactor = createInteractor(for: state), + let currencyManager = CurrencyManager.shared else { + return nil + } + + let wireframe = NPoolsUnstakeSetupWireframe(state: state) + + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: state.chainAsset.assetDisplayInfo, + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager) + ) + + let hintsViewModelFactory = NPoolsUnstakeHintsFactory( + chainAsset: state.chainAsset, + balanceViewModelFactory: balanceViewModelFactory + ) + + let dataValidatingFactory = NominationPoolDataValidatorFactory( + presentable: wireframe, + balanceFactory: balanceViewModelFactory + ) + + let presenter = NPoolsUnstakeSetupPresenter( + interactor: interactor, + wireframe: wireframe, + chainAsset: state.chainAsset, + hintsViewModelFactory: hintsViewModelFactory, + balanceViewModelFactory: balanceViewModelFactory, + dataValidatorFactory: dataValidatingFactory, + localizationManager: LocalizationManager.shared, + logger: Logger.shared + ) + + let view = NPoolsUnstakeSetupViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + interactor.presenter = presenter + dataValidatingFactory.view = view + + return view + } + + private static func createInteractor( + for state: NPoolsStakingSharedStateProtocol + ) -> NPoolsUnstakeSetupInteractor? { + let chainAsset = state.chainAsset + let chainRegistry = ChainRegistryFacade.sharedRegistry + + guard + let selectedAccount = SelectedWalletSettings.shared.value?.fetchMetaChainAccount( + for: chainAsset.chain.accountRequest() + ), + let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), + let currencyManager = CurrencyManager.shared else { + return nil + } + + let operationQueue = OperationManagerFacade.sharedDefaultQueue + + let extrinsicService = ExtrinsicServiceFactory( + runtimeRegistry: runtimeService, + engine: connection, + operationManager: OperationManager(operationQueue: operationQueue) + ).createService(account: selectedAccount.chainAccount, chain: chainAsset.chain) + + let eraCountdownOperationFactory = state.createEraCountdownOperationFactory(for: operationQueue) + let durationOperationFactory = state.createStakingDurationOperationFactory() + + let npoolsOperationFactory = NominationPoolsOperationFactory(operationQueue: operationQueue) + + return NPoolsUnstakeSetupInteractor( + selectedAccount: selectedAccount, + chainAsset: chainAsset, + extrinsicService: extrinsicService, + feeProxy: ExtrinsicFeeProxy(), + npoolsLocalSubscriptionFactory: state.npLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.relaychainLocalSubscriptionFactory, + priceLocalSubscriptionFactory: PriceProviderFactory.shared, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + connection: connection, + runtimeService: runtimeService, + eraCountdownOperationFactory: eraCountdownOperationFactory, + durationFactory: durationOperationFactory, + npoolsOperationFactory: npoolsOperationFactory, + unstakeLimitsFactory: NPoolsUnstakeOperationFactory(), + eventCenter: EventCenter.shared, + currencyManager: currencyManager, + operationQueue: operationQueue + ) + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewLayout.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewLayout.swift new file mode 100644 index 0000000000..c9d68fa089 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewLayout.swift @@ -0,0 +1,41 @@ +import UIKit + +final class NPoolsUnstakeSetupViewLayout: SCSingleActionLayoutView { + let amountView = TitleHorizontalMultiValueView() + + let amountInputView = NewAmountInputView() + + let transferableView = TitleAmountView.dark() + + let networkFeeView = UIFactory.default.createNetworkFeeView() + + let hintListView = HintListView() + + var actionButton: TriangularedButton { + genericActionView + } + + override func setupStyle() { + super.setupStyle() + + actionButton.applyDefaultStyle() + } + + override func setupLayout() { + super.setupLayout() + + addArrangedSubview(amountView, spacingAfter: 8) + amountView.snp.makeConstraints { make in + make.height.equalTo(34.0) + } + + addArrangedSubview(amountInputView, spacingAfter: 16) + amountInputView.snp.makeConstraints { make in + make.height.equalTo(64) + } + + addArrangedSubview(transferableView) + addArrangedSubview(networkFeeView, spacingAfter: 16) + addArrangedSubview(hintListView) + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupWireframe.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupWireframe.swift new file mode 100644 index 0000000000..60cc7910ef --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupWireframe.swift @@ -0,0 +1,18 @@ +import Foundation + +final class NPoolsUnstakeSetupWireframe: NPoolsUnstakeSetupWireframeProtocol { + let state: NPoolsStakingSharedStateProtocol + + init(state: NPoolsStakingSharedStateProtocol) { + self.state = state + } + + func showConfirm(from view: NPoolsUnstakeSetupViewProtocol?, amount: Decimal) { + guard + let confirmView = NPoolsUnstakeConfirmViewFactory.createView(for: amount, state: state) else { + return + } + + view?.controller.navigationController?.pushViewController(confirmView.controller, animated: true) + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/ViewModel/NPoolsUnstakeHintsFactory.swift b/novawallet/Modules/Staking/NominationPools/Unstake/ViewModel/NPoolsUnstakeHintsFactory.swift new file mode 100644 index 0000000000..d080aa03a5 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Unstake/ViewModel/NPoolsUnstakeHintsFactory.swift @@ -0,0 +1,61 @@ +import Foundation +import BigInt + +protocol NPoolsUnstakeHintsFactoryProtocol { + func createHints( + stakingDuration: StakingDuration?, + rewards: BigUInt?, + locale: Locale + ) -> [String] +} + +final class NPoolsUnstakeHintsFactory { + let chainAsset: ChainAsset + let balanceViewModelFactory: BalanceViewModelFactoryProtocol + + init( + chainAsset: ChainAsset, + balanceViewModelFactory: BalanceViewModelFactoryProtocol + ) { + self.chainAsset = chainAsset + self.balanceViewModelFactory = balanceViewModelFactory + } +} + +extension NPoolsUnstakeHintsFactory: NPoolsUnstakeHintsFactoryProtocol { + func createHints( + stakingDuration: StakingDuration?, + rewards: BigUInt?, + locale: Locale + ) -> [String] { + var hints: [String] = [] + + if let stakingDuration = stakingDuration { + let duration = stakingDuration.localizableUnlockingString.value(for: locale) + let hint = R.string.localizable.stakingHintUnstakeFormat_v2_2_0( + duration, + preferredLanguages: locale.rLanguages + ) + + hints.append(hint) + } + + hints.append(contentsOf: [ + R.string.localizable.stakingHintNoRewards_V2_2_0(preferredLanguages: locale.rLanguages), + R.string.localizable.stakingHintRedeem(preferredLanguages: locale.rLanguages) + ]) + + if let rewards = rewards, rewards > 0 { + let decimalAmount = rewards.decimal(precision: chainAsset.asset.precision) + let amountString = balanceViewModelFactory.amountFromValue(decimalAmount).value(for: locale) + let hint = R.string.localizable.stakingPoolRewardsClaimHint( + amountString, + preferredLanguages: locale.rLanguages + ) + + hints.append(hint) + } + + return hints + } +} diff --git a/novawallet/Modules/Staking/Operations/BlockTimeOperationFactory.swift b/novawallet/Modules/Staking/Operations/BlockTimeOperationFactory.swift index 5d21492b5e..aacd25cd04 100644 --- a/novawallet/Modules/Staking/Operations/BlockTimeOperationFactory.swift +++ b/novawallet/Modules/Staking/Operations/BlockTimeOperationFactory.swift @@ -26,9 +26,14 @@ final class BlockTimeOperationFactory { private func createExpectedBlockTimeWrapper( dependingOn codingFactoryOperation: BaseOperation, + chainDefaultTime: BlockTime?, fallbackTime: BlockTime, fallbackThreshold: BlockTime ) -> CompoundOperationWrapper { + if let chainDefaultTime = chainDefaultTime { + return CompoundOperationWrapper.createWithResult(chainDefaultTime) + } + let babeTimeOperation: BaseOperation = PrimitiveConstantOperation.operation( for: .babeBlockTime, dependingOn: codingFactoryOperation @@ -67,6 +72,7 @@ extension BlockTimeOperationFactory: BlockTimeOperationFactoryProtocol { let estimatedOperation = blockTimeEstimationService.createEstimatedBlockTimeOperation() let expectedWrapper = createExpectedBlockTimeWrapper( dependingOn: codingFactoryOperation, + chainDefaultTime: chain.defaultBlockTimeMillis, fallbackTime: fallbackBlockTime, fallbackThreshold: Self.fallbackThreshold ) diff --git a/novawallet/Modules/Staking/Operations/NominationPools/NominationPoolsDataProviding.swift b/novawallet/Modules/Staking/Operations/NominationPools/NominationPoolsDataProviding.swift new file mode 100644 index 0000000000..a74abf279d --- /dev/null +++ b/novawallet/Modules/Staking/Operations/NominationPools/NominationPoolsDataProviding.swift @@ -0,0 +1,46 @@ +import Foundation +import RobinHood + +protocol NominationPoolsDataProviding { + func fetchBondedAccounts( + for operationFactory: NominationPoolsOperationFactoryProtocol, + poolIds: @escaping () throws -> Set, + runtimeService: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue, + completion closure: @escaping (Result<[NominationPools.PoolId: AccountId], Error>) -> Void + ) -> CancellableCall +} + +extension NominationPoolsDataProviding { + func fetchBondedAccounts( + for operationFactory: NominationPoolsOperationFactoryProtocol, + poolIds: @escaping () throws -> Set, + runtimeService: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue, + completion closure: @escaping (Result<[NominationPools.PoolId: AccountId], Error>) -> Void + ) -> CancellableCall { + let wrapper = operationFactory.createBondedAccountsWrapper( + for: poolIds, + runtimeService: runtimeService + ) + + wrapper.targetOperation.completionBlock = { + DispatchQueue.main.async { + guard !wrapper.targetOperation.isCancelled else { + return + } + + do { + let accountIds = try wrapper.targetOperation.extractNoCancellableResultData() + closure(.success(accountIds)) + } catch { + closure(.failure(error)) + } + } + } + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + + return wrapper + } +} diff --git a/novawallet/Modules/Staking/Operations/NominationPools/NominationPoolsFilters.swift b/novawallet/Modules/Staking/Operations/NominationPools/NominationPoolsFilters.swift new file mode 100644 index 0000000000..c2e9d041c8 --- /dev/null +++ b/novawallet/Modules/Staking/Operations/NominationPools/NominationPoolsFilters.swift @@ -0,0 +1,16 @@ +protocol NominationPoolsFilterProtocol { + func apply(for pool: NominationPools.BondedPool) throws -> Bool +} + +final class SpareNominationPoolsFilter: NominationPoolsFilterProtocol { + let maxMembersPerPoolClosure: () throws -> UInt32? + + init(maxMembersPerPoolClosure: @escaping () throws -> UInt32?) { + self.maxMembersPerPoolClosure = maxMembersPerPoolClosure + } + + func apply(for pool: NominationPools.BondedPool) throws -> Bool { + let maxMembersPerPool = try maxMembersPerPoolClosure() + return pool.checkPoolSpare(for: maxMembersPerPool) + } +} diff --git a/novawallet/Modules/Staking/Operations/NominationPools/NominationPoolsOperationFactory.swift b/novawallet/Modules/Staking/Operations/NominationPools/NominationPoolsOperationFactory.swift new file mode 100644 index 0000000000..0f5c26473b --- /dev/null +++ b/novawallet/Modules/Staking/Operations/NominationPools/NominationPoolsOperationFactory.swift @@ -0,0 +1,510 @@ +import Foundation +import SubstrateSdk +import RobinHood +import BigInt + +struct RecommendedNominationPoolsParams { + let maxMembersPerPool: () throws -> UInt32? + let preferrablePool: () throws -> NominationPools.PoolId? +} + +protocol NominationPoolsOperationFactoryProtocol { + func createSparePoolsInfoWrapper( + for poolService: EraNominationPoolsServiceProtocol, + rewardEngine: @escaping () throws -> NominationPoolsRewardEngineProtocol, + maxMembersPerPool: @escaping () throws -> UInt32?, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper<[NominationPools.PoolStats]> + + func createActivePoolsInfoWrapper( + for eraValidationService: EraValidatorServiceProtocol, + lastPoolId: NominationPools.PoolId, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper + + func createBondedPoolsWrapper( + for poolIds: @escaping () throws -> Set, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper<[NominationPools.PoolId: NominationPools.BondedPool]> + + func createMetadataWrapper( + for poolIds: @escaping () throws -> Set, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper<[NominationPools.PoolId: Data]> + + func createBondedAccountsWrapper( + for poolIds: @escaping () throws -> Set, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper<[NominationPools.PoolId: AccountId]> + + func createAllPoolsInfoWrapper( + rewardEngine: @escaping () throws -> NominationPoolsRewardEngineProtocol, + lastPoolId: NominationPools.PoolId, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper<[NominationPools.PoolStats]> + + func createPoolsActiveStakeWrapper( + for poolIds: @escaping () throws -> Set, + eraValidatorService: EraValidatorServiceProtocol, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper<[NominationPools.PoolId: BigUInt]> +} + +extension NominationPoolsOperationFactoryProtocol { + func createPoolsActiveStakeWrapper( + for lastPoolId: NominationPools.PoolId, + eraValidatorService: EraValidatorServiceProtocol, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper<[NominationPools.PoolId: BigUInt]> { + let allPoolIds = Set(0 ... lastPoolId) + + return createPoolsActiveStakeWrapper( + for: { allPoolIds }, + eraValidatorService: eraValidatorService, + runtimeService: runtimeService + ) + } + + func createPoolsActiveTotalStakeWrapper( + for lastPoolId: NominationPools.PoolId, + eraValidatorService: EraValidatorServiceProtocol, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper { + let wrapper = createPoolsActiveStakeWrapper( + for: lastPoolId, + eraValidatorService: eraValidatorService, + runtimeService: runtimeService + ) + + let mapOperation = ClosureOperation { + let stakes = try wrapper.targetOperation.extractNoCancellableResultData() + + return stakes.values.reduce(0) { $0 + $1 } + } + + mapOperation.addDependency(wrapper.targetOperation) + + return CompoundOperationWrapper(targetOperation: mapOperation, dependencies: wrapper.allOperations) + } + + func createPoolRecommendationsInfoWrapper( + for poolService: EraNominationPoolsServiceProtocol, + rewardEngine: @escaping () throws -> NominationPoolsRewardEngineProtocol, + params: RecommendedNominationPoolsParams, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper<[NominationPools.PoolStats]> { + let sparePoolsWrapper = createSparePoolsInfoWrapper( + for: poolService, + rewardEngine: rewardEngine, + maxMembersPerPool: params.maxMembersPerPool, + connection: connection, + runtimeService: runtimeService + ) + + let recommendationOperation = ClosureOperation<[NominationPools.PoolStats]> { + var recommendationList = try sparePoolsWrapper.targetOperation.extractNoCancellableResultData() + + guard + let preferrablePoolId = try params.preferrablePool(), + let currentIndex = recommendationList.firstIndex(where: { $0.poolId == preferrablePoolId }), + currentIndex > 0 else { + return recommendationList + } + + // move preferrable pool to the first place + let preferrablePool = recommendationList.remove(at: currentIndex) + recommendationList.insert(preferrablePool, at: 0) + + return recommendationList + } + + recommendationOperation.addDependency(sparePoolsWrapper.targetOperation) + + return CompoundOperationWrapper( + targetOperation: recommendationOperation, + dependencies: sparePoolsWrapper.allOperations + ) + } +} + +final class NominationPoolsOperationFactory { + let requestFactory: StorageRequestFactoryProtocol + + init(operationQueue: OperationQueue) { + requestFactory = StorageRequestFactory( + remoteFactory: StorageKeyFactory(), + operationManager: OperationManager(operationQueue: operationQueue) + ) + } + + private func createPoolWrapper( + for poolIds: @escaping () throws -> Set, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol, + storagePath: StorageCodingPath + ) -> CompoundOperationWrapper<[NominationPools.PoolId: T]> { + let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() + + let wrapper: CompoundOperationWrapper<[StorageResponse]> = requestFactory.queryItems( + engine: connection, + keyParams: { + try poolIds().sorted().map { StringScaleMapper(value: $0) } + }, + factory: { + try codingFactoryOperation.extractNoCancellableResultData() + }, + storagePath: storagePath + ) + + wrapper.addDependency(operations: [codingFactoryOperation]) + + let mapOperation = ClosureOperation<[NominationPools.PoolId: T]> { + let metadataList = try wrapper.targetOperation.extractNoCancellableResultData() + let poolList = try poolIds().sorted() + + return zip(poolList, metadataList).reduce(into: [NominationPools.PoolId: T]()) { accum, value in + accum[value.0] = value.1.value + } + } + + mapOperation.addDependency(wrapper.targetOperation) + + let dependencies = [codingFactoryOperation] + wrapper.allOperations + + return CompoundOperationWrapper(targetOperation: mapOperation, dependencies: dependencies) + } + + private func createPoolsMergeOperation( + dependedingOn bondedPoolsOperation: BaseOperation<[NominationPools.PoolId: NominationPools.BondedPool]>, + bondedAccountsOperation: BaseOperation<[NominationPools.PoolId: AccountId]>, + metadataOperation: BaseOperation<[NominationPools.PoolId: Data]>, + rewardEngine: @escaping () throws -> NominationPoolsRewardEngineProtocol, + filter: NominationPoolsFilterProtocol? + ) -> BaseOperation<[NominationPools.PoolStats]> { + ClosureOperation<[NominationPools.PoolStats]> { + let bondedPools = try bondedPoolsOperation.extractNoCancellableResultData() + let bondedAccounts = try bondedAccountsOperation.extractNoCancellableResultData() + let metadataDict = try metadataOperation.extractNoCancellableResultData() + let rewardEngine = try rewardEngine() + + let poolStats: [NominationPools.PoolStats] = try bondedPools.keys.compactMap { poolId in + guard + let bondedPool = bondedPools[poolId], + let bondedAccountId = bondedAccounts[poolId] else { + return nil + } + + if let filter = filter, try filter.apply(for: bondedPool) == false { + return nil + } + + let maxPoolApy = try? rewardEngine.calculateMaxReturn(poolId: poolId, isCompound: true, period: .year) + + return NominationPools.PoolStats( + poolId: poolId, + bondedAccountId: bondedAccountId, + membersCount: bondedPool.memberCounter, + maxApy: maxPoolApy?.maxApy, + metadata: metadataDict[poolId], + state: bondedPool.state + ) + } + + return poolStats.sorted { stat1, stat2 in + if stat1.maxApy != stat2.maxApy { + let apy1 = stat1.maxApy ?? 0 + let apy2 = stat2.maxApy ?? 0 + + return apy1 > apy2 + } else if stat1.membersCount != stat2.membersCount { + return stat1.membersCount > stat2.membersCount + } else { + return stat1.poolId > stat2.poolId + } + } + } + } + + private func fetchPoolsInfoWrapper( + pollIdsOperationWrapper: CompoundOperationWrapper>, + rewardEngine: @escaping () throws -> NominationPoolsRewardEngineProtocol, + filter: NominationPoolsFilterProtocol?, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper<[NominationPools.PoolStats]> { + let poolIdsClosure: () throws -> Set = { + try pollIdsOperationWrapper.targetOperation.extractNoCancellableResultData() + } + + let bondedWrapper = createBondedPoolsWrapper( + for: poolIdsClosure, + connection: connection, + runtimeService: runtimeService + ) + + let bondedAccountWrapper = createBondedAccountsWrapper( + for: poolIdsClosure, + runtimeService: runtimeService + ) + + let metadataWrapper = createMetadataWrapper( + for: poolIdsClosure, + connection: connection, + runtimeService: runtimeService + ) + + bondedWrapper.addDependency(wrapper: pollIdsOperationWrapper) + bondedAccountWrapper.addDependency(wrapper: pollIdsOperationWrapper) + metadataWrapper.addDependency(wrapper: pollIdsOperationWrapper) + + let mergeOperation = createPoolsMergeOperation( + dependedingOn: bondedWrapper.targetOperation, + bondedAccountsOperation: bondedAccountWrapper.targetOperation, + metadataOperation: metadataWrapper.targetOperation, + rewardEngine: rewardEngine, + filter: filter + ) + + let dependencies = pollIdsOperationWrapper.allOperations + bondedWrapper.allOperations + + bondedAccountWrapper.allOperations + metadataWrapper.allOperations + + dependencies.forEach { mergeOperation.addDependency($0) } + + return CompoundOperationWrapper(targetOperation: mergeOperation, dependencies: dependencies) + } +} + +extension NominationPoolsOperationFactory: NominationPoolsOperationFactoryProtocol { + func createActivePoolsInfoWrapper( + for eraValidationService: EraValidatorServiceProtocol, + lastPoolId: NominationPools.PoolId, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper { + let validatorsOperation = eraValidationService.fetchInfoOperation() + + let allPoolIds = Set(0 ... lastPoolId) + + let poolAccountsWrapper = createBondedAccountsWrapper( + for: { allPoolIds }, + runtimeService: runtimeService + ) + + let maxNominatorsWrapper: CompoundOperationWrapper = PrimitiveConstantOperation.wrapper( + for: .maxNominatorRewardedPerValidator, + runtimeService: runtimeService + ) + + let validatorsResolveOperation = ClosureOperation<[NominationPools.PoolId: Set]> { + let poolAccounts = try poolAccountsWrapper.targetOperation.extractNoCancellableResultData() + let activeValidators = try validatorsOperation.extractNoCancellableResultData() + let maxRewardedNominators = try maxNominatorsWrapper.targetOperation.extractNoCancellableResultData() + + let indexedValidators = activeValidators.validators.reduce( + into: [AccountId: Set]() + ) { accum, validator in + let allNominators = validator.exposure.others + let targetNominators = Array(allNominators.prefix(Int(maxRewardedNominators))) + + for nominator in targetNominators { + let currentValidators = accum[nominator.who] ?? Set() + accum[nominator.who] = currentValidators.union([validator.accountId]) + } + } + + return poolAccounts.compactMapValues { indexedValidators[$0] } + } + + validatorsResolveOperation.addDependency(poolAccountsWrapper.targetOperation) + validatorsResolveOperation.addDependency(validatorsOperation) + validatorsResolveOperation.addDependency(maxNominatorsWrapper.targetOperation) + + let mapOperation = ClosureOperation { + let activeValidators = try validatorsOperation.extractNoCancellableResultData() + + let resolvedValidators = try validatorsResolveOperation.extractNoCancellableResultData() + let poolAccounts = try poolAccountsWrapper.targetOperation.extractNoCancellableResultData() + + let activePools: [NominationPools.ActivePool] = allPoolIds.compactMap { poolId in + guard + let validatorAccountIds = resolvedValidators[poolId], + let bondedAccountId = poolAccounts[poolId] else { + return nil + } + + return .init( + poolId: poolId, + bondedAccountId: bondedAccountId, + validators: validatorAccountIds + ) + } + + return .init(era: activeValidators.activeEra, pools: activePools) + } + + let dependencies = [validatorsOperation] + maxNominatorsWrapper.allOperations + + poolAccountsWrapper.allOperations + [validatorsResolveOperation] + + dependencies.forEach { mapOperation.addDependency($0) } + + return CompoundOperationWrapper(targetOperation: mapOperation, dependencies: dependencies) + } + + func createBondedPoolsWrapper( + for poolIds: @escaping () throws -> Set, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper<[NominationPools.PoolId: NominationPools.BondedPool]> { + createPoolWrapper( + for: poolIds, + connection: connection, + runtimeService: runtimeService, + storagePath: NominationPools.bondedPoolPath + ) + } + + func createMetadataWrapper( + for poolIds: @escaping () throws -> Set, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper<[NominationPools.PoolId: Data]> { + let wrapper: CompoundOperationWrapper<[NominationPools.PoolId: BytesCodable]> = createPoolWrapper( + for: poolIds, + connection: connection, + runtimeService: runtimeService, + storagePath: NominationPools.metadataPath + ) + + let mapOperation = ClosureOperation<[NominationPools.PoolId: Data]> { + let result = try wrapper.targetOperation.extractNoCancellableResultData() + + return result.mapValues { $0.wrappedValue } + } + + mapOperation.addDependency(wrapper.targetOperation) + + return CompoundOperationWrapper(targetOperation: mapOperation, dependencies: wrapper.allOperations) + } + + func createBondedAccountsWrapper( + for poolIds: @escaping () throws -> Set, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper<[NominationPools.PoolId: AccountId]> { + let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() + + let constantOperation = StorageConstantOperation(path: NominationPools.palletIdPath) + constantOperation.configurationBlock = { + do { + constantOperation.codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + } catch { + constantOperation.result = .failure(error) + } + } + + constantOperation.addDependency(codingFactoryOperation) + + let mergeOperation = ClosureOperation<[NominationPools.PoolId: AccountId]> { + let palletId = try constantOperation.extractNoCancellableResultData().wrappedValue + + return try poolIds().reduce(into: [NominationPools.PoolId: AccountId]()) { accum, poolId in + accum[poolId] = try NominationPools.derivedAccount( + for: poolId, + accountType: .bonded, + palletId: palletId + ) + } + } + + mergeOperation.addDependency(constantOperation) + + return CompoundOperationWrapper( + targetOperation: mergeOperation, + dependencies: [codingFactoryOperation, constantOperation] + ) + } + + func createSparePoolsInfoWrapper( + for poolService: EraNominationPoolsServiceProtocol, + rewardEngine: @escaping () throws -> NominationPoolsRewardEngineProtocol, + maxMembersPerPool: @escaping () throws -> UInt32?, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper<[NominationPools.PoolStats]> { + let activePoolsOperation = poolService.fetchInfoOperation() + let mapOperation = ClosureOperation> { + let poolIds = try activePoolsOperation.extractNoCancellableResultData().pools.map(\.poolId) + return Set(poolIds) + } + mapOperation.addDependency(activePoolsOperation) + let wrapper = CompoundOperationWrapper(targetOperation: mapOperation, dependencies: [activePoolsOperation]) + let filter = SpareNominationPoolsFilter(maxMembersPerPoolClosure: maxMembersPerPool) + + return fetchPoolsInfoWrapper( + pollIdsOperationWrapper: wrapper, + rewardEngine: rewardEngine, + filter: filter, + connection: connection, + runtimeService: runtimeService + ) + } + + func createAllPoolsInfoWrapper( + rewardEngine: @escaping () throws -> NominationPoolsRewardEngineProtocol, + lastPoolId: NominationPools.PoolId, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper<[NominationPools.PoolStats]> { + let allPoolIds = Set(0 ... lastPoolId) + let wrapper = CompoundOperationWrapper.createWithResult(allPoolIds) + + return fetchPoolsInfoWrapper( + pollIdsOperationWrapper: wrapper, + rewardEngine: rewardEngine, + filter: nil, + connection: connection, + runtimeService: runtimeService + ) + } + + func createPoolsActiveStakeWrapper( + for poolIds: @escaping () throws -> Set, + eraValidatorService: EraValidatorServiceProtocol, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper<[NominationPools.PoolId: BigUInt]> { + let bondedAccountIdWrapper = createBondedAccountsWrapper( + for: poolIds, + runtimeService: runtimeService + ) + + let validatorsOperation = eraValidatorService.fetchInfoOperation() + + let mapOperation = ClosureOperation<[NominationPools.PoolId: BigUInt]> { + let validators = try validatorsOperation.extractNoCancellableResultData() + let poolAccountIds = try bondedAccountIdWrapper.targetOperation.extractNoCancellableResultData() + + let stakeByAccountId = validators.validators.reduce(into: [AccountId: BigUInt]()) { accum, validator in + accum[validator.accountId] = validator.exposure.own + + for nominator in validator.exposure.others { + let prevStake = accum[nominator.who] ?? 0 + accum[nominator.who] = prevStake + nominator.value + } + } + + return poolAccountIds.reduce(into: [NominationPools.PoolId: BigUInt]()) { accum, keyValue in + accum[keyValue.key] = stakeByAccountId[keyValue.value] + } + } + + mapOperation.addDependency(bondedAccountIdWrapper.targetOperation) + mapOperation.addDependency(validatorsOperation) + + let dependencies = bondedAccountIdWrapper.allOperations + [validatorsOperation] + + return CompoundOperationWrapper(targetOperation: mapOperation, dependencies: dependencies) + } +} diff --git a/novawallet/Modules/Staking/Operations/ParachainStaking/ParaStkDurationOperationFactory.swift b/novawallet/Modules/Staking/Operations/ParachainStaking/ParaStkDurationOperationFactory.swift index f5dd2c77d9..92a4abc3c1 100644 --- a/novawallet/Modules/Staking/Operations/ParachainStaking/ParaStkDurationOperationFactory.swift +++ b/novawallet/Modules/Staking/Operations/ParachainStaking/ParaStkDurationOperationFactory.swift @@ -2,7 +2,7 @@ import Foundation import RobinHood import SubstrateSdk -struct ParachainStakingDuration { +struct ParachainStakingDuration: Equatable { let block: TimeInterval let round: TimeInterval let unstaking: TimeInterval diff --git a/novawallet/Modules/Staking/Operations/SlashesOperationFactory.swift b/novawallet/Modules/Staking/Operations/SlashesOperationFactory.swift index 35adc7225f..7b6b2628ef 100644 --- a/novawallet/Modules/Staking/Operations/SlashesOperationFactory.swift +++ b/novawallet/Modules/Staking/Operations/SlashesOperationFactory.swift @@ -5,7 +5,7 @@ import SubstrateSdk protocol SlashesOperationFactoryProtocol { func createSlashingSpansOperationForStash( - _ stashAddress: AccountAddress, + _ stashAccount: @escaping () throws -> AccountId, engine: JSONRPCEngine, runtimeService: RuntimeCodingServiceProtocol ) @@ -24,7 +24,7 @@ final class SlashesOperationFactory { extension SlashesOperationFactory: SlashesOperationFactoryProtocol { func createSlashingSpansOperationForStash( - _ stashAddress: AccountAddress, + _ stashAccount: @escaping () throws -> AccountId, engine: JSONRPCEngine, runtimeService: RuntimeCodingServiceProtocol ) @@ -32,7 +32,7 @@ extension SlashesOperationFactory: SlashesOperationFactoryProtocol { let runtimeFetchOperation = runtimeService.fetchCoderFactoryOperation() let keyParams: () throws -> [AccountId] = { - let accountId: AccountId = try stashAddress.toAccountId() + let accountId: AccountId = try stashAccount() return [accountId] } diff --git a/novawallet/Modules/Staking/Operations/ValidatorOperationFactory/ValidatorOperationFactory+Protocol.swift b/novawallet/Modules/Staking/Operations/ValidatorOperationFactory/ValidatorOperationFactory+Protocol.swift index bca7b4ff26..b1a43b7903 100644 --- a/novawallet/Modules/Staking/Operations/ValidatorOperationFactory/ValidatorOperationFactory+Protocol.swift +++ b/novawallet/Modules/Staking/Operations/ValidatorOperationFactory/ValidatorOperationFactory+Protocol.swift @@ -343,4 +343,32 @@ extension ValidatorOperationFactory: ValidatorOperationFactoryProtocol { slashingsWrapper.allOperations + stakeInfoWrapper.allOperations return CompoundOperationWrapper(targetOperation: mergeOperation, dependencies: dependencies) } + + func allPreferred( + for preferredAccountIds: [AccountId] + ) -> CompoundOperationWrapper { + let allElectedWrapper = allElectedOperation() + let wannabeWrapper = !preferredAccountIds.isEmpty ? + wannabeValidatorsOperation(for: preferredAccountIds) : nil + + let mergeOperation = ClosureOperation { + let electedValidators = try allElectedWrapper.targetOperation.extractNoCancellableResultData() + let prefValidators = try wannabeWrapper?.targetOperation.extractNoCancellableResultData() + + return ElectedAndPrefValidators( + electedValidators: electedValidators, + preferredValidators: prefValidators ?? [] + ) + } + + mergeOperation.addDependency(allElectedWrapper.targetOperation) + + if let wrapper = wannabeWrapper { + mergeOperation.addDependency(wrapper.targetOperation) + } + + let dependencies = allElectedWrapper.allOperations + (wannabeWrapper?.allOperations ?? []) + + return CompoundOperationWrapper(targetOperation: mergeOperation, dependencies: dependencies) + } } diff --git a/novawallet/Modules/Staking/Operations/ValidatorOperationFactory/ValidatorOperationFactoryProtocol.swift b/novawallet/Modules/Staking/Operations/ValidatorOperationFactory/ValidatorOperationFactoryProtocol.swift index 3cd7218cf6..dd66c059ab 100644 --- a/novawallet/Modules/Staking/Operations/ValidatorOperationFactory/ValidatorOperationFactoryProtocol.swift +++ b/novawallet/Modules/Staking/Operations/ValidatorOperationFactory/ValidatorOperationFactoryProtocol.swift @@ -19,4 +19,8 @@ protocol ValidatorOperationFactoryProtocol { func wannabeValidatorsOperation( for accountIdList: [AccountId] ) -> CompoundOperationWrapper<[SelectedValidatorInfo]> + + func allPreferred( + for preferredAccountIds: [AccountId] + ) -> CompoundOperationWrapper } diff --git a/novawallet/Modules/Staking/Parachain/ParaStkCollatorInfo/ParaStkCollatorInfoViewFactory.swift b/novawallet/Modules/Staking/Parachain/ParaStkCollatorInfo/ParaStkCollatorInfoViewFactory.swift index 6dd6c47d1b..425abdd7d0 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkCollatorInfo/ParaStkCollatorInfoViewFactory.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkCollatorInfo/ParaStkCollatorInfoViewFactory.swift @@ -3,7 +3,7 @@ import SoraFoundation struct ParaStkCollatorInfoViewFactory { static func createView( - for state: ParachainStakingSharedState, + for state: ParachainStakingSharedStateProtocol, collatorInfo: CollatorSelectionInfo ) -> ParaStkCollatorInfoViewProtocol? { let chainAsset = state.stakingOption.chainAsset diff --git a/novawallet/Modules/Staking/Parachain/ParaStkCollatorsSearch/ParaStkCollatorsSearchViewFactory.swift b/novawallet/Modules/Staking/Parachain/ParaStkCollatorsSearch/ParaStkCollatorsSearchViewFactory.swift index 86ed624e86..7bb2dbb009 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkCollatorsSearch/ParaStkCollatorsSearchViewFactory.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkCollatorsSearch/ParaStkCollatorsSearchViewFactory.swift @@ -3,7 +3,7 @@ import SoraFoundation struct ParaStkCollatorsSearchViewFactory { static func createView( - for state: ParachainStakingSharedState, + for state: ParachainStakingSharedStateProtocol, collators: [CollatorSelectionInfo], delegate: ParaStkSelectCollatorsDelegate ) -> ParaStkCollatorsSearchViewProtocol? { diff --git a/novawallet/Modules/Staking/Parachain/ParaStkCollatorsSearch/ParaStkCollatorsSearchWireframe.swift b/novawallet/Modules/Staking/Parachain/ParaStkCollatorsSearch/ParaStkCollatorsSearchWireframe.swift index c6ec797f8b..195587a5fd 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkCollatorsSearch/ParaStkCollatorsSearchWireframe.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkCollatorsSearch/ParaStkCollatorsSearchWireframe.swift @@ -1,14 +1,21 @@ import Foundation final class ParaStkCollatorsSearchWireframe: ParaStkCollatorsSearchWireframeProtocol { - let sharedState: ParachainStakingSharedState + let sharedState: ParachainStakingSharedStateProtocol - init(sharedState: ParachainStakingSharedState) { + init(sharedState: ParachainStakingSharedStateProtocol) { self.sharedState = sharedState } func complete(on view: ParaStkCollatorsSearchViewProtocol?) { - view?.controller.navigationController?.popToRootViewController(animated: true) + let navigationController = view?.controller.navigationController + let viewControllers = navigationController?.viewControllers ?? [] + + if let setupScreenController = viewControllers.first(where: { $0 is ParaStkStakeSetupViewProtocol }) { + navigationController?.popToViewController(setupScreenController, animated: true) + } else { + view?.controller.navigationController?.popToRootViewController(animated: true) + } } func showCollatorInfo( diff --git a/novawallet/Modules/Staking/Parachain/ParaStkRebond/ParaStkRebondViewFactory.swift b/novawallet/Modules/Staking/Parachain/ParaStkRebond/ParaStkRebondViewFactory.swift index a6cad21101..9d417c268f 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkRebond/ParaStkRebondViewFactory.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkRebond/ParaStkRebondViewFactory.swift @@ -5,7 +5,7 @@ import SoraFoundation struct ParaStkRebondViewFactory { static func createView( - for state: ParachainStakingSharedState, + for state: ParachainStakingSharedStateProtocol, selectedCollator: AccountId, collatorIdentity: AccountIdentity? ) -> ParaStkRebondViewProtocol? { @@ -64,7 +64,7 @@ struct ParaStkRebondViewFactory { } private static func createInteractor( - from state: ParachainStakingSharedState + from state: ParachainStakingSharedStateProtocol ) -> ParaStkRebondInteractor? { let optMetaAccount = SelectedWalletSettings.shared.value let chainRegistry = ChainRegistryFacade.sharedRegistry diff --git a/novawallet/Modules/Staking/Parachain/ParaStkRedeem/ParaStkRedeemViewFactory.swift b/novawallet/Modules/Staking/Parachain/ParaStkRedeem/ParaStkRedeemViewFactory.swift index 31e4691372..a9b0ae5ec7 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkRedeem/ParaStkRedeemViewFactory.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkRedeem/ParaStkRedeemViewFactory.swift @@ -3,7 +3,7 @@ import SoraFoundation import SoraKeystore struct ParaStkRedeemViewFactory { - static func createView(for state: ParachainStakingSharedState) -> ParaStkRedeemViewProtocol? { + static func createView(for state: ParachainStakingSharedStateProtocol) -> ParaStkRedeemViewProtocol? { let chainAsset = state.stakingOption.chainAsset guard @@ -54,7 +54,7 @@ struct ParaStkRedeemViewFactory { } private static func createInteractor( - from state: ParachainStakingSharedState + from state: ParachainStakingSharedStateProtocol ) -> ParaStkRedeemInteractor? { let optMetaAccount = SelectedWalletSettings.shared.value let chainRegistry = ChainRegistryFacade.sharedRegistry diff --git a/novawallet/Modules/Staking/Parachain/ParaStkSelectCollators/ParaStkSelectCollatorsViewFactory.swift b/novawallet/Modules/Staking/Parachain/ParaStkSelectCollators/ParaStkSelectCollatorsViewFactory.swift index 200ffc5f1e..272d46eeea 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkSelectCollators/ParaStkSelectCollatorsViewFactory.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkSelectCollators/ParaStkSelectCollatorsViewFactory.swift @@ -5,7 +5,7 @@ import RobinHood struct ParaStkSelectCollatorsViewFactory { static func createView( - with state: ParachainStakingSharedState, + with state: ParachainStakingSharedStateProtocol, delegate: ParaStkSelectCollatorsDelegate ) -> ParaStkSelectCollatorsViewProtocol? { guard @@ -48,16 +48,12 @@ struct ParaStkSelectCollatorsViewFactory { } private static func createInteractor( - for state: ParachainStakingSharedState + for state: ParachainStakingSharedStateProtocol ) -> ParaStkSelectCollatorsInteractor? { let chainAsset = state.stakingOption.chainAsset - guard - let collatorService = state.collatorService, - let rewardEngineService = state.rewardCalculationService, - let currencyManager = CurrencyManager.shared else { - return nil - } + let collatorService = state.collatorService + let rewardEngineService = state.rewardCalculationService let chain = chainAsset.chain @@ -65,7 +61,8 @@ struct ParaStkSelectCollatorsViewFactory { guard let connection = chainRegistry.getConnection(for: chain.chainId), - let runtimeProvider = chainRegistry.getRuntimeProvider(for: chain.chainId) else { + let runtimeProvider = chainRegistry.getRuntimeProvider(for: chain.chainId), + let currencyManager = CurrencyManager.shared else { return nil } diff --git a/novawallet/Modules/Staking/Parachain/ParaStkSelectCollators/ParaStkSelectCollatorsWireframe.swift b/novawallet/Modules/Staking/Parachain/ParaStkSelectCollators/ParaStkSelectCollatorsWireframe.swift index c516d87650..cac108d12e 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkSelectCollators/ParaStkSelectCollatorsWireframe.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkSelectCollators/ParaStkSelectCollatorsWireframe.swift @@ -1,9 +1,9 @@ import Foundation final class ParaStkSelectCollatorsWireframe: ParaStkSelectCollatorsWireframeProtocol { - let sharedState: ParachainStakingSharedState + let sharedState: ParachainStakingSharedStateProtocol - init(sharedState: ParachainStakingSharedState) { + init(sharedState: ParachainStakingSharedStateProtocol) { self.sharedState = sharedState } diff --git a/novawallet/Modules/Staking/Parachain/ParaStkStakeConfirm/ParaStkStakeConfirmPresenter+StartStaking.swift b/novawallet/Modules/Staking/Parachain/ParaStkStakeConfirm/ParaStkStakeConfirmPresenter+StartStaking.swift index e28fb4d113..5a276c5461 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkStakeConfirm/ParaStkStakeConfirmPresenter+StartStaking.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkStakeConfirm/ParaStkStakeConfirmPresenter+StartStaking.swift @@ -3,31 +3,7 @@ import BigInt extension ParaStkStakeConfirmPresenter { func provideStartStakingHintsViewModel() { - var hints: [String] = [] - let languages = selectedLocale.rLanguages - - if let stakingDuration = stakingDuration { - let roundDuration = stakingDuration.round.localizedDaysHours(for: selectedLocale) - let unstakingPeriod = stakingDuration.unstaking.localizedDaysHours(for: selectedLocale) - - hints.append(contentsOf: [ - R.string.localizable.parachainStakingHintRewardsFormat( - "~\(roundDuration)", - preferredLanguages: languages - ), - R.string.localizable.stakingHintUnstakeFormat_v2_2_0( - "~\(unstakingPeriod)", - preferredLanguages: languages - ) - ]) - } - - hints.append(contentsOf: [ - R.string.localizable.stakingHintNoRewards_V2_2_0(preferredLanguages: languages), - R.string.localizable.stakingHintRedeem_v2_2_0(preferredLanguages: languages) - ]) - - view?.didReceiveHints(viewModel: hints) + view?.didReceiveHints(viewModel: []) } private func createStartStakingValidationRunner( diff --git a/novawallet/Modules/Staking/Parachain/ParaStkStakeConfirm/ParaStkStakeConfirmViewFactory.swift b/novawallet/Modules/Staking/Parachain/ParaStkStakeConfirm/ParaStkStakeConfirmViewFactory.swift index 740c12d441..6e390f844a 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkStakeConfirm/ParaStkStakeConfirmViewFactory.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkStakeConfirm/ParaStkStakeConfirmViewFactory.swift @@ -5,7 +5,7 @@ import SoraKeystore struct ParaStkStakeConfirmViewFactory { static func createView( - for state: ParachainStakingSharedState, + for state: ParachainStakingSharedStateProtocol, collator: DisplayAddress, amount: Decimal, initialDelegator: ParachainStaking.Delegator? @@ -81,7 +81,7 @@ struct ParaStkStakeConfirmViewFactory { } private static func createInteractor( - from state: ParachainStakingSharedState, + from state: ParachainStakingSharedStateProtocol, collator: DisplayAddress ) -> ParaStkStakeConfirmInteractor? { let optMetaAccount = SelectedWalletSettings.shared.value @@ -93,12 +93,13 @@ struct ParaStkStakeConfirmViewFactory { let selectedAccount = optMetaAccount?.fetchMetaChainAccount(for: chainAsset.chain.accountRequest()), let runtimeProvider = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), - let blockEstimationService = state.blockTimeService, let currencyManager = CurrencyManager.shared else { return nil } + let blockEstimationService = state.blockTimeService + let extrinsicService = ExtrinsicServiceFactory( runtimeRegistry: runtimeProvider, engine: connection, diff --git a/novawallet/Modules/Staking/Parachain/ParaStkStakeSetup/ParaStkStakeSetupViewFactory.swift b/novawallet/Modules/Staking/Parachain/ParaStkStakeSetup/ParaStkStakeSetupViewFactory.swift index 8df04ab7d2..7c2f461990 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkStakeSetup/ParaStkStakeSetupViewFactory.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkStakeSetup/ParaStkStakeSetupViewFactory.swift @@ -4,7 +4,7 @@ import SubstrateSdk struct ParaStkStakeSetupViewFactory { static func createView( - with state: ParachainStakingSharedState, + with state: ParachainStakingSharedStateProtocol, initialDelegator: ParachainStaking.Delegator?, initialScheduledRequests: [ParachainStaking.DelegatorScheduledRequest]?, delegationIdentities: [AccountId: AccountIdentity]? @@ -88,17 +88,15 @@ struct ParaStkStakeSetupViewFactory { } private static func createInteractor( - from state: ParachainStakingSharedState + from state: ParachainStakingSharedStateProtocol ) -> ParaStkStakeSetupInteractor? { let optMetaAccount = SelectedWalletSettings.shared.value - let chainRegistry = ChainRegistryFacade.sharedRegistry + let chainRegistry = state.chainRegistry let chainAsset = state.stakingOption.chainAsset guard let selectedAccount = optMetaAccount?.fetchMetaChainAccount(for: chainAsset.chain.accountRequest()), - let collatorService = state.collatorService, - let rewardService = state.rewardCalculationService, let runtimeProvider = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), let currencyManager = CurrencyManager.shared @@ -106,6 +104,9 @@ struct ParaStkStakeSetupViewFactory { return nil } + let collatorService = state.collatorService + let rewardService = state.rewardCalculationService + let extrinsicService = ExtrinsicServiceFactory( runtimeRegistry: runtimeProvider, engine: connection, diff --git a/novawallet/Modules/Staking/Parachain/ParaStkStakeSetup/ParaStkStakeSetupWireframe.swift b/novawallet/Modules/Staking/Parachain/ParaStkStakeSetup/ParaStkStakeSetupWireframe.swift index be6bdfc04b..3549041ad2 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkStakeSetup/ParaStkStakeSetupWireframe.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkStakeSetup/ParaStkStakeSetupWireframe.swift @@ -2,9 +2,9 @@ import Foundation import SoraFoundation final class ParaStkStakeSetupWireframe: ParaStkStakeSetupWireframeProtocol { - let state: ParachainStakingSharedState + let state: ParachainStakingSharedStateProtocol - init(state: ParachainStakingSharedState) { + init(state: ParachainStakingSharedStateProtocol) { self.state = state } diff --git a/novawallet/Modules/Staking/Parachain/ParaStkUnstake/ParaStkUnstakeViewFactory.swift b/novawallet/Modules/Staking/Parachain/ParaStkUnstake/ParaStkUnstakeViewFactory.swift index ed4023af0b..ec97c3a36d 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkUnstake/ParaStkUnstakeViewFactory.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkUnstake/ParaStkUnstakeViewFactory.swift @@ -4,7 +4,7 @@ import SubstrateSdk struct ParaStkUnstakeViewFactory { static func createView( - with state: ParachainStakingSharedState, + with state: ParachainStakingSharedStateProtocol, initialDelegator: ParachainStaking.Delegator?, initialScheduledRequests: [ParachainStaking.DelegatorScheduledRequest]?, delegationIdentities: [AccountId: AccountIdentity]? @@ -69,16 +69,15 @@ struct ParaStkUnstakeViewFactory { } private static func createInteractor( - from state: ParachainStakingSharedState + from state: ParachainStakingSharedStateProtocol ) -> ParaStkUnstakeInteractor? { let optMetaAccount = SelectedWalletSettings.shared.value - let chainRegistry = ChainRegistryFacade.sharedRegistry + let chainRegistry = state.chainRegistry let chainAsset = state.stakingOption.chainAsset guard let selectedAccount = optMetaAccount?.fetchMetaChainAccount(for: chainAsset.chain.accountRequest()), - let blocktimeService = state.blockTimeService, let runtimeProvider = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), let currencyManager = CurrencyManager.shared @@ -86,6 +85,8 @@ struct ParaStkUnstakeViewFactory { return nil } + let blocktimeService = state.blockTimeService + let extrinsicService = ExtrinsicServiceFactory( runtimeRegistry: runtimeProvider, engine: connection, diff --git a/novawallet/Modules/Staking/Parachain/ParaStkUnstake/ParaStkUnstakeWireframe.swift b/novawallet/Modules/Staking/Parachain/ParaStkUnstake/ParaStkUnstakeWireframe.swift index 19f8f7b6c7..d656df4517 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkUnstake/ParaStkUnstakeWireframe.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkUnstake/ParaStkUnstakeWireframe.swift @@ -1,9 +1,9 @@ import Foundation final class ParaStkUnstakeWireframe: ParaStkUnstakeWireframeProtocol { - let state: ParachainStakingSharedState + let state: ParachainStakingSharedStateProtocol - init(state: ParachainStakingSharedState) { + init(state: ParachainStakingSharedStateProtocol) { self.state = state } diff --git a/novawallet/Modules/Staking/Parachain/ParaStkUnstakeConfirm/ParaStkUnstakeConfirmViewFactory.swift b/novawallet/Modules/Staking/Parachain/ParaStkUnstakeConfirm/ParaStkUnstakeConfirmViewFactory.swift index c0d5daf61a..205ba6ad49 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkUnstakeConfirm/ParaStkUnstakeConfirmViewFactory.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkUnstakeConfirm/ParaStkUnstakeConfirmViewFactory.swift @@ -5,7 +5,7 @@ import SoraKeystore struct ParaStkUnstakeConfirmViewFactory { static func createView( - for state: ParachainStakingSharedState, + for state: ParachainStakingSharedStateProtocol, callWrapper: UnstakeCallWrapper, collator: DisplayAddress ) -> ParaStkUnstakeConfirmViewProtocol? { @@ -66,16 +66,15 @@ struct ParaStkUnstakeConfirmViewFactory { } private static func createInteractor( - from state: ParachainStakingSharedState + from state: ParachainStakingSharedStateProtocol ) -> ParaStkUnstakeConfirmInteractor? { let optMetaAccount = SelectedWalletSettings.shared.value - let chainRegistry = ChainRegistryFacade.sharedRegistry + let chainRegistry = state.chainRegistry let chainAsset = state.stakingOption.chainAsset guard let selectedAccount = optMetaAccount?.fetchMetaChainAccount(for: chainAsset.chain.accountRequest()), - let blocktimeService = state.blockTimeService, let runtimeProvider = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), let currencyManager = CurrencyManager.shared @@ -83,6 +82,8 @@ struct ParaStkUnstakeConfirmViewFactory { return nil } + let blocktimeService = state.blockTimeService + let extrinsicService = ExtrinsicServiceFactory( runtimeRegistry: runtimeProvider, engine: connection, diff --git a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostSetup/ParaStkYieldBoostSetupViewFactory.swift b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostSetup/ParaStkYieldBoostSetupViewFactory.swift index 53a1979815..8b4db5821a 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostSetup/ParaStkYieldBoostSetupViewFactory.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostSetup/ParaStkYieldBoostSetupViewFactory.swift @@ -5,7 +5,7 @@ import RobinHood struct ParaStkYieldBoostSetupViewFactory { static func createView( - with state: ParachainStakingSharedState, + with state: ParachainStakingSharedStateProtocol, initData: ParaStkYieldBoostInitState ) -> ParaStkYieldBoostSetupViewProtocol? { let chainAsset = state.stakingOption.chainAsset @@ -60,7 +60,7 @@ struct ParaStkYieldBoostSetupViewFactory { // swiftlint:disable:next function_body_length private static func createInteractor( - with state: ParachainStakingSharedState, + with state: ParachainStakingSharedStateProtocol, currencyManager: CurrencyManagerProtocol ) -> ParaStkYieldBoostSetupInteractor? { let chainRegistry = ChainRegistryFacade.sharedRegistry @@ -72,11 +72,12 @@ struct ParaStkYieldBoostSetupViewFactory { for: chainAsset.chain.accountRequest() ), let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), - let runtimeProvider = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), - let rewardService = state.rewardCalculationService else { + let runtimeProvider = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { return nil } + let rewardService = state.rewardCalculationService + let requestFactory = StorageRequestFactory( remoteFactory: StorageKeyFactory(), operationManager: OperationManagerFacade.sharedManager diff --git a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostSetup/ParaStkYieldBoostSetupWireframe.swift b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostSetup/ParaStkYieldBoostSetupWireframe.swift index 9fa43b1868..68cb4925e7 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostSetup/ParaStkYieldBoostSetupWireframe.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostSetup/ParaStkYieldBoostSetupWireframe.swift @@ -2,9 +2,9 @@ import Foundation import SoraFoundation final class ParaStkYieldBoostSetupWireframe: ParaStkYieldBoostSetupWireframeProtocol { - let state: ParachainStakingSharedState + let state: ParachainStakingSharedStateProtocol - init(state: ParachainStakingSharedState) { + init(state: ParachainStakingSharedStateProtocol) { self.state = state } diff --git a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStart/ParaStkYieldBoostStartViewFactory.swift b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStart/ParaStkYieldBoostStartViewFactory.swift index 3923f4d14f..afc4a38570 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStart/ParaStkYieldBoostStartViewFactory.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStart/ParaStkYieldBoostStartViewFactory.swift @@ -4,7 +4,7 @@ import SoraFoundation struct ParaStkYieldBoostStartViewFactory { static func createView( - with state: ParachainStakingSharedState, + with state: ParachainStakingSharedStateProtocol, confirmModel: ParaStkYieldBoostConfirmModel ) -> ParaStkYieldBoostStartViewProtocol? { let chainAsset = state.stakingOption.chainAsset diff --git a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStop/ParaStkYieldBoostStopViewFactory.swift b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStop/ParaStkYieldBoostStopViewFactory.swift index 76df4e4c03..0f4b2c7cea 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStop/ParaStkYieldBoostStopViewFactory.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkYieldBoostStop/ParaStkYieldBoostStopViewFactory.swift @@ -3,7 +3,7 @@ import SoraFoundation struct ParaStkYieldBoostStopViewFactory { static func createView( - with state: ParachainStakingSharedState, + with state: ParachainStakingSharedStateProtocol, collatorId: AccountId, collatorIdentity: AccountIdentity? ) -> ParaStkYieldBoostStopViewProtocol? { diff --git a/novawallet/Modules/Staking/Parachain/ParaStkYourCollators/ParaStkYourCollatorsViewFactory.swift b/novawallet/Modules/Staking/Parachain/ParaStkYourCollators/ParaStkYourCollatorsViewFactory.swift index a0a6fc50e1..039bfb0c80 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkYourCollators/ParaStkYourCollatorsViewFactory.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkYourCollators/ParaStkYourCollatorsViewFactory.swift @@ -3,7 +3,7 @@ import SoraFoundation import SubstrateSdk struct ParaStkYourCollatorsViewFactory { - static func createView(for state: ParachainStakingSharedState) -> ParaStkYourCollatorsViewProtocol? { + static func createView(for state: ParachainStakingSharedStateProtocol) -> ParaStkYourCollatorsViewProtocol? { let chainAsset = state.stakingOption.chainAsset guard @@ -60,18 +60,19 @@ struct ParaStkYourCollatorsViewFactory { private static func createInteractor( for chainAsset: ChainAsset, selectedAccount: MetaChainAccountResponse, - state: ParachainStakingSharedState + state: ParachainStakingSharedStateProtocol ) -> ParaStkYourCollatorsInteractor? { let chainRegistry = ChainRegistryFacade.sharedRegistry guard let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), - let runtimeProvider = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), - let collatorService = state.collatorService, - let rewardService = state.rewardCalculationService else { + let runtimeProvider = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { return nil } + let collatorService = state.collatorService + let rewardService = state.rewardCalculationService + let requestFactory = StorageRequestFactory( remoteFactory: StorageKeyFactory(), operationManager: OperationManagerFacade.sharedManager diff --git a/novawallet/Modules/Staking/Parachain/ParaStkYourCollators/ParaStkYourCollatorsWireframe.swift b/novawallet/Modules/Staking/Parachain/ParaStkYourCollators/ParaStkYourCollatorsWireframe.swift index c307326c5d..90634fc0f3 100644 --- a/novawallet/Modules/Staking/Parachain/ParaStkYourCollators/ParaStkYourCollatorsWireframe.swift +++ b/novawallet/Modules/Staking/Parachain/ParaStkYourCollators/ParaStkYourCollatorsWireframe.swift @@ -1,9 +1,9 @@ import Foundation final class ParaStkYourCollatorsWireframe: ParaStkYourCollatorsWireframeProtocol { - let state: ParachainStakingSharedState + let state: ParachainStakingSharedStateProtocol - init(state: ParachainStakingSharedState) { + init(state: ParachainStakingSharedStateProtocol) { self.state = state } diff --git a/novawallet/Modules/Staking/Protocols/NominationPoolErrorPresentable.swift b/novawallet/Modules/Staking/Protocols/NominationPoolErrorPresentable.swift new file mode 100644 index 0000000000..37d67a04dc --- /dev/null +++ b/novawallet/Modules/Staking/Protocols/NominationPoolErrorPresentable.swift @@ -0,0 +1,252 @@ +import Foundation + +protocol NominationPoolErrorPresentable: BaseErrorPresentable { + func presentNominationPoolHasNoApy( + from view: ControllerBackedProtocol, + action: @escaping () -> Void, + locale: Locale? + ) + func presentNominationPoolIsDestroing( + from view: ControllerBackedProtocol, + locale: Locale? + ) + func presentPoolIsFullyUnbonding( + from view: ControllerBackedProtocol, + locale: Locale? + ) + + func presentUnstakeAmountToHigh(from view: ControllerBackedProtocol?, locale: Locale) + + func presentNoUnstakeSpace( + from view: ControllerBackedProtocol?, + unstakeAfter: String, + locale: Locale + ) + + func presentCrossedMinStake( + from view: ControllerBackedProtocol?, + minStake: String, + remaining: String, + action: @escaping () -> Void, + locale: Locale + ) + + func presentNoProfitAfterClaimRewards( + from view: ControllerBackedProtocol, + action: @escaping () -> Void, + locale: Locale + ) + + func presentPoolIsNotOpen( + from view: ControllerBackedProtocol, + locale: Locale + ) + + func presentPoolIsFull( + from view: ControllerBackedProtocol, + locale: Locale + ) + + func presentExistentialDepositViolation( + from view: ControllerBackedProtocol, + params: NPoolsEDViolationErrorParams, + action: (() -> Void)?, + locale: Locale + ) +} + +extension NominationPoolErrorPresentable where Self: AlertPresentable & ErrorPresentable { + func presentNominationPoolHasNoApy( + from view: ControllerBackedProtocol, + action: @escaping () -> Void, + locale: Locale? + ) { + let title = R.string.localizable.stakingPoolHasNoApyTitle(preferredLanguages: locale?.rLanguages) + let message = R.string.localizable.stakingPoolHasNoApyMessage(preferredLanguages: locale?.rLanguages) + + presentWarning( + for: title, + message: message, + action: action, + view: view, + locale: locale + ) + } + + func presentNominationPoolIsDestroing( + from view: ControllerBackedProtocol, + locale: Locale? + ) { + let title = R.string.localizable.commonErrorGeneralTitle(preferredLanguages: locale?.rLanguages) + let message = R.string.localizable.stakingPoolRewardsBondMorePoolIsDestroing( + preferredLanguages: locale?.rLanguages) + + let closeAction = R.string.localizable.commonClose(preferredLanguages: locale?.rLanguages) + present(message: message, title: title, closeAction: closeAction, from: view) + } + + func presentPoolIsFullyUnbonding( + from view: ControllerBackedProtocol, + locale: Locale? + ) { + let title = R.string.localizable.stakingPoolRewardsBondMorePoolUnbondingErrorTitle( + preferredLanguages: locale?.rLanguages) + let message = R.string.localizable.stakingPoolRewardsBondMorePoolUnbondingErrorMessage( + preferredLanguages: locale?.rLanguages) + let closeAction = R.string.localizable.commonClose(preferredLanguages: locale?.rLanguages) + + present(message: message, title: title, closeAction: closeAction, from: view) + } + + func presentUnstakeAmountToHigh(from view: ControllerBackedProtocol?, locale: Locale) { + let title = R.string.localizable.commonInsufficientBalance(preferredLanguages: locale.rLanguages) + let message = R.string.localizable.stakingUnstakeTooHighMessage(preferredLanguages: locale.rLanguages) + + present( + message: message, + title: title, + closeAction: R.string.localizable.commonClose(preferredLanguages: locale.rLanguages), + from: view + ) + } + + func presentNoUnstakeSpace( + from view: ControllerBackedProtocol?, + unstakeAfter: String, + locale: Locale + ) { + let title = R.string.localizable.stakingUnstakeNoSpaceTitle(preferredLanguages: locale.rLanguages) + let message = R.string.localizable.stakingUnstakeNoSpaceMessage( + "~\(unstakeAfter)", + preferredLanguages: locale.rLanguages + ) + + present( + message: message, + title: title, + closeAction: R.string.localizable.commonClose(preferredLanguages: locale.rLanguages), + from: view + ) + } + + func presentCrossedMinStake( + from view: ControllerBackedProtocol?, + minStake: String, + remaining: String, + action: @escaping () -> Void, + locale: Locale + ) { + let title = R.string.localizable.stakingUnstakeCrossedMinTitle(preferredLanguages: locale.rLanguages) + let message = R.string.localizable.stakingUnstakeCrossedMinMessage( + minStake, + remaining, + preferredLanguages: locale.rLanguages + ) + + let cancelAction = AlertPresentableAction( + title: R.string.localizable.commonCancel(preferredLanguages: locale.rLanguages) + ) + + let unstakeAllAction = AlertPresentableAction( + title: R.string.localizable.stakingUnstakeAll(preferredLanguages: locale.rLanguages), + handler: action + ) + + let viewModel = AlertPresentableViewModel( + title: title, + message: message, + actions: [cancelAction, unstakeAllAction], + closeAction: nil + ) + + present(viewModel: viewModel, style: .alert, from: view) + } + + func presentNoProfitAfterClaimRewards( + from view: ControllerBackedProtocol, + action: @escaping () -> Void, + locale: Locale + ) { + let title = R.string.localizable.commonConfirmationTitle(preferredLanguages: locale.rLanguages) + let message = R.string.localizable.stakingWarningTinyPayout(preferredLanguages: locale.rLanguages) + + presentWarning( + for: title, + message: message, + action: action, + view: view, + locale: locale + ) + } + + func presentPoolIsNotOpen( + from view: ControllerBackedProtocol, + locale: Locale + ) { + let title = R.string.localizable.stakingPoolIsNotOpenTitle(preferredLanguages: locale.rLanguages) + let message = R.string.localizable.stakingPoolIsNotOpenMessage(preferredLanguages: locale.rLanguages) + + present( + message: message, + title: title, + closeAction: R.string.localizable.commonClose(preferredLanguages: locale.rLanguages), + from: view + ) + } + + func presentPoolIsFull( + from view: ControllerBackedProtocol, + locale: Locale + ) { + let title = R.string.localizable.stakingPoolIsFullTitle(preferredLanguages: locale.rLanguages) + let message = R.string.localizable.stakingPoolIsFullMessage(preferredLanguages: locale.rLanguages) + + present( + message: message, + title: title, + closeAction: R.string.localizable.commonClose(preferredLanguages: locale.rLanguages), + from: view + ) + } + + func presentExistentialDepositViolation( + from view: ControllerBackedProtocol, + params: NPoolsEDViolationErrorParams, + action: (() -> Void)?, + locale: Locale + ) { + let title = R.string.localizable.commonInsufficientBalance(preferredLanguages: locale.rLanguages) + let message = R.string.localizable.stakingPoolEdErrorMessage( + params.availableBalance, + params.minimumBalance, + params.fee, + params.maxStake, + preferredLanguages: locale.rLanguages + ) + + let proceedTitle = R.string.localizable.stakingMaximumAction(preferredLanguages: locale.rLanguages) + + let actions: [AlertPresentableAction] + + if let action = action { + let proceedAction = AlertPresentableAction(title: proceedTitle) { + action() + } + + actions = [proceedAction] + } else { + actions = [] + } + + let closeTitle = R.string.localizable.commonCancel(preferredLanguages: locale.rLanguages) + + let viewModel = AlertPresentableViewModel( + title: title, + message: message, + actions: actions, + closeAction: closeTitle + ) + + present(viewModel: viewModel, style: .alert, from: view) + } +} diff --git a/novawallet/Modules/Staking/Protocols/StakingErrorPresentable.swift b/novawallet/Modules/Staking/Protocols/StakingErrorPresentable.swift index f01bf0e5c1..ecfc4efaa6 100644 --- a/novawallet/Modules/Staking/Protocols/StakingErrorPresentable.swift +++ b/novawallet/Modules/Staking/Protocols/StakingErrorPresentable.swift @@ -1,5 +1,12 @@ import Foundation +struct NPoolsEDViolationErrorParams { + let availableBalance: String + let minimumBalance: String + let fee: String + let maxStake: String +} + protocol StakingErrorPresentable: BaseErrorPresentable { func presentAmountTooLow(value: String, from view: ControllerBackedProtocol, locale: Locale?) @@ -48,12 +55,20 @@ protocol StakingErrorPresentable: BaseErrorPresentable { func presentMaxNumberOfNominatorsReached(from view: ControllerBackedProtocol?, locale: Locale?) - func presentMinStakeViolated( + func presentMinRewardableStakeViolated( from view: ControllerBackedProtocol, action: @escaping () -> Void, minStake: String, locale: Locale? ) + + func presentLockedTokensInPoolStaking( + from view: ControllerBackedProtocol?, + lockReason: String, + availableToStake: String, + directRewardableToStake: String, + locale: Locale? + ) } extension StakingErrorPresentable where Self: AlertPresentable & ErrorPresentable { @@ -219,7 +234,7 @@ extension StakingErrorPresentable where Self: AlertPresentable & ErrorPresentabl present(message: message, title: title, closeAction: closeAction, from: view) } - func presentMinStakeViolated( + func presentMinRewardableStakeViolated( from view: ControllerBackedProtocol, action: @escaping () -> Void, minStake: String, @@ -239,4 +254,26 @@ extension StakingErrorPresentable where Self: AlertPresentable & ErrorPresentabl locale: locale ) } + + func presentLockedTokensInPoolStaking( + from view: ControllerBackedProtocol?, + lockReason: String, + availableToStake: String, + directRewardableToStake: String, + locale: Locale? + ) { + let message = R.string.localizable.stakingLockedPoolViolationError( + lockReason, + availableToStake, + directRewardableToStake, + preferredLanguages: locale?.rLanguages + ) + + let title = R.string.localizable.stakingLockedPoolViolationTitle( + preferredLanguages: locale?.rLanguages + ) + let closeAction = R.string.localizable.commonClose(preferredLanguages: locale?.rLanguages) + + present(message: message, title: title, closeAction: closeAction, from: view) + } } diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/CustomValidatorListPresenter.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/CustomValidatorListPresenter.swift index cd968d448f..38b27651f4 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/CustomValidatorListPresenter.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/CustomValidatorListPresenter.swift @@ -12,7 +12,7 @@ final class CustomValidatorListPresenter { let logger: LoggerProtocol? private let recommendedValidatorList: [SelectedValidatorInfo] - private var fullValidatorList: [SelectedValidatorInfo] + private var fullValidatorList: CustomValidatorsFullList private var filteredValidatorList: [SelectedValidatorInfo] = [] private var viewModel: CustomValidatorListViewModel? @@ -24,7 +24,7 @@ final class CustomValidatorListPresenter { wireframe: CustomValidatorListWireframeProtocol, viewModelFactory: CustomValidatorListViewModelFactory, localizationManager: LocalizationManagerProtocol, - fullValidatorList: [SelectedValidatorInfo], + fullValidatorList: CustomValidatorsFullList, recommendedValidatorList: [SelectedValidatorInfo], selectedValidatorList: SharedList, validatorsSelectionParams: ValidatorsSelectionParams, @@ -48,7 +48,10 @@ final class CustomValidatorListPresenter { private func composeFilteredValidatorList() -> [SelectedValidatorInfo] { let composer = CustomValidatorListComposer(filter: filter) - return composer.compose(from: fullValidatorList) + return composer.compose( + from: fullValidatorList.allValidators, + preferrences: fullValidatorList.preferredValidators + ) } private func updateFilteredValidatorsList() { @@ -59,7 +62,7 @@ final class CustomValidatorListPresenter { let viewModel = viewModelFactory.createViewModel( from: filteredValidatorList, selectedValidatorList: selectedValidatorList.items, - totalValidatorsCount: fullValidatorList.count, + totalValidatorsCount: fullValidatorList.distinctCount(), filter: filter, priceData: priceData, locale: selectedLocale @@ -200,7 +203,7 @@ extension CustomValidatorListPresenter: CustomValidatorListPresenterProtocol { func presentSearch() { wireframe.presentSearch( from: view, - fullValidatorList: fullValidatorList, + fullValidatorList: fullValidatorList.distinctAll(), selectedValidatorList: selectedValidatorList.items, delegate: self ) diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/CustomValidatorListViewFactory.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/CustomValidatorListViewFactory.swift index d4dfea6b5d..5888ec9efd 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/CustomValidatorListViewFactory.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/CustomValidatorListViewFactory.swift @@ -4,7 +4,7 @@ import SoraKeystore enum CustomValidatorListViewFactory { private static func createView( - for stakingState: StakingSharedState, + chainAsset: ChainAsset, selectionValidatorGroups: SelectionValidatorGroups, selectedValidatorList: SharedList, validatorsSelectionParams: ValidatorsSelectionParams, @@ -14,8 +14,6 @@ enum CustomValidatorListViewFactory { return nil } - let chainAsset = stakingState.stakingOption.chainAsset - let interactor = CustomValidatorListInteractor( selectedAsset: chainAsset.asset, priceLocalSubscriptionFactory: PriceProviderFactory.shared, @@ -58,7 +56,7 @@ enum CustomValidatorListViewFactory { extension CustomValidatorListViewFactory { static func createInitiatedBondingView( - for stakingState: StakingSharedState, + for stakingState: RelaychainStakingSharedStateProtocol, selectionValidatorGroups: SelectionValidatorGroups, selectedValidatorList: SharedList, validatorsSelectionParams: ValidatorsSelectionParams, @@ -66,7 +64,27 @@ extension CustomValidatorListViewFactory { ) -> CustomValidatorListViewProtocol? { let wireframe = InitBondingCustomValidatorListWireframe(state: state, stakingState: stakingState) return createView( - for: stakingState, + chainAsset: stakingState.stakingOption.chainAsset, + selectionValidatorGroups: selectionValidatorGroups, + selectedValidatorList: selectedValidatorList, + validatorsSelectionParams: validatorsSelectionParams, + wireframe: wireframe + ) + } + + static func createValidatorListView( + for stakingState: RelaychainStartStakingStateProtocol, + selectionValidatorGroups: SelectionValidatorGroups, + selectedValidatorList: SharedList, + validatorsSelectionParams: ValidatorsSelectionParams, + delegate: StakingSelectValidatorsDelegateProtocol? + ) -> CustomValidatorListViewProtocol? { + let wireframe = StartStakingCustomValidatorListWireframe( + stakingState: stakingState, + delegate: delegate + ) + return createView( + chainAsset: stakingState.chainAsset, selectionValidatorGroups: selectionValidatorGroups, selectedValidatorList: selectedValidatorList, validatorsSelectionParams: validatorsSelectionParams, @@ -75,7 +93,7 @@ extension CustomValidatorListViewFactory { } static func createChangeTargetsView( - for stakingState: StakingSharedState, + for stakingState: RelaychainStakingSharedStateProtocol, selectionValidatorGroups: SelectionValidatorGroups, selectedValidatorList: SharedList, validatorsSelectionParams: ValidatorsSelectionParams, @@ -83,7 +101,7 @@ extension CustomValidatorListViewFactory { ) -> CustomValidatorListViewProtocol? { let wireframe = ChangeTargetsCustomValidatorListWireframe(state: state, stakingState: stakingState) return createView( - for: stakingState, + chainAsset: stakingState.stakingOption.chainAsset, selectionValidatorGroups: selectionValidatorGroups, selectedValidatorList: selectedValidatorList, validatorsSelectionParams: validatorsSelectionParams, @@ -92,7 +110,7 @@ extension CustomValidatorListViewFactory { } static func createChangeYourValidatorsView( - for stakingState: StakingSharedState, + for stakingState: RelaychainStakingSharedStateProtocol, selectionValidatorGroups: SelectionValidatorGroups, selectedValidatorList: SharedList, validatorsSelectionParams: ValidatorsSelectionParams, @@ -100,7 +118,7 @@ extension CustomValidatorListViewFactory { ) -> CustomValidatorListViewProtocol? { let wireframe = YourValidatorList.CustomListWireframe(state: state, stakingState: stakingState) return createView( - for: stakingState, + chainAsset: stakingState.stakingOption.chainAsset, selectionValidatorGroups: selectionValidatorGroups, selectedValidatorList: selectedValidatorList, validatorsSelectionParams: validatorsSelectionParams, diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Model/CustomValidatorListComposer.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Model/CustomValidatorListComposer.swift index c212487ef5..7067b60779 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Model/CustomValidatorListComposer.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Model/CustomValidatorListComposer.swift @@ -13,8 +13,12 @@ class CustomValidatorListComposer { extension CustomValidatorListComposer: RecommendationsComposing { typealias RecommendableType = SelectedValidatorInfo - func compose(from validators: [RecommendableType]) -> [RecommendableType] { - var filtered = validators + func compose( + from recommendables: [RecommendableType], + preferrences: [RecommendableType] + ) -> [RecommendableType] { + let preferredAddresses = Set(preferrences.map(\.address)) + var filtered = preferrences + recommendables.filter { !preferredAddresses.contains($0.address) } if !filter.allowsNoIdentity { filtered = filtered.filter { diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Model/SelectionValidatorGroups.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Model/SelectionValidatorGroups.swift index c6efa3dc76..e37fc4009e 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Model/SelectionValidatorGroups.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Model/SelectionValidatorGroups.swift @@ -1,6 +1,6 @@ import Foundation struct SelectionValidatorGroups { - let fullValidatorList: [SelectedValidatorInfo] + let fullValidatorList: CustomValidatorsFullList let recommendedValidatorList: [SelectedValidatorInfo] } diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/View/CustomValidatorListHeaderView.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/View/CustomValidatorListHeaderView.swift index 4ca9b111f3..edd7cd1dca 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/View/CustomValidatorListHeaderView.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/View/CustomValidatorListHeaderView.swift @@ -4,7 +4,7 @@ class CustomValidatorListHeaderView: UITableViewHeaderFooterView { let titleLabel: UILabel = { let label = UILabel() label.font = .semiBoldCaps2 - label.textColor = R.color.colorTextSecondary() + label.textColor = R.color.colorTextTertiary() label.lineBreakMode = .byTruncatingHead label.setContentHuggingPriority(.defaultLow, for: .horizontal) return label @@ -14,7 +14,7 @@ class CustomValidatorListHeaderView: UITableViewHeaderFooterView { let label = UILabel() label.font = .semiBoldCaps2 label.textAlignment = .right - label.textColor = R.color.colorTextSecondary() + label.textColor = R.color.colorTextTertiary() label.lineBreakMode = .byTruncatingTail label.setContentHuggingPriority(.defaultHigh, for: .horizontal) return label diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/ViewModel/CustomValidatorListViewModelFactory.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/ViewModel/CustomValidatorListViewModelFactory.swift index 2266508e5b..9bc5e1623c 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/ViewModel/CustomValidatorListViewModelFactory.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/ViewModel/CustomValidatorListViewModelFactory.swift @@ -59,8 +59,8 @@ final class CustomValidatorListViewModelFactory { switch filter.sortedBy { case .estimatedReward: - detailsText = - apyFormatter.string(from: validator.stakeReturn as NSNumber) + detailsText = validator.stakeReturn > 0 ? + apyFormatter.string(from: validator.stakeReturn as NSNumber) : "" auxDetailsText = nil case .ownStake: diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Wireframe/ChangeTargetsCustomValidatorListWireframe.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Wireframe/ChangeTargetsCustomValidatorListWireframe.swift index cf4628c3f5..1e7cb18612 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Wireframe/ChangeTargetsCustomValidatorListWireframe.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Wireframe/ChangeTargetsCustomValidatorListWireframe.swift @@ -1,7 +1,7 @@ final class ChangeTargetsCustomValidatorListWireframe: CustomValidatorListWireframe { let state: ExistingBonding - init(state: ExistingBonding, stakingState: StakingSharedState) { + init(state: ExistingBonding, stakingState: RelaychainStakingSharedStateProtocol) { self.state = state super.init(stakingState: stakingState) diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Wireframe/CustomValidatorListWireframe.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Wireframe/CustomValidatorListWireframe.swift index 7a38a41afe..af9f82bafc 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Wireframe/CustomValidatorListWireframe.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Wireframe/CustomValidatorListWireframe.swift @@ -1,9 +1,9 @@ import Foundation class CustomValidatorListWireframe: CustomValidatorListWireframeProtocol { - let stakingState: StakingSharedState + let stakingState: RelaychainStakingSharedStateProtocol - init(stakingState: StakingSharedState) { + init(stakingState: RelaychainStakingSharedStateProtocol) { self.stakingState = stakingState } diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Wireframe/InitBondingCustomValidatorListWireframe.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Wireframe/InitBondingCustomValidatorListWireframe.swift index b16d554279..04dd9a57ad 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Wireframe/InitBondingCustomValidatorListWireframe.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Wireframe/InitBondingCustomValidatorListWireframe.swift @@ -1,7 +1,7 @@ final class InitBondingCustomValidatorListWireframe: CustomValidatorListWireframe { let state: InitiatedBonding - init(state: InitiatedBonding, stakingState: StakingSharedState) { + init(state: InitiatedBonding, stakingState: RelaychainStakingSharedStateProtocol) { self.state = state super.init(stakingState: stakingState) diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Wireframe/StartStakingCustomValidatorListWireframe.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Wireframe/StartStakingCustomValidatorListWireframe.swift new file mode 100644 index 0000000000..f42601eae3 --- /dev/null +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/CustomValidatorList/Wireframe/StartStakingCustomValidatorListWireframe.swift @@ -0,0 +1,89 @@ +final class StartStakingCustomValidatorListWireframe: CustomValidatorListWireframeProtocol { + private let stakingState: RelaychainStartStakingStateProtocol + weak var stakingSelectValidatorsDelegate: StakingSelectValidatorsDelegateProtocol? + + init( + stakingState: RelaychainStartStakingStateProtocol, + delegate: StakingSelectValidatorsDelegateProtocol? + ) { + self.stakingState = stakingState + stakingSelectValidatorsDelegate = delegate + } + + func present( + _ validatorInfo: ValidatorInfoProtocol, + from view: ControllerBackedProtocol? + ) { + guard let validatorInfoView = ValidatorInfoViewFactory.createView( + with: validatorInfo, + chainAsset: stakingState.chainAsset + ) else { + return + } + + view?.controller.navigationController?.pushViewController( + validatorInfoView.controller, + animated: true + ) + } + + func presentFilters( + from view: ControllerBackedProtocol?, + filter: CustomValidatorListFilter, + hasIdentity: Bool, + delegate: ValidatorListFilterDelegate? + ) { + guard let filterView = ValidatorListFilterViewFactory.createView( + chainAsset: stakingState.chainAsset, + filter: filter, + hasIdentity: hasIdentity, + delegate: delegate + ) else { return } + + view?.controller.navigationController?.pushViewController( + filterView.controller, + animated: true + ) + } + + func presentSearch( + from view: ControllerBackedProtocol?, + fullValidatorList: [SelectedValidatorInfo], + selectedValidatorList: [SelectedValidatorInfo], + delegate: ValidatorSearchDelegate? + ) { + guard let searchView = ValidatorSearchViewFactory.createView( + startStakingState: stakingState, + validatorList: fullValidatorList, + selectedValidatorList: selectedValidatorList, + delegate: delegate + ) else { return } + + view?.controller.navigationController?.pushViewController( + searchView.controller, + animated: true + ) + } + + func proceed( + from view: ControllerBackedProtocol?, + validatorList: [SelectedValidatorInfo], + maxTargets: Int, + delegate: SelectedValidatorListDelegate + ) { + guard let selectedValidatorListView = SelectedValidatorListViewFactory.createStartStakingView( + startStakingState: stakingState, + validatorList: validatorList, + maxTargets: maxTargets, + delegate: delegate, + stakingSelectValidatorsDelegate: stakingSelectValidatorsDelegate + ) else { + return + } + + view?.controller.navigationController?.pushViewController( + selectedValidatorListView.controller, + animated: true + ) + } +} diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/ChangeTargets/ChangeTargetsRecommendationWireframe.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/ChangeTargets/ChangeTargetsRecommendationWireframe.swift index d700dc69fd..04c71fa684 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/ChangeTargets/ChangeTargetsRecommendationWireframe.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/ChangeTargets/ChangeTargetsRecommendationWireframe.swift @@ -3,7 +3,7 @@ import Foundation final class ChangeTargetsRecommendationWireframe: RecommendedValidatorListWireframe { let state: ExistingBonding - init(state: ExistingBonding, stakingState: StakingSharedState) { + init(state: ExistingBonding, stakingState: RelaychainStakingSharedStateProtocol) { self.state = state super.init(stakingState: stakingState) diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/InitiatedBonding/InitiatedBondingRecommendationWireframe.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/InitiatedBonding/InitiatedBondingRecommendationWireframe.swift index 729a212e61..5f1b76dbd3 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/InitiatedBonding/InitiatedBondingRecommendationWireframe.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/InitiatedBonding/InitiatedBondingRecommendationWireframe.swift @@ -3,7 +3,7 @@ import Foundation final class InitiatedBondingRecommendationWireframe: RecommendedValidatorListWireframe { let state: InitiatedBonding - init(state: InitiatedBonding, stakingState: StakingSharedState) { + init(state: InitiatedBonding, stakingState: RelaychainStakingSharedStateProtocol) { self.state = state super.init(stakingState: stakingState) diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/RecommendedValidatorListViewFactory.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/RecommendedValidatorListViewFactory.swift index 648c12737a..85e7afa2ad 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/RecommendedValidatorListViewFactory.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/RecommendedValidatorListViewFactory.swift @@ -4,7 +4,7 @@ import SubstrateSdk final class RecommendedValidatorListViewFactory { static func createInitiatedBondingView( - stakingState: StakingSharedState, + stakingState: RelaychainStakingSharedStateProtocol, validators: [SelectedValidatorInfo], maxTargets: Int, state: InitiatedBonding @@ -14,7 +14,7 @@ final class RecommendedValidatorListViewFactory { } static func createChangeTargetsView( - stakingState: StakingSharedState, + stakingState: RelaychainStakingSharedStateProtocol, validators: [SelectedValidatorInfo], maxTargets: Int, state: ExistingBonding @@ -24,7 +24,7 @@ final class RecommendedValidatorListViewFactory { } static func createChangeYourValidatorsView( - stakingState: StakingSharedState, + stakingState: RelaychainStakingSharedStateProtocol, validators: [SelectedValidatorInfo], maxTargets: Int, state: ExistingBonding diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/RecommendedValidatorListWireframe.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/RecommendedValidatorListWireframe.swift index 9145c6988d..07439ee72a 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/RecommendedValidatorListWireframe.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/RecommendedValidatorListWireframe.swift @@ -1,9 +1,9 @@ import Foundation class RecommendedValidatorListWireframe: RecommendedValidatorListWireframeProtocol { - let stakingState: StakingSharedState + let stakingState: RelaychainStakingSharedStateProtocol - init(stakingState: StakingSharedState) { + init(stakingState: RelaychainStakingSharedStateProtocol) { self.stakingState = stakingState } diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/ViewModel/RecommendedValidatorListViewModelFactory.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/ViewModel/RecommendedValidatorListViewModelFactory.swift index fa796cdc7e..96acb38fc2 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/ViewModel/RecommendedValidatorListViewModelFactory.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/RecommendedValidatorList/ViewModel/RecommendedValidatorListViewModelFactory.swift @@ -20,7 +20,7 @@ final class RecommendedValidatorListViewModelFactory { private func createStakeReturnString(from stakeReturn: Decimal?) -> LocalizableResource { LocalizableResource { locale in - guard let stakeReturn = stakeReturn else { return "" } + guard let stakeReturn = stakeReturn, stakeReturn > 0 else { return "" } let percentageFormatter = NumberFormatter.percent.localizableResource().value(for: locale) diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/SelectValidatorsConfirmViewFactory.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/SelectValidatorsConfirmViewFactory.swift index f28da216a8..2c75cd6c3b 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/SelectValidatorsConfirmViewFactory.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsConfirm/SelectValidatorsConfirmViewFactory.swift @@ -7,7 +7,7 @@ import SubstrateSdk final class SelectValidatorsConfirmViewFactory { static func createInitiatedBondingView( for state: PreparedNomination, - stakingState: StakingSharedState + stakingState: RelaychainStakingSharedStateProtocol ) -> SelectValidatorsConfirmViewProtocol? { let keystore = Keychain() @@ -43,7 +43,7 @@ final class SelectValidatorsConfirmViewFactory { static func createChangeTargetsView( for state: PreparedNomination, - stakingState: StakingSharedState + stakingState: RelaychainStakingSharedStateProtocol ) -> SelectValidatorsConfirmViewProtocol? { let wireframe = SelectValidatorsConfirmWireframe() return createExistingBondingView(for: state, wireframe: wireframe, stakingState: stakingState) @@ -51,7 +51,7 @@ final class SelectValidatorsConfirmViewFactory { static func createChangeYourValidatorsView( for state: PreparedNomination, - stakingState: StakingSharedState + stakingState: RelaychainStakingSharedStateProtocol ) -> SelectValidatorsConfirmViewProtocol? { let wireframe = YourValidatorList.SelectValidatorsConfirmWireframe() return createExistingBondingView(for: state, wireframe: wireframe, stakingState: stakingState) @@ -60,7 +60,7 @@ final class SelectValidatorsConfirmViewFactory { private static func createExistingBondingView( for state: PreparedNomination, wireframe: SelectValidatorsConfirmWireframeProtocol, - stakingState: StakingSharedState + stakingState: RelaychainStakingSharedStateProtocol ) -> SelectValidatorsConfirmViewProtocol? { let keystore = Keychain() @@ -89,7 +89,7 @@ final class SelectValidatorsConfirmViewFactory { private static func createView( for interactor: SelectValidatorsConfirmInteractorBase, wireframe: SelectValidatorsConfirmWireframeProtocol, - stakingState: StakingSharedState, + stakingState: RelaychainStakingSharedStateProtocol, title: LocalizableResource, priceAssetInfoFactory: PriceAssetInfoFactoryProtocol ) -> SelectValidatorsConfirmViewProtocol? { @@ -136,7 +136,7 @@ final class SelectValidatorsConfirmViewFactory { private static func createInitiatedBondingInteractor( _ nomination: PreparedNomination, selectedMetaAccount: MetaChainAccountResponse, - stakingState: StakingSharedState, + stakingState: RelaychainStakingSharedStateProtocol, keystore: KeystoreProtocol ) -> SelectValidatorsConfirmInteractorBase? { let chainRegistry = ChainRegistryFacade.sharedRegistry @@ -149,13 +149,12 @@ final class SelectValidatorsConfirmViewFactory { let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), let selectedAccount = try? selectedMetaAccount.toWalletDisplayAddress(), - let currencyManager = CurrencyManager.shared, - let stakingDurationFactory = try? stakingState.createStakingDurationOperationFactory( - for: chainAsset.chain - ) else { + let currencyManager = CurrencyManager.shared else { return nil } + let stakingDurationFactory = stakingState.createStakingDurationOperationFactory() + let extrinsicService = ExtrinsicServiceFactory( runtimeRegistry: runtimeService, engine: connection, @@ -170,7 +169,7 @@ final class SelectValidatorsConfirmViewFactory { return InitiatedBondingConfirmInteractor( selectedAccount: selectedAccount, chainAsset: chainAsset, - stakingLocalSubscriptionFactory: stakingState.stakingLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: stakingState.localSubscriptionFactory, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, extrinsicService: extrinsicService, @@ -185,7 +184,7 @@ final class SelectValidatorsConfirmViewFactory { private static func createChangeTargetsInteractor( _ nomination: PreparedNomination, - state: StakingSharedState, + state: RelaychainStakingSharedStateProtocol, keystore: KeystoreProtocol ) -> SelectValidatorsConfirmInteractorBase? { let chainRegistry = ChainRegistryFacade.sharedRegistry @@ -197,13 +196,12 @@ final class SelectValidatorsConfirmViewFactory { guard let currencyManager = CurrencyManager.shared, let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), - let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), - let stakingDurationFactory = try? state.createStakingDurationOperationFactory( - for: chainAsset.chain - ) else { + let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { return nil } + let stakingDurationFactory = state.createStakingDurationOperationFactory() + let extrinsicSender = nomination.bonding.controllerAccount let extrinsicService = ExtrinsicServiceFactory( @@ -221,7 +219,7 @@ final class SelectValidatorsConfirmViewFactory { return ChangeTargetsConfirmInteractor( chainAsset: chainAsset, - stakingLocalSubscriptionFactory: state.stakingLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.localSubscriptionFactory, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, extrinsicService: extrinsicService, diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/ChangeTargets/ChangeTargetsSelectValidatorsStartWireframe.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/ChangeTargets/ChangeTargetsSelectValidatorsStartWireframe.swift index 512b8cc112..701272322c 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/ChangeTargets/ChangeTargetsSelectValidatorsStartWireframe.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/ChangeTargets/ChangeTargetsSelectValidatorsStartWireframe.swift @@ -2,9 +2,9 @@ import Foundation final class ChangeTargetsSelectValidatorsStartWireframe: SelectValidatorsStartWireframe { let state: ExistingBonding - let stakingState: StakingSharedState + let stakingState: RelaychainStakingSharedStateProtocol - init(state: ExistingBonding, stakingState: StakingSharedState) { + init(state: ExistingBonding, stakingState: RelaychainStakingSharedStateProtocol) { self.state = state self.stakingState = stakingState } diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/InitiatedBonding/InitBondSelectValidatorsStartWireframe.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/InitiatedBonding/InitBondSelectValidatorsStartWireframe.swift index 13e6a0a6c4..b7dd08bb94 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/InitiatedBonding/InitBondSelectValidatorsStartWireframe.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/InitiatedBonding/InitBondSelectValidatorsStartWireframe.swift @@ -2,9 +2,9 @@ import Foundation final class InitBondSelectValidatorsStartWireframe: SelectValidatorsStartWireframe { let state: InitiatedBonding - let stakingState: StakingSharedState + let stakingState: RelaychainStakingSharedStateProtocol - init(state: InitiatedBonding, stakingState: StakingSharedState) { + init(state: InitiatedBonding, stakingState: RelaychainStakingSharedStateProtocol) { self.state = state self.stakingState = stakingState } diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/RecommendationsComposing.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/RecommendationsComposing.swift index 6e2c461643..fca6ba5394 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/RecommendationsComposing.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/RecommendationsComposing.swift @@ -14,7 +14,12 @@ protocol Recommendable { protocol RecommendationsComposing { associatedtype RecommendableType: Recommendable - func compose(from validators: [RecommendableType]) -> [RecommendableType] + + func compose( + from recommendables: [RecommendableType], + preferrences: [RecommendableType] + ) -> [RecommendableType] + func processClusters( items: [RecommendableType], clusterSizeLimit: Int, @@ -53,7 +58,7 @@ extension RecommendationsComposing { } final class RecommendationsComposer { - typealias RecommendableType = ElectedValidatorInfo + typealias RecommendableType = SelectedValidatorInfo let resultSize: Int let clusterSizeLimit: Int @@ -81,11 +86,34 @@ final class RecommendationsComposer { } extension RecommendationsComposer: RecommendationsComposing { - func compose(from validators: [RecommendableType]) -> [RecommendableType] { - if validators.contains(where: { $0.hasIdentity }) { - return composeWithIdentities(from: validators) + func compose( + from recommendables: [RecommendableType], + preferrences: [RecommendableType] + ) -> [RecommendableType] { + let recommendationList: [RecommendableType] + + if recommendables.contains(where: { $0.hasIdentity }) { + recommendationList = composeWithIdentities(from: recommendables) + } else { + recommendationList = composeWithoutIdentities(from: recommendables) + } + + let allIncludedAddresses = Set(recommendationList.map(\.address)) + let validPreferences = preferrences + .filter { !allIncludedAddresses.contains($0.address) && !$0.oversubscribed && !$0.blocked } + + let finalSize = recommendationList.count + validPreferences.count + + let recommendationsWithPrefs: [RecommendableType] + + if finalSize > resultSize { + let dropSize = finalSize - resultSize + recommendationsWithPrefs = recommendationList.dropLast(dropSize) + validPreferences } else { - return composeWithoutIdentities(from: validators) + recommendationsWithPrefs = recommendationList + validPreferences } + + // make sure we don't overload the result with prefs + return Array(recommendationsWithPrefs.prefix(resultSize)) } } diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartInteractor.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartInteractor.swift index bcac56531a..b89db30ae3 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartInteractor.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartInteractor.swift @@ -9,19 +9,22 @@ final class SelectValidatorsStartInteractor: RuntimeConstantFetching { let operationFactory: ValidatorOperationFactoryProtocol let operationManager: OperationManagerProtocol let runtimeService: RuntimeCodingServiceProtocol + let preferredValidators: [AccountId] init( runtimeService: RuntimeCodingServiceProtocol, operationFactory: ValidatorOperationFactoryProtocol, - operationManager: OperationManagerProtocol + operationManager: OperationManagerProtocol, + preferredValidators: [AccountId] ) { self.runtimeService = runtimeService self.operationFactory = operationFactory self.operationManager = operationManager + self.preferredValidators = preferredValidators } private func prepareRecommendedValidatorList() { - let wrapper = operationFactory.allElectedOperation() + let wrapper = operationFactory.allPreferred(for: preferredValidators) wrapper.targetOperation.completionBlock = { [weak self] in DispatchQueue.main.async { diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartPresenter.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartPresenter.swift index ee419b5457..898dfdf2a6 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartPresenter.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartPresenter.swift @@ -11,8 +11,9 @@ final class SelectValidatorsStartPresenter { let existingStashAddress: AccountAddress? let logger: LoggerProtocol? + private var electedAndPrefValidators: ElectedAndPrefValidators? private var electedValidators: [AccountAddress: ElectedValidatorInfo]? - private var recommendedValidators: [ElectedValidatorInfo]? + private var recommendedValidators: [SelectedValidatorInfo]? private var selectedValidators: SharedList? private var maxNominations: Int? private var hasIdentity: Bool? @@ -52,16 +53,19 @@ final class SelectValidatorsStartPresenter { private func updateRecommendedValidators() { guard - let electedValidators = electedValidators, + let electedAndPrefValidators = electedAndPrefValidators, let maxNominations = maxNominations else { return } - let resultLimit = min(electedValidators.count, maxNominations) + let resultLimit = min(electedAndPrefValidators.electedValidators.count, maxNominations) let recomendedValidators = RecommendationsComposer( resultSize: resultLimit, clusterSizeLimit: StakingConstants.targetsClusterLimit - ).compose(from: Array(electedValidators.values)) + ).compose( + from: electedAndPrefValidators.electedToSelectedValidators(for: existingStashAddress), + preferrences: electedAndPrefValidators.preferredValidators + ) recommendedValidators = recomendedValidators } @@ -111,31 +115,31 @@ extension SelectValidatorsStartPresenter: SelectValidatorsStartPresenterProtocol return } - let recommendedValidatorList = recommendedValidators.map { $0.toSelected(for: existingStashAddress) } - wireframe.proceedToRecommendedList( from: view, - validatorList: recommendedValidatorList, + validatorList: recommendedValidators, maxTargets: maxNominations ) } func selectCustomValidators() { guard - let electedValidators = electedValidators, + let electedAndPrefValidators = electedAndPrefValidators, let maxNominations = maxNominations, let hasIdentity = hasIdentity, let selectedValidators = selectedValidators else { return } - let electedValidatorList = electedValidators.values.map { $0.toSelected(for: existingStashAddress) } - let recommendedValidatorList = recommendedValidators?.map { - $0.toSelected(for: existingStashAddress) - } ?? [] + let customValidatorList = CustomValidatorsFullList( + allValidators: electedAndPrefValidators.electedToSelectedValidators(for: existingStashAddress), + preferredValidators: electedAndPrefValidators.preferredValidators + ) + + let recommendedValidatorList = recommendedValidators ?? [] let groups = SelectionValidatorGroups( - fullValidatorList: electedValidatorList, + fullValidatorList: customValidatorList, recommendedValidatorList: recommendedValidatorList ) @@ -166,16 +170,18 @@ extension SelectValidatorsStartPresenter: SelectValidatorsStartPresenterProtocol } extension SelectValidatorsStartPresenter: SelectValidatorsStartInteractorOutputProtocol { - func didReceiveValidators(result: Result<[ElectedValidatorInfo], Error>) { + func didReceiveValidators(result: Result) { switch result { case let .success(validators): - electedValidators = validators.reduce( + electedAndPrefValidators = validators + + electedValidators = validators.electedValidators.reduce( into: [AccountAddress: ElectedValidatorInfo]() ) { dict, validator in dict[validator.address] = validator } - hasIdentity = validators.contains { $0.hasIdentity } + hasIdentity = validators.electedValidators.contains { $0.hasIdentity } updateRecommendedValidators() updateSelectedValidatorsIfNeeded() diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartProtocols.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartProtocols.swift index 20bf387616..8dd8831193 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartProtocols.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartProtocols.swift @@ -19,7 +19,7 @@ protocol SelectValidatorsStartInteractorInputProtocol: AnyObject { } protocol SelectValidatorsStartInteractorOutputProtocol: AnyObject { - func didReceiveValidators(result: Result<[ElectedValidatorInfo], Error>) + func didReceiveValidators(result: Result) func didReceiveMaxNominations(result: Result) } diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartViewFactory.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartViewFactory.swift index c5ab7e8b7e..73f1c685f7 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartViewFactory.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectValidatorsStart/SelectValidatorsStartViewFactory.swift @@ -6,7 +6,7 @@ import SoraFoundation final class SelectValidatorsStartViewFactory { static func createInitiatedBondingView( with state: InitiatedBonding, - stakingState: StakingSharedState + stakingState: RelaychainStakingSharedStateProtocol ) -> SelectValidatorsStartViewProtocol? { let wireframe = InitBondSelectValidatorsStartWireframe(state: state, stakingState: stakingState) return createView( @@ -19,7 +19,7 @@ final class SelectValidatorsStartViewFactory { static func createChangeTargetsView( with state: ExistingBonding, - stakingState: StakingSharedState + stakingState: RelaychainStakingSharedStateProtocol ) -> SelectValidatorsStartViewProtocol? { let wireframe = ChangeTargetsSelectValidatorsStartWireframe( state: state, @@ -36,7 +36,7 @@ final class SelectValidatorsStartViewFactory { static func createChangeYourValidatorsView( with state: ExistingBonding, - stakingState: StakingSharedState + stakingState: RelaychainStakingSharedStateProtocol ) -> SelectValidatorsStartViewProtocol? { let wireframe = YourValidatorList.SelectionStartWireframe(state: state, stakingState: stakingState) return createView( @@ -51,7 +51,7 @@ final class SelectValidatorsStartViewFactory { with wireframe: SelectValidatorsStartWireframeProtocol, existingStashAddress: AccountAddress?, selectedValidators: [SelectedValidatorInfo]?, - stakingState: StakingSharedState + stakingState: RelaychainStakingSharedStateProtocol ) -> SelectValidatorsStartViewProtocol? { let chainRegistry = ChainRegistryFacade.sharedRegistry @@ -59,12 +59,13 @@ final class SelectValidatorsStartViewFactory { guard let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), - let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), - let eraValidatorService = stakingState.eraValidatorService, - let rewardCalculationService = stakingState.rewardCalculationService else { + let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { return nil } + let eraValidatorService = stakingState.eraValidatorService + let rewardCalculationService = stakingState.rewardCalculatorService + let operationManager = OperationManagerFacade.sharedManager let storageOperationFactory = StorageRequestFactory( remoteFactory: StorageKeyFactory(), @@ -85,7 +86,10 @@ final class SelectValidatorsStartViewFactory { let interactor = SelectValidatorsStartInteractor( runtimeService: runtimeService, operationFactory: operationFactory, - operationManager: operationManager + operationManager: operationManager, + preferredValidators: StakingConstants.preferredValidatorIds( + for: chainAsset.chain + ) ) let presenter = SelectValidatorsStartPresenter( diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/SelectedValidatorListViewFactory.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/SelectedValidatorListViewFactory.swift index bd47ea43f5..6118d7254a 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/SelectedValidatorListViewFactory.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/SelectedValidatorListViewFactory.swift @@ -3,7 +3,7 @@ import SoraFoundation struct SelectedValidatorListViewFactory { static func createInitiatedBondingView( - stakingState: StakingSharedState, + stakingState: RelaychainStakingSharedStateProtocol, validatorList: [SelectedValidatorInfo], maxTargets: Int, delegate: SelectedValidatorListDelegate, @@ -11,7 +11,6 @@ struct SelectedValidatorListViewFactory { ) -> SelectedValidatorListViewProtocol? { let wireframe = InitiatedBondingSelectedValidatorListWireframe(state: state, stakingState: stakingState) return createView( - stakingState: stakingState, validatorList: validatorList, maxTargets: maxTargets, delegate: delegate, @@ -20,7 +19,7 @@ struct SelectedValidatorListViewFactory { } static func createChangeTargetsView( - stakingState: StakingSharedState, + stakingState: RelaychainStakingSharedStateProtocol, validatorList: [SelectedValidatorInfo], maxTargets: Int, delegate: SelectedValidatorListDelegate, @@ -28,7 +27,6 @@ struct SelectedValidatorListViewFactory { ) -> SelectedValidatorListViewProtocol? { let wireframe = ChangeTargetsSelectedValidatorListWireframe(state: state, stakingState: stakingState) return createView( - stakingState: stakingState, validatorList: validatorList, maxTargets: maxTargets, delegate: delegate, @@ -37,7 +35,7 @@ struct SelectedValidatorListViewFactory { } static func createChangeYourValidatorsView( - stakingState: StakingSharedState, + stakingState: RelaychainStakingSharedStateProtocol, validatorList: [SelectedValidatorInfo], maxTargets: Int, delegate: SelectedValidatorListDelegate, @@ -45,7 +43,6 @@ struct SelectedValidatorListViewFactory { ) -> SelectedValidatorListViewProtocol? { let wireframe = YourValidatorList.SelectedListWireframe(state: state, stakingState: stakingState) return createView( - stakingState: stakingState, validatorList: validatorList, maxTargets: maxTargets, delegate: delegate, @@ -54,7 +51,6 @@ struct SelectedValidatorListViewFactory { } static func createView( - stakingState _: StakingSharedState, validatorList: [SelectedValidatorInfo], maxTargets: Int, delegate: SelectedValidatorListDelegate, @@ -82,4 +78,23 @@ struct SelectedValidatorListViewFactory { return view } + + static func createStartStakingView( + startStakingState: RelaychainStartStakingStateProtocol, + validatorList: [SelectedValidatorInfo], + maxTargets: Int, + delegate: SelectedValidatorListDelegate, + stakingSelectValidatorsDelegate: StakingSelectValidatorsDelegateProtocol? + ) -> SelectedValidatorListViewProtocol? { + let wireframe = StartStakingSelectedValidatorsListWireframe( + state: startStakingState, + delegate: stakingSelectValidatorsDelegate + ) + return createView( + validatorList: validatorList, + maxTargets: maxTargets, + delegate: delegate, + wireframe: wireframe + ) + } } diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/ViewModel/SelectedValidatorListViewModelFactory.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/ViewModel/SelectedValidatorListViewModelFactory.swift index 1daed9fb26..b69eea0834 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/ViewModel/SelectedValidatorListViewModelFactory.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/ViewModel/SelectedValidatorListViewModelFactory.swift @@ -34,7 +34,8 @@ final class SelectedValidatorListViewModelFactory { return validatorList.map { validator in let icon = try? self.iconGenerator.generateFromAddress(validator.address) - let detailsText = apyFormatter.string(from: validator.stakeReturn as NSNumber) + let detailsText = validator.stakeReturn > 0 ? + apyFormatter.string(from: validator.stakeReturn as NSNumber) : "" return SelectedValidatorCellViewModel( icon: icon, diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/Wireframe/ChangeTargetsSelectedValidatorsListWireframe.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/Wireframe/ChangeTargetsSelectedValidatorsListWireframe.swift index bcf0b9bb6a..dfd729177e 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/Wireframe/ChangeTargetsSelectedValidatorsListWireframe.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/Wireframe/ChangeTargetsSelectedValidatorsListWireframe.swift @@ -1,7 +1,7 @@ final class ChangeTargetsSelectedValidatorListWireframe: SelectedValidatorListWireframe { let state: ExistingBonding - init(state: ExistingBonding, stakingState: StakingSharedState) { + init(state: ExistingBonding, stakingState: RelaychainStakingSharedStateProtocol) { self.state = state super.init(stakingState: stakingState) diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/Wireframe/InitiatedBondingSelectedValidatorsListWireframe.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/Wireframe/InitiatedBondingSelectedValidatorsListWireframe.swift index 43319cf8a1..48c6e9ea9f 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/Wireframe/InitiatedBondingSelectedValidatorsListWireframe.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/Wireframe/InitiatedBondingSelectedValidatorsListWireframe.swift @@ -1,7 +1,7 @@ final class InitiatedBondingSelectedValidatorListWireframe: SelectedValidatorListWireframe { let state: InitiatedBonding - init(state: InitiatedBonding, stakingState: StakingSharedState) { + init(state: InitiatedBonding, stakingState: RelaychainStakingSharedStateProtocol) { self.state = state super.init(stakingState: stakingState) diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/Wireframe/SelectedValidatorListWireframe.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/Wireframe/SelectedValidatorListWireframe.swift index 01676a80e9..4090190a26 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/Wireframe/SelectedValidatorListWireframe.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/Wireframe/SelectedValidatorListWireframe.swift @@ -1,7 +1,7 @@ class SelectedValidatorListWireframe: SelectedValidatorListWireframeProtocol { - let stakingState: StakingSharedState + let stakingState: RelaychainStakingSharedStateProtocol - init(stakingState: StakingSharedState) { + init(stakingState: RelaychainStakingSharedStateProtocol) { self.stakingState = stakingState } diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/Wireframe/StartStakingSelectedValidatorsListWireframe.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/Wireframe/StartStakingSelectedValidatorsListWireframe.swift new file mode 100644 index 0000000000..c875d771b4 --- /dev/null +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/SelectedValidatorList/Wireframe/StartStakingSelectedValidatorsListWireframe.swift @@ -0,0 +1,52 @@ +final class StartStakingSelectedValidatorsListWireframe: SelectedValidatorListWireframeProtocol { + let state: RelaychainStartStakingStateProtocol + weak var delegate: StakingSelectValidatorsDelegateProtocol? + + init( + state: RelaychainStartStakingStateProtocol, + delegate: StakingSelectValidatorsDelegateProtocol? + ) { + self.delegate = delegate + self.state = state + } + + func present(_ validatorInfo: ValidatorInfoProtocol, from view: ControllerBackedProtocol?) { + guard let validatorInfoView = ValidatorInfoViewFactory.createView( + with: validatorInfo, + chainAsset: state.chainAsset + ) else { + return + } + + view?.controller.navigationController?.pushViewController( + validatorInfoView.controller, + animated: true + ) + } + + func dismiss(_ view: ControllerBackedProtocol?) { + view?.controller + .navigationController? + .popViewController(animated: true) + } + + func proceed( + from view: SelectedValidatorListViewProtocol?, + targets: [SelectedValidatorInfo], + maxTargets: Int + ) { + delegate?.changeValidatorsSelection( + validatorList: targets, + maxTargets: maxTargets + ) + + if let setupAmountView: StakingSetupAmountViewProtocol = view?.controller.navigationController?.findTopView() { + view?.controller.navigationController?.popToViewController( + setupAmountView.controller, + animated: true + ) + } else { + view?.controller.navigationController?.popViewController(animated: true) + } + } +} diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListPresenter.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListPresenter.swift new file mode 100644 index 0000000000..d2145cbaa4 --- /dev/null +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListPresenter.swift @@ -0,0 +1,61 @@ +import Foundation +import SoraFoundation + +final class StaticValidatorListPresenter { + weak var view: StaticValidatorListViewProtocol? + + let wireframe: StaticValidatorListWireframeProtocol + let viewModelFactory: SelectedValidatorListViewModelFactory + let maxTargets: Int + + private var selectedValidatorList: [SelectedValidatorInfo] + + init( + wireframe: StaticValidatorListWireframeProtocol, + viewModelFactory: SelectedValidatorListViewModelFactory, + selectedValidatorList: [SelectedValidatorInfo], + maxTargets: Int, + localizationManager: LocalizationManagerProtocol + ) { + self.wireframe = wireframe + self.viewModelFactory = viewModelFactory + self.selectedValidatorList = selectedValidatorList + self.maxTargets = maxTargets + self.localizationManager = localizationManager + } + + // MARK: - Private functions + + private func provideViewModel() { + let viewModel = viewModelFactory.createViewModel( + from: selectedValidatorList, + totalValidatorsCount: maxTargets, + locale: selectedLocale + ) + + view?.didReload(viewModel) + } +} + +// MARK: - SelectedValidatorListPresenterProtocol + +extension StaticValidatorListPresenter: StaticValidatorListPresenterProtocol { + func setup() { + provideViewModel() + } + + func didSelectValidator(at index: Int) { + let validatorInfo = selectedValidatorList[index] + wireframe.present(validatorInfo, from: view) + } +} + +// MARK: - Localizable + +extension StaticValidatorListPresenter: Localizable { + func applyLocalization() { + if let view = view, view.isSetup { + provideViewModel() + } + } +} diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListProtocols.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListProtocols.swift new file mode 100644 index 0000000000..dc679f966d --- /dev/null +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListProtocols.swift @@ -0,0 +1,17 @@ +import Foundation + +protocol StaticValidatorListViewProtocol: ControllerBackedProtocol { + func didReload(_ viewModel: SelectedValidatorListViewModel) +} + +protocol StaticValidatorListPresenterProtocol: AnyObject { + func setup() + func didSelectValidator(at index: Int) +} + +protocol StaticValidatorListWireframeProtocol: AnyObject { + func present( + _ validatorInfo: ValidatorInfoProtocol, + from view: ControllerBackedProtocol? + ) +} diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListViewController.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListViewController.swift new file mode 100644 index 0000000000..cca6ec0365 --- /dev/null +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListViewController.swift @@ -0,0 +1,134 @@ +import UIKit +import SoraFoundation + +class StaticValidatorListViewController: UIViewController, ViewHolder { + typealias RootViewType = StaticValidatorListViewLayout + + let presenter: StaticValidatorListPresenterProtocol + let selectedValidatorsLimit: Int + + private var viewModel: SelectedValidatorListViewModel? + + // MARK: - Lifecycle + + init( + presenter: StaticValidatorListPresenterProtocol, + selectedValidatorsLimit: Int, + localizationManager: LocalizationManagerProtocol? = nil + ) { + self.presenter = presenter + self.selectedValidatorsLimit = selectedValidatorsLimit + + super.init(nibName: nil, bundle: nil) + + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = StaticValidatorListViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupTable() + + applyLocalization() + + presenter.setup() + } + + // MARK: - Private functions + + private func setupTable() { + rootView.tableView.dataSource = self + rootView.tableView.delegate = self + rootView.tableView.registerClassForCell(SelectedValidatorCell.self) + rootView.tableView.registerHeaderFooterView(withClass: SelectedValidatorListHeaderView.self) + } + + private func updateHeaderView() { + guard let viewModel = viewModel, + let headerView = rootView.tableView + .headerView(forSection: 0) as? SelectedValidatorListHeaderView + else { return } + + headerView.bind( + viewModel: viewModel.headerViewModel, + shouldAlert: viewModel.limitIsExceeded + ) + } + + private func presentValidatorInfo(at index: Int) { + presenter.didSelectValidator(at: index) + } +} + +// MARK: - Localizable + +extension StaticValidatorListViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + title = R.string.localizable + .stakingSelectedValidatorsTitle(preferredLanguages: selectedLocale.rLanguages) + + updateHeaderView() + } + } +} + +// MARK: - SelectedValidatorListViewProtocol + +extension StaticValidatorListViewController: StaticValidatorListViewProtocol { + func didReload(_ viewModel: SelectedValidatorListViewModel) { + self.viewModel = viewModel + rootView.tableView.reloadData() + } +} + +// MARK: - UITableViewDataSource + +extension StaticValidatorListViewController: UITableViewDataSource { + func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { + viewModel?.cellViewModels.count ?? 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let viewModel = viewModel else { + return UITableViewCell() + } + + let cell = tableView.dequeueReusableCellWithType(SelectedValidatorCell.self)! + + let cellViewModel = viewModel.cellViewModels[indexPath.row] + cell.bind(viewModel: cellViewModel) + + return cell + } +} + +// MARK: - UITableViewDelegate + +extension StaticValidatorListViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + presenter.didSelectValidator(at: indexPath.row) + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection _: Int) -> UIView? { + guard let viewModel = viewModel else { return nil } + + let headerView: SelectedValidatorListHeaderView = tableView.dequeueReusableHeaderFooterView() + headerView.bind( + viewModel: viewModel.headerViewModel, + shouldAlert: viewModel.limitIsExceeded + ) + + return headerView + } +} diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListViewFactory.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListViewFactory.swift new file mode 100644 index 0000000000..ecf0ad5e15 --- /dev/null +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListViewFactory.swift @@ -0,0 +1,32 @@ +import Foundation +import SoraFoundation + +struct StaticValidatorListViewFactory { + static func createView( + validatorList: PreparedValidators, + stakingState: RelaychainStartStakingStateProtocol + ) -> StaticValidatorListViewProtocol? { + let viewModelFactory = SelectedValidatorListViewModelFactory() + let wireframe = StaticValidatorListWireframe(stakingState: stakingState) + + let selectedValidators = validatorList.targets + + let presenter = StaticValidatorListPresenter( + wireframe: wireframe, + viewModelFactory: viewModelFactory, + selectedValidatorList: selectedValidators, + maxTargets: validatorList.maxTargets, + localizationManager: LocalizationManager.shared + ) + + let view = StaticValidatorListViewController( + presenter: presenter, + selectedValidatorsLimit: validatorList.maxTargets, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + + return view + } +} diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListViewLayout.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListViewLayout.swift new file mode 100644 index 0000000000..7d673779e7 --- /dev/null +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListViewLayout.swift @@ -0,0 +1,32 @@ +import UIKit + +final class StaticValidatorListViewLayout: UIView { + let tableView: UITableView = { + let tableView = UITableView() + tableView.tableFooterView = UIView() + tableView.backgroundColor = .clear + tableView.separatorStyle = .none + return tableView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = R.color.colorSecondaryScreenBackground() + + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + addSubview(tableView) + tableView.snp.makeConstraints { make in + make.leading.trailing.top.equalTo(safeAreaLayoutGuide) + make.bottom.equalToSuperview() + } + } +} diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListWireframe.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListWireframe.swift new file mode 100644 index 0000000000..ced174d53e --- /dev/null +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/StaticValidatorList/StaticValidatorListWireframe.swift @@ -0,0 +1,23 @@ +import Foundation + +class StaticValidatorListWireframe: StaticValidatorListWireframeProtocol { + let stakingState: RelaychainStartStakingStateProtocol + + init(stakingState: RelaychainStartStakingStateProtocol) { + self.stakingState = stakingState + } + + func present(_ validatorInfo: ValidatorInfoProtocol, from view: ControllerBackedProtocol?) { + guard let validatorInfoView = ValidatorInfoViewFactory.createView( + with: validatorInfo, + chainAsset: stakingState.chainAsset + ) else { + return + } + + view?.controller.navigationController?.pushViewController( + validatorInfoView.controller, + animated: true + ) + } +} diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/ValidatorInfo/ValidatorInfoViewFactory.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/ValidatorInfo/ValidatorInfoViewFactory.swift index 4f07d44fa8..8460ff8483 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/ValidatorInfo/ValidatorInfoViewFactory.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/ValidatorInfo/ValidatorInfoViewFactory.swift @@ -50,10 +50,17 @@ final class ValidatorInfoViewFactory { extension ValidatorInfoViewFactory { static func createView( with validatorInfo: ValidatorInfoProtocol, - state: StakingSharedState + state: RelaychainStakingSharedStateProtocol ) -> ValidatorInfoViewProtocol? { let chainAsset = state.stakingOption.chainAsset + return createView(with: validatorInfo, chainAsset: chainAsset) + } + + static func createView( + with validatorInfo: ValidatorInfoProtocol, + chainAsset: ChainAsset + ) -> ValidatorInfoViewProtocol? { guard let currencyManager = CurrencyManager.shared else { return nil } let interactor = AnyValidatorInfoInteractor( @@ -72,7 +79,7 @@ extension ValidatorInfoViewFactory { static func createView( with accountAddress: AccountAddress, - state: StakingSharedState + state: RelaychainStakingSharedStateProtocol ) -> ValidatorInfoViewProtocol? { let chainRegistry = ChainRegistryFacade.sharedRegistry @@ -81,11 +88,12 @@ extension ValidatorInfoViewFactory { guard let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), - let eraValidatorService = state.eraValidatorService, - let rewardCalculationService = state.rewardCalculationService, let currencyManager = CurrencyManager.shared else { return nil } + let eraValidatorService = state.eraValidatorService + let rewardCalculationService = state.rewardCalculatorService + let storageRequestFactory = StorageRequestFactory( remoteFactory: StorageKeyFactory(), operationManager: OperationManagerFacade.sharedManager diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/ValidatorListFilter/ValidatorListFilterViewFactory.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/ValidatorListFilter/ValidatorListFilterViewFactory.swift index 7db132ff20..1404d60f8a 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/ValidatorListFilter/ValidatorListFilterViewFactory.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/ValidatorListFilter/ValidatorListFilterViewFactory.swift @@ -3,13 +3,27 @@ import SoraKeystore struct ValidatorListFilterViewFactory { static func createView( - for state: StakingSharedState, + for state: RelaychainStakingSharedStateProtocol, filter: CustomValidatorListFilter, hasIdentity: Bool, delegate: ValidatorListFilterDelegate? ) -> ValidatorListFilterViewProtocol? { let chainAsset = state.stakingOption.chainAsset + return createView( + chainAsset: chainAsset, + filter: filter, + hasIdentity: hasIdentity, + delegate: delegate + ) + } + + static func createView( + chainAsset: ChainAsset, + filter: CustomValidatorListFilter, + hasIdentity: Bool, + delegate: ValidatorListFilterDelegate? + ) -> ValidatorListFilterViewProtocol? { let wireframe = ValidatorListFilterWireframe() let viewModelFactory = ValidatorListFilterViewModelFactory() diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/ValidatorSearch/ValidatorSearchViewFactory.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/ValidatorSearch/ValidatorSearchViewFactory.swift index 3530246993..74727c85c9 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/ValidatorSearch/ValidatorSearchViewFactory.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/ValidatorSearch/ValidatorSearchViewFactory.swift @@ -5,17 +5,14 @@ import SubstrateSdk struct ValidatorSearchViewFactory { private static func createInteractor( - state: StakingSharedState + chainAsset: ChainAsset, + eraValidatorService: EraValidatorServiceProtocol, + rewardCalculationService: RewardCalculatorServiceProtocol ) -> ValidatorSearchInteractor? { let chainRegistry = ChainRegistryFacade.sharedRegistry - - let chainAsset = state.stakingOption.chainAsset - guard let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), - let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), - let eraValidatorService = state.eraValidatorService, - let rewardCalculationService = state.rewardCalculationService else { + let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) else { return nil } @@ -43,16 +40,61 @@ struct ValidatorSearchViewFactory { extension ValidatorSearchViewFactory { static func createView( - for state: StakingSharedState, + for state: RelaychainStakingSharedStateProtocol, + validatorList: [SelectedValidatorInfo], + selectedValidatorList: [SelectedValidatorInfo], + delegate: ValidatorSearchDelegate? + ) -> ValidatorSearchViewProtocol? { + guard let interactor = createInteractor( + chainAsset: state.stakingOption.chainAsset, + eraValidatorService: state.eraValidatorService, + rewardCalculationService: state.rewardCalculatorService + ) else { + return nil + } + + let wireframe = ValidatorSearchWireframe(chainAsset: state.stakingOption.chainAsset) + + let viewModelFactory = ValidatorSearchViewModelFactory() + + let presenter = ValidatorSearchPresenter( + wireframe: wireframe, + interactor: interactor, + viewModelFactory: viewModelFactory, + fullValidatorList: validatorList, + selectedValidatorList: selectedValidatorList, + localizationManager: LocalizationManager.shared, + logger: Logger.shared + ) + + presenter.delegate = delegate + interactor.presenter = presenter + + let view = ValidatorSearchViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + + return view + } + + static func createView( + startStakingState state: RelaychainStartStakingStateProtocol, validatorList: [SelectedValidatorInfo], selectedValidatorList: [SelectedValidatorInfo], delegate: ValidatorSearchDelegate? ) -> ValidatorSearchViewProtocol? { - guard let interactor = createInteractor(state: state) else { + guard let interactor = createInteractor( + chainAsset: state.chainAsset, + eraValidatorService: state.eraValidatorService, + rewardCalculationService: state.relaychainRewardCalculatorService + ) else { return nil } - let wireframe = ValidatorSearchWireframe(state: state) + let wireframe = ValidatorSearchWireframe(chainAsset: state.chainAsset) let viewModelFactory = ValidatorSearchViewModelFactory() diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/ValidatorSearch/ValidatorSearchWireframe.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/ValidatorSearch/ValidatorSearchWireframe.swift index 54b8ba543b..1e567059a6 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/ValidatorSearch/ValidatorSearchWireframe.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/ValidatorSearch/ValidatorSearchWireframe.swift @@ -1,8 +1,8 @@ final class ValidatorSearchWireframe: ValidatorSearchWireframeProtocol { - let state: StakingSharedState + let chainAsset: ChainAsset - init(state: StakingSharedState) { - self.state = state + init(chainAsset: ChainAsset) { + self.chainAsset = chainAsset } func present( @@ -11,7 +11,7 @@ final class ValidatorSearchWireframe: ValidatorSearchWireframeProtocol { ) { guard let validatorInfoView = ValidatorInfoViewFactory.createView( with: validatorInfo, - state: state + chainAsset: chainAsset ) else { return } diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/ChangeValidators/YourValidatorList+CustomList.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/ChangeValidators/YourValidatorList+CustomList.swift index 469ac4c923..4b23ee0454 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/ChangeValidators/YourValidatorList+CustomList.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/ChangeValidators/YourValidatorList+CustomList.swift @@ -2,7 +2,7 @@ extension YourValidatorList { final class CustomListWireframe: CustomValidatorListWireframe { private let state: ExistingBonding - init(state: ExistingBonding, stakingState: StakingSharedState) { + init(state: ExistingBonding, stakingState: RelaychainStakingSharedStateProtocol) { self.state = state super.init(stakingState: stakingState) diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/ChangeValidators/YourValidatorList+RecommendedList.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/ChangeValidators/YourValidatorList+RecommendedList.swift index e29435fc78..9466cc8b9a 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/ChangeValidators/YourValidatorList+RecommendedList.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/ChangeValidators/YourValidatorList+RecommendedList.swift @@ -2,7 +2,7 @@ extension YourValidatorList { final class RecommendationWireframe: RecommendedValidatorListWireframe { private let state: ExistingBonding - init(state: ExistingBonding, stakingState: StakingSharedState) { + init(state: ExistingBonding, stakingState: RelaychainStakingSharedStateProtocol) { self.state = state super.init(stakingState: stakingState) } diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/ChangeValidators/YourValidatorList+SelectedList.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/ChangeValidators/YourValidatorList+SelectedList.swift index 355ee1d065..cd5904c59a 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/ChangeValidators/YourValidatorList+SelectedList.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/ChangeValidators/YourValidatorList+SelectedList.swift @@ -2,7 +2,7 @@ extension YourValidatorList { final class SelectedListWireframe: SelectedValidatorListWireframe { private let state: ExistingBonding - init(state: ExistingBonding, stakingState: StakingSharedState) { + init(state: ExistingBonding, stakingState: RelaychainStakingSharedStateProtocol) { self.state = state super.init(stakingState: stakingState) diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/ChangeValidators/YourValidatorList+SelectionStart.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/ChangeValidators/YourValidatorList+SelectionStart.swift index 3e5aef3180..8b4921a072 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/ChangeValidators/YourValidatorList+SelectionStart.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/ChangeValidators/YourValidatorList+SelectionStart.swift @@ -3,9 +3,9 @@ import Foundation extension YourValidatorList { final class SelectionStartWireframe: SelectValidatorsStartWireframe { let state: ExistingBonding - let stakingState: StakingSharedState + let stakingState: RelaychainStakingSharedStateProtocol - init(state: ExistingBonding, stakingState: StakingSharedState) { + init(state: ExistingBonding, stakingState: RelaychainStakingSharedStateProtocol) { self.state = state self.stakingState = stakingState } diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListInteractor.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListInteractor.swift index c13809816b..b9a10bbe6f 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListInteractor.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListInteractor.swift @@ -79,7 +79,7 @@ final class YourValidatorListInteractor: AccountFetching { self.activeEra = activeEra if activeEra != nil, let address = selectedAccount.toAddress() { - stashControllerProvider = subscribeStashItemProvider(for: address) + stashControllerProvider = subscribeStashItemProvider(for: address, chainId: chainAsset.chain.chainId) } else { presenter.didReceiveController(result: .success(nil)) presenter.didReceiveValidators(result: .success(nil)) diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListViewFactory.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListViewFactory.swift index 7ade147cd8..0b3d789544 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListViewFactory.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListViewFactory.swift @@ -5,7 +5,7 @@ import SoraKeystore import SubstrateSdk struct YourValidatorListViewFactory { - static func createView(for state: StakingSharedState) -> YourValidatorListViewProtocol? { + static func createView(for state: RelaychainStakingSharedStateProtocol) -> YourValidatorListViewProtocol? { let chainAsset = state.stakingOption.chainAsset guard @@ -48,17 +48,18 @@ struct YourValidatorListViewFactory { return view } - private static func createInteractor(state: StakingSharedState) -> YourValidatorListInteractor? { + private static func createInteractor(state: RelaychainStakingSharedStateProtocol) -> YourValidatorListInteractor? { let chainAsset = state.stakingOption.chainAsset guard let metaAccount = SelectedWalletSettings.shared.value, - let selectedAccount = metaAccount.fetch(for: chainAsset.chain.accountRequest()), - let eraValidatorService = state.eraValidatorService, - let rewardCalculationService = state.rewardCalculationService else { + let selectedAccount = metaAccount.fetch(for: chainAsset.chain.accountRequest()) else { return nil } + let eraValidatorService = state.eraValidatorService + let rewardCalculationService = state.rewardCalculatorService + let chainRegistry = ChainRegistryFacade.sharedRegistry let operationManager = OperationManagerFacade.sharedManager @@ -91,7 +92,7 @@ struct YourValidatorListViewFactory { return YourValidatorListInteractor( selectedAccount: selectedAccount, chainAsset: chainAsset, - stakingLocalSubscriptionFactory: state.stakingLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.localSubscriptionFactory, accountRepositoryFactory: accountRepositoryFactory, eraValidatorService: eraValidatorService, validatorOperationFactory: validatorOperationFactory, diff --git a/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListWireframe.swift b/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListWireframe.swift index 5f1e143bab..733441d723 100644 --- a/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListWireframe.swift +++ b/novawallet/Modules/Staking/SelectValidatorsFlow/YourValidatorList/YourValidatorListWireframe.swift @@ -1,9 +1,9 @@ import Foundation final class YourValidatorListWireframe: YourValidatorListWireframeProtocol { - let state: StakingSharedState + let state: RelaychainStakingSharedStateProtocol - init(state: StakingSharedState) { + init(state: RelaychainStakingSharedStateProtocol) { self.state = state } diff --git a/novawallet/Modules/Staking/Services/BlockTimeEstimationService.swift b/novawallet/Modules/Staking/Services/BlockTimeEstimationService.swift index 8d42b497be..c6be355812 100644 --- a/novawallet/Modules/Staking/Services/BlockTimeEstimationService.swift +++ b/novawallet/Modules/Staking/Services/BlockTimeEstimationService.swift @@ -193,7 +193,7 @@ final class BlockTimeEstimationService { } subscription = CallbackBatchStorageSubscription( - requests: requests, + requests: requests.map { BatchStorageSubscriptionRequest(innerRequest: $0, mappingKey: nil) }, connection: connection, runtimeService: runtimeService, repository: repository, diff --git a/novawallet/Modules/Staking/Services/EraValidatorsService/EraNominationPoolsService.swift b/novawallet/Modules/Staking/Services/EraValidatorsService/EraNominationPoolsService.swift new file mode 100644 index 0000000000..13ea4e99b1 --- /dev/null +++ b/novawallet/Modules/Staking/Services/EraValidatorsService/EraNominationPoolsService.swift @@ -0,0 +1,248 @@ +import Foundation +import SubstrateSdk +import RobinHood + +final class EraNominationPoolsService: BaseSyncService, AnyProviderAutoCleaning { + private struct PendingRequest { + let resultClosure: (NominationPools.ActivePools) -> Void + let queue: DispatchQueue? + } + + private var snapshot: NominationPools.ActivePools? + private var pendingRequests: [PendingRequest] = [] + + let chainAsset: ChainAsset + let runtimeCodingService: RuntimeCodingServiceProtocol + let operationQueue: OperationQueue + let eventCenter: EventCenterProtocol + let npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol + let operationFactory: NominationPoolsOperationFactoryProtocol + let eraValidatorService: EraValidatorServiceProtocol + + private var lastPoolId: NominationPools.PoolId? + private var activePoolCancellable: CancellableCall? + private var lastPoolIdProvider: AnyDataProvider? + + init( + chainAsset: ChainAsset, + runtimeCodingService: RuntimeCodingServiceProtocol, + operationFactory: NominationPoolsOperationFactoryProtocol, + npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, + eraValidatorService: EraValidatorServiceProtocol, + operationQueue: OperationQueue, + eventCenter: EventCenterProtocol = EventCenter.shared, + logger: LoggerProtocol = Logger.shared + ) { + self.chainAsset = chainAsset + self.runtimeCodingService = runtimeCodingService + self.operationFactory = operationFactory + self.npoolsLocalSubscriptionFactory = npoolsLocalSubscriptionFactory + self.eraValidatorService = eraValidatorService + self.operationQueue = operationQueue + self.eventCenter = eventCenter + + super.init(logger: logger) + } + + override func performSyncUp() { + updateActiveValidatorSubscription() + + lastPoolIdProvider = subscribeLastPoolId( + for: chainAsset.chain.chainId, + callbackQueue: DispatchQueue.global(qos: .userInitiated) + ) + + if lastPoolIdProvider == nil { + logger?.error("Can't subscribe last pool id") + + completeImmediate(CommonError.dataCorruption) + } + } + + override func stopSyncUp() { + clearUpdateOperation() + } + + override func deactivate() { + eventCenter.remove(observer: self) + clearSubscriptions() + } + + private func clearSubscriptions() { + clear(dataProvider: &lastPoolIdProvider) + } + + private func clearUpdateOperation() { + activePoolCancellable?.cancel() + activePoolCancellable = nil + } + + private func updateActiveValidatorSubscription() { + eventCenter.remove(observer: self) + eventCenter.add(observer: self, dispatchIn: DispatchQueue.global(qos: .userInitiated)) + } + + private func updateActivePools() { + guard let lastPoolId = lastPoolId else { + completeImmediate(CommonError.dataCorruption) + return + } + + clearUpdateOperation() + + let wrapper = operationFactory.createActivePoolsInfoWrapper( + for: eraValidatorService, + lastPoolId: lastPoolId, + runtimeService: runtimeCodingService + ) + + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.global(qos: .userInitiated).async { + self?.mutex.lock() + + defer { + self?.mutex.unlock() + } + + self?.logger?.debug("Did receive active pools response") + + guard self?.activePoolCancellable === wrapper else { + return + } + + self?.activePoolCancellable = nil + + do { + let activePools = try wrapper.targetOperation.extractNoCancellableResultData() + + self?.logger?.debug("Active pools for era \(activePools.era): \(activePools.pools.count)") + self?.handle(newActivePools: activePools) + self?.completeImmediate(nil) + self?.notifyAll() + } catch { + self?.logger?.error("Can't fetch active pools: \(error)") + self?.completeImmediate(error) + } + } + } + + activePoolCancellable = wrapper + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } + + private func handle(newActivePools: NominationPools.ActivePools) { + logger?.debug("New active pools \(newActivePools)") + + snapshot = newActivePools + + if !pendingRequests.isEmpty { + let requests = pendingRequests + pendingRequests = [] + + requests.forEach { deliver(snapshot: newActivePools, to: $0) } + + logger?.debug("Fulfilled pendings") + } + } + + private func deliver(snapshot: NominationPools.ActivePools, to request: PendingRequest) { + dispatchInQueueWhenPossible(request.queue) { + request.resultClosure(snapshot) + } + } + + private func notifyAll() { + eventCenter.notify(with: EraNominationPoolsChanged()) + } + + private func fetchInfoFactory( + runCompletionIn queue: DispatchQueue?, + executing closure: @escaping (NominationPools.ActivePools) -> Void + ) { + let request = PendingRequest(resultClosure: closure, queue: queue) + + if let snapshot = snapshot { + deliver(snapshot: snapshot, to: request) + } else { + pendingRequests.append(request) + } + } +} + +extension EraNominationPoolsService: EraNominationPoolsServiceProtocol { + func fetchInfoOperation() -> BaseOperation { + ClosureOperation { + var fetchedInfo: NominationPools.ActivePools? + + let semaphore = DispatchSemaphore(value: 0) + + self.mutex.lock() + + self.fetchInfoFactory(runCompletionIn: nil) { [weak semaphore] info in + fetchedInfo = info + semaphore?.signal() + } + + self.mutex.unlock() + + semaphore.wait() + + guard let info = fetchedInfo else { + throw CommonError.dataCorruption + } + + return info + } + } +} + +extension EraNominationPoolsService: NPoolsLocalStorageSubscriber, NPoolsLocalSubscriptionHandler { + func handleLastPoolId(result: Result, chainId _: ChainModel.Id) { + mutex.lock() + + defer { + mutex.unlock() + } + + guard isActive else { + return + } + + markSyncingImmediate() + + switch result { + case let .success(optLastPoolId): + lastPoolId = optLastPoolId + + if let lastPoolId = optLastPoolId { + logger?.debug("Last pool id: \(lastPoolId)") + updateActivePools() + } else { + logger?.warning("No pools registered yet") + completeImmediate(nil) + } + case let .failure(error): + logger?.error("Did receive error: \(error)") + completeImmediate(error) + } + } +} + +extension EraNominationPoolsService: EventVisitorProtocol { + func processEraStakersInfoChanged(event _: EraStakersInfoChanged) { + mutex.lock() + + defer { + mutex.unlock() + } + + guard isActive else { + return + } + + markSyncingImmediate() + + updateActivePools() + } +} diff --git a/novawallet/Modules/Staking/Services/EraValidatorsService/EraNominationPoolsServiceProtocol.swift b/novawallet/Modules/Staking/Services/EraValidatorsService/EraNominationPoolsServiceProtocol.swift new file mode 100644 index 0000000000..03f6eb6c9f --- /dev/null +++ b/novawallet/Modules/Staking/Services/EraValidatorsService/EraNominationPoolsServiceProtocol.swift @@ -0,0 +1,6 @@ +import Foundation +import RobinHood + +protocol EraNominationPoolsServiceProtocol: ApplicationServiceProtocol { + func fetchInfoOperation() -> BaseOperation +} diff --git a/novawallet/Modules/Staking/Services/EraValidatorsService/Model/NominationPoolModel.swift b/novawallet/Modules/Staking/Services/EraValidatorsService/Model/NominationPoolModel.swift new file mode 100644 index 0000000000..fc9e62c48f --- /dev/null +++ b/novawallet/Modules/Staking/Services/EraValidatorsService/Model/NominationPoolModel.swift @@ -0,0 +1,72 @@ +import Foundation +import BigInt + +extension NominationPools { + struct ActivePool { + let poolId: PoolId + let bondedAccountId: AccountId + let validators: Set + } + + struct ActivePools { + let era: EraIndex + let pools: [ActivePool] + } + + struct PoolApy { + let poolId: PoolId + let bondedAccountId: AccountId + let maxApy: Decimal + } + + struct PoolStats { + let poolId: PoolId + let bondedAccountId: AccountId + let membersCount: UInt32 + let maxApy: Decimal? + let metadata: Data? + let state: PoolState? + } + + struct SelectedPool { + let poolId: PoolId + let bondedAccountId: AccountId + let metadata: Data? + let maxApy: Decimal? + + var name: String? { + metadata.flatMap { String(data: $0, encoding: .utf8) } + } + + init( + poolId: PoolId, + bondedAccountId: AccountId, + metadata: Data?, + maxApy: Decimal? + ) { + self.poolId = poolId + self.bondedAccountId = bondedAccountId + self.metadata = metadata + self.maxApy = maxApy + } + + init(poolStats: PoolStats) { + poolId = poolStats.poolId + bondedAccountId = poolStats.bondedAccountId + metadata = poolStats.metadata + maxApy = poolStats.maxApy + } + + func bondedAddress(for chainFormat: ChainFormat) -> AccountAddress? { + try? bondedAccountId.toAddress(using: chainFormat) + } + + func title(for chainFormat: ChainFormat) -> String? { + if let poolName = name, !poolName.isEmpty { + return poolName + } else { + return bondedAddress(for: chainFormat) + } + } + } +} diff --git a/novawallet/Modules/Staking/Services/ParachainStakingServiceFactory.swift b/novawallet/Modules/Staking/Services/ParachainStakingServiceFactory.swift index df911ec759..af811767f0 100644 --- a/novawallet/Modules/Staking/Services/ParachainStakingServiceFactory.swift +++ b/novawallet/Modules/Staking/Services/ParachainStakingServiceFactory.swift @@ -63,6 +63,7 @@ final class ParachainStakingServiceFactory: ParachainStakingServiceFactoryProtoc ) } + // swiftlint:disable:next function_body_length func createRewardCalculatorService( for chainId: ChainModel.Id, stakingType: StakingType, diff --git a/novawallet/Modules/Staking/Services/RewardCalculatorService/NominationPools/NPoolsRewardEngineFactory.swift b/novawallet/Modules/Staking/Services/RewardCalculatorService/NominationPools/NPoolsRewardEngineFactory.swift new file mode 100644 index 0000000000..e4f7051996 --- /dev/null +++ b/novawallet/Modules/Staking/Services/RewardCalculatorService/NominationPools/NPoolsRewardEngineFactory.swift @@ -0,0 +1,70 @@ +import Foundation +import RobinHood +import SubstrateSdk + +protocol NPoolsRewardEngineFactoryProtocol { + func createEngineWrapper( + for eraPoolsService: EraNominationPoolsServiceProtocol, + validatorRewardService: RewardCalculatorServiceProtocol, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper +} + +final class NPoolsRewardEngineFactory { + let operationFactory: NominationPoolsOperationFactoryProtocol + + init(operationFactory: NominationPoolsOperationFactoryProtocol) { + self.operationFactory = operationFactory + } +} + +extension NPoolsRewardEngineFactory: NPoolsRewardEngineFactoryProtocol { + func createEngineWrapper( + for eraPoolsService: EraNominationPoolsServiceProtocol, + validatorRewardService: RewardCalculatorServiceProtocol, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol + ) -> CompoundOperationWrapper { + let activePoolsOperation = eraPoolsService.fetchInfoOperation() + let rewardEngineOperation = validatorRewardService.fetchCalculatorOperation() + + let bondingDetailsWrapper = operationFactory.createBondedPoolsWrapper( + for: { + let poolIds = try activePoolsOperation.extractNoCancellableResultData().pools.map(\.poolId) + return Set(poolIds) + }, + connection: connection, + runtimeService: runtimeService + ) + + bondingDetailsWrapper.addDependency(operations: [activePoolsOperation]) + + let mergeOperation = ClosureOperation { + let validatorRewardCalculator = try rewardEngineOperation.extractNoCancellableResultData() + let activePools = try activePoolsOperation.extractNoCancellableResultData().pools.reduce( + into: [NominationPools.PoolId: NominationPools.ActivePool]() + ) { + $0[$1.poolId] = $1 + } + + let bondingDetails = try bondingDetailsWrapper.targetOperation.extractNoCancellableResultData() + + let engine = NominationPoolsRewardEngine( + innerRewardCalculator: validatorRewardCalculator, + activePools: activePools, + bondingDetails: bondingDetails + ) + + engine.setup() + + return engine + } + + let dependencies = [activePoolsOperation, rewardEngineOperation] + bondingDetailsWrapper.allOperations + + dependencies.forEach { mergeOperation.addDependency($0) } + + return CompoundOperationWrapper(targetOperation: mergeOperation, dependencies: dependencies) + } +} diff --git a/novawallet/Modules/Staking/Services/RewardCalculatorService/NominationPools/NominationPoolsRewardEngine.swift b/novawallet/Modules/Staking/Services/RewardCalculatorService/NominationPools/NominationPoolsRewardEngine.swift new file mode 100644 index 0000000000..68ea41b3c5 --- /dev/null +++ b/novawallet/Modules/Staking/Services/RewardCalculatorService/NominationPools/NominationPoolsRewardEngine.swift @@ -0,0 +1,92 @@ +import Foundation + +protocol NominationPoolsRewardEngineProtocol { + func calculateMaxReturn( + poolId: NominationPools.PoolId, + isCompound: Bool, + period: CalculationPeriod + ) throws -> NominationPools.PoolApy + + func calculateMaxReturn(isCompound: Bool, period: CalculationPeriod) throws -> NominationPools.PoolApy +} + +enum NominationPoolsRewardEngineError: Error { + case noPoolFound(NominationPools.PoolId) +} + +final class NominationPoolsRewardEngine { + let innerRewardCalculator: RewardCalculatorEngineProtocol + let activePools: [NominationPools.PoolId: NominationPools.ActivePool] + let bondingDetails: [NominationPools.PoolId: NominationPools.BondedPool] + + private var maxPoolId: NominationPools.PoolId? + + init( + innerRewardCalculator: RewardCalculatorEngineProtocol, + activePools: [NominationPools.PoolId: NominationPools.ActivePool], + bondingDetails: [NominationPools.PoolId: NominationPools.BondedPool] + ) { + self.innerRewardCalculator = innerRewardCalculator + self.activePools = activePools + self.bondingDetails = bondingDetails + } + + func setup() { + maxPoolId = activePools.keys.max { poolId1, poolId2 in + let maxApy1 = (try? calculateMaxReturn(poolId: poolId1, isCompound: false, period: .year))?.maxApy ?? 0 + let maxApy2 = (try? calculateMaxReturn(poolId: poolId2, isCompound: false, period: .year))?.maxApy ?? 0 + + return maxApy1 <= maxApy2 + } + } +} + +extension NominationPoolsRewardEngine: NominationPoolsRewardEngineProtocol { + func calculateMaxReturn( + poolId: NominationPools.PoolId, + isCompound: Bool, + period: CalculationPeriod + ) throws -> NominationPools.PoolApy { + guard + let activePool = activePools[poolId], + let bondedPool = bondingDetails[poolId] else { + throw NominationPoolsRewardEngineError.noPoolFound(poolId) + } + + let optMaxReturn = try activePool.validators + .map { validator in + try innerRewardCalculator.calculateValidatorReturn( + validatorAccountId: validator, + isCompound: isCompound, + period: period + ) + } + .max() + + guard let maxReturn = optMaxReturn else { + throw CommonError.dataCorruption + } + + let commission = bondedPool.commission?.current.flatMap { Decimal.fromSubstratePerbill(value: $0.percent) } ?? 0 + + let maxApy = maxReturn * (1 - commission) + + return .init( + poolId: activePool.poolId, + bondedAccountId: activePool.bondedAccountId, + maxApy: maxApy + ) + } + + func calculateMaxReturn(isCompound: Bool, period: CalculationPeriod) throws -> NominationPools.PoolApy { + if maxPoolId == nil { + setup() + } + + guard let maxPoolId = maxPoolId else { + throw CommonError.dataCorruption + } + + return try calculateMaxReturn(poolId: maxPoolId, isCompound: isCompound, period: period) + } +} diff --git a/novawallet/Modules/Staking/Services/RewardCalculatorService/RelayChain/RewardCalculatorParamsServiceFactory.swift b/novawallet/Modules/Staking/Services/RewardCalculatorService/RelayChain/RewardCalculatorParamsServiceFactory.swift index 329e197988..183dcd38e4 100644 --- a/novawallet/Modules/Staking/Services/RewardCalculatorService/RelayChain/RewardCalculatorParamsServiceFactory.swift +++ b/novawallet/Modules/Staking/Services/RewardCalculatorService/RelayChain/RewardCalculatorParamsServiceFactory.swift @@ -27,7 +27,7 @@ final class RewardCalculatorParamsServiceFactory { extension RewardCalculatorParamsServiceFactory: RewardCalculatorParamsServiceFactoryProtocol { func createRewardCalculatorParamsService() -> RewardCalculatorParamsServiceProtocol { switch stakingType { - case .relaychain: + case .relaychain, .nominationPools: return InflationRewardCalculatorParamsService( connection: connection, runtimeService: runtimeService, diff --git a/novawallet/Modules/Staking/Services/StakingServiceFactory.swift b/novawallet/Modules/Staking/Services/StakingServiceFactory.swift index 4baebda4c0..f2897ce1e4 100644 --- a/novawallet/Modules/Staking/Services/StakingServiceFactory.swift +++ b/novawallet/Modules/Staking/Services/StakingServiceFactory.swift @@ -11,10 +11,7 @@ protocol StakingServiceFactoryProtocol { validatorService: EraValidatorServiceProtocol ) throws -> RewardCalculatorServiceProtocol - func createBlockTimeService( - for chainId: ChainModel.Id, - consensus: ConsensusType - ) throws -> BlockTimeEstimationServiceProtocol? + func createTimeModel(for chainId: ChainModel.Id, consensus: ConsensusType) throws -> StakingTimeModel } final class StakingServiceFactory: StakingServiceFactoryProtocol { @@ -107,35 +104,40 @@ final class StakingServiceFactory: StakingServiceFactoryProtocol { ) } - func createBlockTimeService( - for chainId: ChainModel.Id, - consensus: ConsensusType - ) throws -> BlockTimeEstimationServiceProtocol? { + func createTimeModel(for chainId: ChainModel.Id, consensus: ConsensusType) throws -> StakingTimeModel { switch consensus { case .babe: - return nil - case .auraGeneral, .auraAzero: - guard let runtimeService = chainRegisty.getRuntimeProvider(for: chainId) else { - throw ChainRegistryError.runtimeMetadaUnavailable - } - - guard let connection = chainRegisty.getConnection(for: chainId) else { - throw ChainRegistryError.connectionUnavailable - } - - let repositoryFactory = SubstrateRepositoryFactory(storageFacade: storageFacade) - - let repository = repositoryFactory.createChainStorageItemRepository() - - return BlockTimeEstimationService( - chainId: chainId, - connection: connection, - runtimeService: runtimeService, - repository: repository, - eventCenter: eventCenter, - operationQueue: operationQueue, - logger: logger - ) + return .babe + case .auraGeneral: + let blockTimeService = try createBlockTimeService(for: chainId) + return .auraGeneral(blockTimeService) + case .auraAzero: + let blockTimeService = try createBlockTimeService(for: chainId) + return .azero(blockTimeService) } } + + private func createBlockTimeService(for chainId: ChainModel.Id) throws -> BlockTimeEstimationServiceProtocol { + guard let runtimeService = chainRegisty.getRuntimeProvider(for: chainId) else { + throw ChainRegistryError.runtimeMetadaUnavailable + } + + guard let connection = chainRegisty.getConnection(for: chainId) else { + throw ChainRegistryError.connectionUnavailable + } + + let repositoryFactory = SubstrateRepositoryFactory(storageFacade: storageFacade) + + let repository = repositoryFactory.createChainStorageItemRepository() + + return BlockTimeEstimationService( + chainId: chainId, + connection: connection, + runtimeService: runtimeService, + repository: repository, + eventCenter: eventCenter, + operationQueue: operationQueue, + logger: logger + ) + } } diff --git a/novawallet/Modules/Staking/StakingAmount/StakingAmountInteractor.swift b/novawallet/Modules/Staking/StakingAmount/StakingAmountInteractor.swift deleted file mode 100644 index 5f889e77e4..0000000000 --- a/novawallet/Modules/Staking/StakingAmount/StakingAmountInteractor.swift +++ /dev/null @@ -1,293 +0,0 @@ -import UIKit -import RobinHood -import SoraKeystore -import IrohaCrypto -import BigInt -import SubstrateSdk - -final class StakingAmountInteractor { - weak var presenter: StakingAmountInteractorOutputProtocol! - - let selectedAccount: ChainAccountResponse - let chainAsset: ChainAsset - let repository: AnyDataProviderRepository - let stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol - let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol - let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol - let extrinsicService: ExtrinsicServiceProtocol - let runtimeService: RuntimeCodingServiceProtocol - let rewardService: RewardCalculatorServiceProtocol - let networkInfoOperationFactory: NetworkStakingInfoOperationFactoryProtocol - let eraValidatorService: EraValidatorServiceProtocol - let operationManager: OperationManagerProtocol - - private var balanceProvider: StreamableProvider? - private var priceProvider: StreamableProvider? - private var minBondProvider: AnyDataProvider? - private var counterForNominatorsProvider: AnyDataProvider? - private var maxNominatorsCountProvider: AnyDataProvider? - private var bagListSizeProvider: AnyDataProvider? - - init( - selectedAccount: ChainAccountResponse, - chainAsset: ChainAsset, - stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol, - walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, - priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, - repository: AnyDataProviderRepository, - extrinsicService: ExtrinsicServiceProtocol, - runtimeService: RuntimeCodingServiceProtocol, - rewardService: RewardCalculatorServiceProtocol, - networkInfoOperationFactory: NetworkStakingInfoOperationFactoryProtocol, - eraValidatorService: EraValidatorServiceProtocol, - operationManager: OperationManagerProtocol, - currencyManager: CurrencyManagerProtocol - ) { - self.selectedAccount = selectedAccount - self.chainAsset = chainAsset - self.stakingLocalSubscriptionFactory = stakingLocalSubscriptionFactory - self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory - self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory - self.repository = repository - self.extrinsicService = extrinsicService - self.rewardService = rewardService - self.runtimeService = runtimeService - self.networkInfoOperationFactory = networkInfoOperationFactory - self.eraValidatorService = eraValidatorService - self.operationManager = operationManager - self.currencyManager = currencyManager - } - - private func provideNetworkInfo() { - let wrapper = networkInfoOperationFactory.networkStakingOperation( - for: eraValidatorService, - runtimeService: runtimeService - ) - - wrapper.targetOperation.completionBlock = { [weak self] in - do { - let info = try wrapper.targetOperation.extractNoCancellableResultData() - self?.presenter.didReceive(networkInfo: info) - } catch { - self?.presenter.didReceive(error: error) - } - } - - operationManager.enqueue(operations: wrapper.allOperations, in: .transient) - } - - private func provideRewardCalculator() { - let operation = rewardService.fetchCalculatorOperation() - - operation.completionBlock = { [weak self] in - DispatchQueue.main.async { - do { - let engine = try operation.extractNoCancellableResultData() - self?.presenter.didReceive(calculator: engine) - } catch { - self?.presenter.didReceive(calculatorError: error) - } - } - } - - operationManager.enqueue( - operations: [operation], - in: .transient - ) - } - - private func estimateFee( - for address: String, - amount: BigUInt, - rewardDestination: RewardDestination, - coderFactory: RuntimeCoderFactoryProtocol - ) { - guard let accountAddress = rewardDestination.accountAddress else { - return - } - - let closure: ExtrinsicBuilderClosure = { builder in - let controller = try address.toAccountId() - let payee = try Staking.RewardDestinationArg(rewardDestination: accountAddress) - - let bondClosure = try Staking.Bond.appendCall( - for: .accoundId(controller), - value: amount, - payee: payee, - codingFactory: coderFactory - ) - - let callFactory = SubstrateCallFactory() - - let targets = Array( - repeating: SelectedValidatorInfo(address: address), - count: SubstrateConstants.maxNominations - ) - let nominateCall = try callFactory.nominate(targets: targets) - - return try bondClosure(builder).adding(call: nominateCall) - } - - extrinsicService.estimateFee(closure, runningIn: .main) { [weak self] result in - switch result { - case let .success(info): - self?.presenter.didReceive( - paymentInfo: info, - for: amount, - rewardDestination: rewardDestination - ) - case let .failure(error): - self?.presenter.didReceive(error: error) - } - } - } -} - -extension StakingAmountInteractor: StakingAmountInteractorInputProtocol, RuntimeConstantFetching, - AccountFetching { - func setup() { - if let priceId = chainAsset.asset.priceId { - priceProvider = subscribeToPrice(for: priceId, currency: selectedCurrency) - } else { - presenter.didReceive(price: nil) - } - - balanceProvider = subscribeToAssetBalanceProvider( - for: selectedAccount.accountId, - chainId: chainAsset.chain.chainId, - assetId: chainAsset.asset.assetId - ) - - minBondProvider = subscribeToMinNominatorBond(for: chainAsset.chain.chainId) - counterForNominatorsProvider = subscribeToCounterForNominators(for: chainAsset.chain.chainId) - maxNominatorsCountProvider = subscribeMaxNominatorsCount(for: chainAsset.chain.chainId) - bagListSizeProvider = subscribeBagsListSize(for: chainAsset.chain.chainId) - - provideRewardCalculator() - provideNetworkInfo() - - fetchConstant( - for: .existentialDeposit, - runtimeCodingService: runtimeService, - operationManager: operationManager - ) { [weak self] (result: Result) in - switch result { - case let .success(amount): - self?.presenter.didReceive(minimalBalance: amount) - case let .failure(error): - self?.presenter.didReceive(error: error) - } - } - } - - func fetchAccounts() { - fetchAllMetaAccountResponses( - for: chainAsset.chain.accountRequest(), - repository: repository, - operationManager: operationManager - ) { [weak self] result in - switch result { - case let .success(responses): - self?.presenter.didReceive(accounts: responses) - case let .failure(error): - self?.presenter.didReceive(error: error) - } - } - } - - func estimateFee( - for address: String, - amount: BigUInt, - rewardDestination: RewardDestination - ) { - runtimeService.fetchCoderFactory( - runningIn: operationManager, - completion: { [weak self] coderFactory in - self?.estimateFee( - for: address, - amount: amount, - rewardDestination: rewardDestination, - coderFactory: coderFactory - ) - }, errorClosure: { [weak self] error in - self?.presenter.didReceive(error: error) - } - ) - } -} - -extension StakingAmountInteractor: StakingLocalStorageSubscriber, StakingLocalSubscriptionHandler { - func handleMinNominatorBond(result: Result, chainId _: ChainModel.Id) { - switch result { - case let .success(value): - presenter.didReceive(minBondAmount: value) - case let .failure(error): - presenter.didReceive(error: error) - } - } - - func handleCounterForNominators(result: Result, chainId _: ChainModel.Id) { - switch result { - case let .success(value): - presenter.didReceive(counterForNominators: value) - case let .failure(error): - presenter.didReceive(error: error) - } - } - - func handleMaxNominatorsCount(result: Result, chainId _: ChainModel.Id) { - switch result { - case let .success(value): - presenter.didReceive(maxNominatorsCount: value) - case let .failure(error): - presenter.didReceive(error: error) - } - } - - func handleBagListSize(result: Result, chainId _: ChainModel.Id) { - switch result { - case let .success(value): - presenter?.didReceive(bagListSize: value) - case let .failure(error): - presenter?.didReceive(error: error) - } - } -} - -extension StakingAmountInteractor: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { - func handleAssetBalance( - result: Result, - accountId _: AccountId, - chainId _: ChainModel.Id, - assetId _: AssetModel.Id - ) { - switch result { - case let .success(assetBalance): - presenter.didReceive(balance: assetBalance) - case let .failure(error): - presenter.didReceive(error: error) - } - } -} - -extension StakingAmountInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { - func handlePrice(result: Result, priceId _: AssetModel.PriceId) { - switch result { - case let .success(priceData): - presenter.didReceive(price: priceData) - case let .failure(error): - presenter.didReceive(error: error) - } - } -} - -extension StakingAmountInteractor: SelectedCurrencyDepending { - func applyCurrency() { - guard presenter != nil, - let priceId = chainAsset.asset.priceId else { - return - } - - priceProvider = subscribeToPrice(for: priceId, currency: selectedCurrency) - } -} diff --git a/novawallet/Modules/Staking/StakingAmount/StakingAmountLayout.swift b/novawallet/Modules/Staking/StakingAmount/StakingAmountLayout.swift deleted file mode 100644 index f146622b06..0000000000 --- a/novawallet/Modules/Staking/StakingAmount/StakingAmountLayout.swift +++ /dev/null @@ -1,102 +0,0 @@ -import UIKit - -final class StakingAmountLayout: UIView { - let containerView: ScrollableContainerView = { - let view = ScrollableContainerView(axis: .vertical, respectsSafeArea: true) - view.stackView.layoutMargins = UIEdgeInsets(top: 0.0, left: 16.0, bottom: 0.0, right: 16.0) - view.stackView.isLayoutMarginsRelativeArrangement = true - view.stackView.alignment = .fill - return view - }() - - let amountView = TitleHorizontalMultiValueView() - - let amountInputView = NewAmountInputView() - - let restakeOptionView = RewardSelectionView() - let payoutOptionView = RewardSelectionView() - - let accountView = WalletAccountSelectionView() - - let aboutLinkView = LinkCellView() - - let networkFeeView = UIFactory.default.createNetworkFeeView() - - let actionButton: TriangularedButton = { - let button = TriangularedButton() - button.applyDefaultStyle() - return button - }() - - override init(frame: CGRect) { - super.init(frame: frame) - - backgroundColor = R.color.colorSecondaryScreenBackground() - - setupLayout() - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setAccountShown(_ isShown: Bool) { - accountView.isHidden = !isShown - - let verticalSpacing = isShown ? 16.0 : 0.0 - - containerView.stackView.setCustomSpacing(verticalSpacing, after: payoutOptionView) - } - - private func setupLayout() { - addSubview(actionButton) - actionButton.snp.makeConstraints { make in - make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) - make.bottom.equalTo(safeAreaLayoutGuide).inset(UIConstants.actionBottomInset) - make.height.equalTo(UIConstants.actionHeight) - } - - addSubview(containerView) - containerView.snp.makeConstraints { make in - make.top.leading.trailing.equalToSuperview() - make.bottom.equalTo(actionButton.snp.top).offset(-8.0) - } - - containerView.stackView.addArrangedSubview(amountView) - amountView.snp.makeConstraints { make in - make.height.equalTo(34.0) - } - - containerView.stackView.addArrangedSubview(amountInputView) - amountInputView.snp.makeConstraints { make in - make.height.equalTo(64) - } - - containerView.stackView.setCustomSpacing(12.0, after: amountInputView) - - containerView.stackView.addArrangedSubview(aboutLinkView) - containerView.stackView.setCustomSpacing(12.0, after: aboutLinkView) - - containerView.stackView.addArrangedSubview(restakeOptionView) - restakeOptionView.snp.makeConstraints { make in - make.width.equalTo(self).offset(-2.0 * UIConstants.horizontalInset) - make.height.equalTo(56.0) - } - - containerView.stackView.setCustomSpacing(12.0, after: restakeOptionView) - - containerView.stackView.addArrangedSubview(payoutOptionView) - payoutOptionView.snp.makeConstraints { make in - make.width.equalTo(self).offset(-2.0 * UIConstants.horizontalInset) - make.height.equalTo(56.0) - } - - containerView.stackView.addArrangedSubview(accountView) - - containerView.stackView.addArrangedSubview(networkFeeView) - networkFeeView.snp.makeConstraints { make in - make.height.equalTo(64.0) - } - } -} diff --git a/novawallet/Modules/Staking/StakingAmount/StakingAmountPresenter.swift b/novawallet/Modules/Staking/StakingAmount/StakingAmountPresenter.swift deleted file mode 100644 index aa7402a691..0000000000 --- a/novawallet/Modules/Staking/StakingAmount/StakingAmountPresenter.swift +++ /dev/null @@ -1,423 +0,0 @@ -import Foundation - -import BigInt - -final class StakingAmountPresenter { - weak var view: StakingAmountViewProtocol? - let wireframe: StakingAmountWireframeProtocol - let interactor: StakingAmountInteractorInputProtocol - - let balanceViewModelFactory: BalanceViewModelFactoryProtocol - let rewardDestViewModelFactory: RewardDestinationViewModelFactoryProtocol - let selectedAccount: MetaChainAccountResponse - let assetInfo: AssetBalanceDisplayInfo - let logger: LoggerProtocol - let applicationConfig: ApplicationConfigProtocol - let dataValidatingFactory: StakingDataValidatingFactoryProtocol - - private var calculator: RewardCalculatorEngineProtocol? - private var priceData: PriceData? - private var freeBalance: Decimal? - private var transferableBalance: Decimal? - private var fee: Decimal? - private var loadingFee: Bool = false - private var amount: Decimal? - private var rewardDestination: RewardDestination = .restake - private var payoutAccount: MetaChainAccountResponse - private var loadingPayouts: Bool = false - private var minimalBalance: Decimal? - private var minBondAmount: Decimal? - private var counterForNominators: UInt32? - private var maxNominatorsCount: UInt32? - private var bagListSize: UInt32? - private var networkInfo: NetworkStakingInfo? - - init( - wireframe: StakingAmountWireframeProtocol, - interactor: StakingAmountInteractorInputProtocol, - amount: Decimal?, - selectedAccount: MetaChainAccountResponse, - assetInfo: AssetBalanceDisplayInfo, - rewardDestViewModelFactory: RewardDestinationViewModelFactoryProtocol, - balanceViewModelFactory: BalanceViewModelFactoryProtocol, - dataValidatingFactory: StakingDataValidatingFactoryProtocol, - applicationConfig: ApplicationConfigProtocol, - logger: LoggerProtocol - ) { - self.wireframe = wireframe - self.interactor = interactor - self.amount = amount - self.selectedAccount = selectedAccount - payoutAccount = selectedAccount - self.assetInfo = assetInfo - self.rewardDestViewModelFactory = rewardDestViewModelFactory - self.balanceViewModelFactory = balanceViewModelFactory - self.dataValidatingFactory = dataValidatingFactory - self.applicationConfig = applicationConfig - self.logger = logger - } - - private func provideRewardDestination() { - do { - let reward: CalculatedReward? - - if let calculator = calculator { - let restake = calculator.calculateMaxReturn( - isCompound: true, - period: .year - ) - - let payout = calculator.calculateMaxReturn( - isCompound: false, - period: .year - ) - - let curAmount = amount ?? 0.0 - reward = CalculatedReward( - restakeReturn: restake * curAmount, - restakeReturnPercentage: restake, - payoutReturn: payout * curAmount, - payoutReturnPercentage: payout - ) - } else { - reward = nil - } - - switch rewardDestination { - case .restake: - let viewModel = rewardDestViewModelFactory.createRestake(from: reward, priceData: priceData) - view?.didReceiveRewardDestination(viewModel: viewModel) - case .payout: - let viewModel = try rewardDestViewModelFactory.createPayout( - from: reward, - priceData: priceData, - account: payoutAccount - ) - view?.didReceiveRewardDestination(viewModel: viewModel) - } - } catch { - logger.error("Can't create reward destination") - } - } - - private func provideAsset() { - let viewModel = balanceViewModelFactory.createAssetBalanceViewModel( - amount ?? 0.0, - balance: freeBalance, - priceData: priceData - ) - view?.didReceiveAsset(viewModel: viewModel) - } - - private func provideFee() { - if let fee = fee { - let feeViewModel = balanceViewModelFactory.balanceFromPrice(fee, priceData: priceData) - view?.didReceiveFee(viewModel: feeViewModel) - } else { - view?.didReceiveFee(viewModel: nil) - } - } - - private func provideAmountInputViewModel() { - let viewModel = balanceViewModelFactory.createBalanceInputViewModel(amount) - view?.didReceiveInput(viewModel: viewModel) - } - - private func scheduleFeeEstimation() { - if !loadingFee, fee == nil { - estimateFee() - } - } - - private func estimateFee() { - if - let amount = StakingConstants.maxAmount.toSubstrateAmount(precision: assetInfo.assetPrecision), - let address = payoutAccount.chainAccount.toAddress() { - loadingFee = true - - let rewardDestination = RewardDestination.payout(account: payoutAccount.chainAccount) - - interactor.estimateFee(for: address, amount: amount, rewardDestination: rewardDestination) - } - } -} - -extension StakingAmountPresenter: StakingAmountPresenterProtocol { - func setup() { - provideAmountInputViewModel() - provideRewardDestination() - - interactor.setup() - - estimateFee() - } - - func selectRestakeDestination() { - rewardDestination = .restake - provideRewardDestination() - - scheduleFeeEstimation() - } - - func selectPayoutDestination() { - rewardDestination = .payout(account: payoutAccount.chainAccount) - provideRewardDestination() - - scheduleFeeEstimation() - } - - func selectAmountPercentage(_ percentage: Float) { - if let balance = freeBalance, let fee = fee { - let newAmount = max(balance - fee, 0.0) * Decimal(Double(percentage)) - - if newAmount > 0 { - amount = newAmount - - provideAmountInputViewModel() - provideAsset() - provideRewardDestination() - } - } - } - - func selectPayoutAccount() { - guard !loadingPayouts else { - return - } - - loadingPayouts = true - - interactor.fetchAccounts() - } - - func selectLearnMore() { - if let view = view { - wireframe.showWeb( - url: applicationConfig.learnPayoutURL, - from: view, - style: .automatic - ) - } - } - - func updateAmount(_ newValue: Decimal) { - amount = newValue - - provideAsset() - provideRewardDestination() - scheduleFeeEstimation() - } - - func proceed() { - let locale = view?.localizationManager?.selectedLocale ?? Locale.current - - DataValidationRunner(validators: [ - dataValidatingFactory.has(fee: fee, locale: locale) { [weak self] in - self?.scheduleFeeEstimation() - }, - dataValidatingFactory.canSpendAmount( - balance: freeBalance, - spendingAmount: amount, - locale: locale - ), - dataValidatingFactory.canPayFee( - balance: transferableBalance, - fee: fee, - asset: assetInfo, - locale: locale - ), - dataValidatingFactory.canPayFeeSpendingAmount( - balance: freeBalance, - fee: fee, - spendingAmount: amount, - asset: assetInfo, - locale: locale - ), - dataValidatingFactory.canNominate( - amount: amount, - minimalBalance: minimalBalance, - minNominatorBond: minBondAmount, - locale: locale - ), - dataValidatingFactory.maxNominatorsCountNotApplied( - counterForNominators: counterForNominators, - maxNominatorsCount: maxNominatorsCount, - hasExistingNomination: false, - locale: locale - ), - dataValidatingFactory.minStakeIsNotViolated( - amount: amount, - params: .init( - networkInfo: networkInfo, - minNominatorBond: minBondAmount?.toSubstrateAmount( - precision: assetInfo.assetPrecision - ), - votersCount: bagListSize - ), - assetInfo: assetInfo, - locale: locale - ) - ]).runValidation { [weak self] in - guard - let amount = self?.amount, - let rewardDestination = self?.rewardDestination else { - return - } - - let stakingState = InitiatedBonding(amount: amount, rewardDestination: rewardDestination) - - self?.wireframe.proceed(from: self?.view, state: stakingState) - } - } - - func close() { - wireframe.close(view: view) - } -} - -extension StakingAmountPresenter: SchedulerDelegate { - func didTrigger(scheduler _: SchedulerProtocol) { - estimateFee() - } -} - -extension StakingAmountPresenter: StakingAmountInteractorOutputProtocol { - func didReceive(accounts: [MetaChainAccountResponse]) { - loadingPayouts = false - - let operatableAccounts = accounts.filter { $0.chainAccount.type.canPerformOperations } - - if !operatableAccounts.isEmpty { - let context = PrimitiveContextWrapper(value: operatableAccounts) - - wireframe.presentAccountSelection( - operatableAccounts, - selectedAccount: payoutAccount, - delegate: self, - from: view, - context: context - ) - } - } - - func didReceive(price: PriceData?) { - priceData = price - provideAsset() - provideFee() - provideRewardDestination() - } - - func didReceive(balance: AssetBalance?) { - if let assetBalance = balance { - freeBalance = Decimal.fromSubstrateAmount( - assetBalance.freeInPlank, - precision: assetInfo.assetPrecision - ) - - transferableBalance = Decimal.fromSubstrateAmount( - assetBalance.transferable, - precision: assetInfo.assetPrecision - ) - } else { - freeBalance = 0.0 - transferableBalance = 0.0 - } - - provideAsset() - } - - func didReceive( - paymentInfo: RuntimeDispatchInfo, - for _: BigUInt, - rewardDestination _: RewardDestination - ) { - loadingFee = false - - if let feeValue = BigUInt(paymentInfo.fee), - let fee = Decimal.fromSubstrateAmount(feeValue, precision: assetInfo.assetPrecision) { - self.fee = fee - } else { - fee = nil - } - - provideFee() - } - - func didReceive(error: Error) { - loadingPayouts = false - loadingFee = false - - let locale = view?.localizationManager?.selectedLocale - - if !wireframe.present(error: error, from: view, locale: locale) { - logger.error("Did receive error: \(error)") - } - } - - func didReceive(calculator: RewardCalculatorEngineProtocol) { - self.calculator = calculator - provideRewardDestination() - } - - func didReceive(calculatorError: Error) { - let locale = view?.localizationManager?.selectedLocale - if !wireframe.present(error: calculatorError, from: view, locale: locale) { - logger.error("Did receive error: \(calculatorError)") - } - } - - func didReceive(minimalBalance: BigUInt) { - if let amount = Decimal.fromSubstrateAmount(minimalBalance, precision: assetInfo.assetPrecision) { - logger.debug("Did receive minimun bonding amount: \(amount)") - self.minimalBalance = amount - } - } - - func didReceive(minBondAmount: BigUInt?) { - self.minBondAmount = minBondAmount.map { Decimal.fromSubstrateAmount( - $0, - precision: assetInfo.assetPrecision - ) } ?? nil - } - - func didReceive(counterForNominators: UInt32?) { - self.counterForNominators = counterForNominators - } - - func didReceive(maxNominatorsCount: UInt32?) { - self.maxNominatorsCount = maxNominatorsCount - } - - func didReceive(bagListSize: UInt32?) { - self.bagListSize = bagListSize - } - - func didReceive(networkInfo: NetworkStakingInfo) { - self.networkInfo = networkInfo - } -} - -extension StakingAmountPresenter: ModalPickerViewControllerDelegate { - func modalPickerDidCancel(context _: AnyObject?) { - view?.didCompletionAccountSelection() - } - - func modalPickerDidSelectModelAtIndex(_ index: Int, context: AnyObject?) { - view?.didCompletionAccountSelection() - - guard - let accounts = - (context as? PrimitiveContextWrapper<[MetaChainAccountResponse]>)?.value - else { - return - } - - payoutAccount = accounts[index] - - if case .payout = rewardDestination { - rewardDestination = .payout(account: payoutAccount.chainAccount) - } - - provideRewardDestination() - } -} diff --git a/novawallet/Modules/Staking/StakingAmount/StakingAmountProtocols.swift b/novawallet/Modules/Staking/StakingAmount/StakingAmountProtocols.swift deleted file mode 100644 index 7a1e9778d9..0000000000 --- a/novawallet/Modules/Staking/StakingAmount/StakingAmountProtocols.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation -import SoraFoundation -import BigInt - -protocol StakingAmountViewProtocol: ControllerBackedProtocol, Localizable { - func didReceiveRewardDestination(viewModel: LocalizableResource) - func didReceiveAsset(viewModel: LocalizableResource) - func didReceiveFee(viewModel: LocalizableResource?) - func didReceiveInput(viewModel: LocalizableResource) - func didCompletionAccountSelection() -} - -protocol StakingAmountPresenterProtocol: AnyObject { - func setup() - func selectRestakeDestination() - func selectPayoutDestination() - func selectAmountPercentage(_ percentage: Float) - func selectPayoutAccount() - func updateAmount(_ newValue: Decimal) - func selectLearnMore() - func proceed() - func close() -} - -protocol StakingAmountInteractorInputProtocol: AnyObject { - func setup() - func estimateFee( - for address: String, - amount: BigUInt, - rewardDestination: RewardDestination - ) - func fetchAccounts() -} - -protocol StakingAmountInteractorOutputProtocol: AnyObject { - func didReceive(accounts: [MetaChainAccountResponse]) - func didReceive(price: PriceData?) - func didReceive(balance: AssetBalance?) - func didReceive( - paymentInfo: RuntimeDispatchInfo, - for amount: BigUInt, - rewardDestination: RewardDestination - ) - func didReceive(error: Error) - func didReceive(calculator: RewardCalculatorEngineProtocol) - func didReceive(calculatorError: Error) - func didReceive(minimalBalance: BigUInt) - func didReceive(minBondAmount: BigUInt?) - func didReceive(counterForNominators: UInt32?) - func didReceive(maxNominatorsCount: UInt32?) - func didReceive(bagListSize: UInt32?) - func didReceive(networkInfo: NetworkStakingInfo) -} - -protocol StakingAmountWireframeProtocol: AlertPresentable, ErrorPresentable, WebPresentable, - StakingErrorPresentable { - func presentAccountSelection( - _ accounts: [MetaChainAccountResponse], - selectedAccount: MetaChainAccountResponse, - delegate: ModalPickerViewControllerDelegate, - from view: StakingAmountViewProtocol?, - context: AnyObject? - ) - - func proceed(from view: StakingAmountViewProtocol?, state: InitiatedBonding) - - func close(view: StakingAmountViewProtocol?) -} diff --git a/novawallet/Modules/Staking/StakingAmount/StakingAmountViewController.swift b/novawallet/Modules/Staking/StakingAmount/StakingAmountViewController.swift deleted file mode 100644 index 809d29b217..0000000000 --- a/novawallet/Modules/Staking/StakingAmount/StakingAmountViewController.swift +++ /dev/null @@ -1,311 +0,0 @@ -import UIKit -import SoraFoundation -import SoraUI -import SubstrateSdk - -final class StakingAmountViewController: UIViewController, ViewHolder { - typealias RootViewType = StakingAmountLayout - - let presenter: StakingAmountPresenterProtocol - - private var rewardDestinationViewModel: LocalizableResource? - private var assetViewModel: LocalizableResource? - private var feeViewModel: LocalizableResource? - private var amountInputViewModel: AmountInputViewModelProtocol? - - init(presenter: StakingAmountPresenterProtocol, localizationManager: LocalizationManagerProtocol) { - self.presenter = presenter - - super.init(nibName: nil, bundle: nil) - - self.localizationManager = localizationManager - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func loadView() { - view = StakingAmountLayout() - } - - override func viewDidLoad() { - super.viewDidLoad() - - setupBalanceAccessoryView() - setupLocalization() - updateActionButton() - updateRewardDestination() - setupHandlers() - - presenter.setup() - } - - // MARK: Private - - private func setupHandlers() { - rootView.restakeOptionView.addTarget(self, action: #selector(actionRestake), for: .touchUpInside) - rootView.payoutOptionView.addTarget(self, action: #selector(actionPayout), for: .touchUpInside) - rootView.actionButton.addTarget(self, action: #selector(actionProceed), for: .touchUpInside) - rootView.aboutLinkView.actionButton.addTarget(self, action: #selector(actionLearnPayout), for: .touchUpInside) - - let accountControl = rootView.accountView.actionControl - accountControl.addTarget(self, action: #selector(actionSelectPayoutAccount), for: .valueChanged) - } - - private func setupBalanceAccessoryView() { - let locale = localizationManager?.selectedLocale ?? Locale.current - let accessoryView = UIFactory.default.createAmountAccessoryView(for: self, locale: locale) - rootView.amountInputView.textField.inputAccessoryView = accessoryView - } - - private func setupLocalization() { - let locale = localizationManager?.selectedLocale ?? Locale.current - let languages = locale.rLanguages - - rootView.amountView.titleView.text = R.string.localizable.walletSendAmountTitle( - preferredLanguages: languages - ) - - rootView.amountView.detailsTitleLabel.text = R.string.localizable.commonAvailablePrefix( - preferredLanguages: languages - ) - - rootView.aboutLinkView.titleView.text = R.string.localizable.stakingRewardsDestinationTitle( - preferredLanguages: languages - ) - - rootView.aboutLinkView.actionButton.imageWithTitleView?.title = R.string.localizable.stakingAboutRewards( - preferredLanguages: languages - ) - - rootView.restakeOptionView.titleLabel.text = R.string.localizable.stakingSetupRestakeTitle_v2_2_0( - preferredLanguages: languages - ) - - rootView.payoutOptionView.titleLabel.text = R.string.localizable.stakingSetupPayoutTitle( - preferredLanguages: languages - ) - - rootView.networkFeeView.locale = locale - - applyAsset() - applyFee() - applyRewardDestinationViewModel() - - rootView.accountView.titleLabel.text = R.string.localizable.stakingRewardPayoutAccount( - preferredLanguages: languages - ) - - setupBalanceAccessoryView() - - updateActionButton() - } - - private func updateActionButton() { - let isEnabled = (amountInputViewModel?.isValid == true) - - if isEnabled { - rootView.actionButton.imageWithTitleView?.title = R.string.localizable.commonContinue( - preferredLanguages: selectedLocale.rLanguages - ) - - rootView.actionButton.applyEnabledStyle() - rootView.actionButton.isUserInteractionEnabled = true - } else { - rootView.actionButton.imageWithTitleView?.title = R.string.localizable.commonInputAmountHint( - preferredLanguages: selectedLocale.rLanguages - ) - - rootView.actionButton.applyDisabledStyle() - rootView.actionButton.isUserInteractionEnabled = false - } - } - - private func updateRewardDestination() { - let hasAmount = !(amountInputViewModel?.displayAmount ?? "").isEmpty - - let textColor: UIColor? - - if hasAmount { - textColor = R.color.colorTextPrimary() - } else { - textColor = R.color.colorTextSecondary() - } - - rootView.restakeOptionView.amountLabel.textColor = textColor - rootView.payoutOptionView.amountLabel.textColor = textColor - } - - private func applyAsset() { - if let viewModel = assetViewModel?.value(for: selectedLocale) { - title = R.string.localizable.stakingStakeFormat( - viewModel.symbol, - preferredLanguages: selectedLocale.rLanguages - ) - - let assetViewModel = AssetViewModel( - symbol: viewModel.symbol, - imageViewModel: viewModel.iconViewModel - ) - - rootView.amountInputView.bind(assetViewModel: assetViewModel) - rootView.amountInputView.bind(priceViewModel: viewModel.price) - - rootView.amountView.detailsValueLabel.text = viewModel.balance - } - } - - private func applyFee() { - let locale = localizationManager?.selectedLocale ?? Locale.current - let fee = feeViewModel?.value(for: locale) - rootView.networkFeeView.bind(viewModel: fee) - } - - private func applyRewardDestinationViewModel() { - guard let rewardDestViewModel = rewardDestinationViewModel else { return } - - let locale = localizationManager?.selectedLocale ?? Locale.current - let viewModel = rewardDestViewModel.value(for: locale) - applyRewardDestinationType(from: viewModel) - applyRewardDestinationContent(from: viewModel) - } - - private func applyRewardDestinationContent(from viewModel: RewardDestinationViewModelProtocol) { - let restakeView = rootView.restakeOptionView - let payoutView = rootView.payoutOptionView - - if let reward = viewModel.rewardViewModel { - restakeView.amountLabel.text = reward.restakeAmount - restakeView.incomeLabel.text = reward.restakePercentage - restakeView.priceLabel.text = reward.restakePrice - payoutView.amountLabel.text = reward.payoutAmount - payoutView.incomeLabel.text = reward.payoutPercentage - payoutView.priceLabel.text = reward.payoutPrice - } else { - restakeView.amountLabel.text = "" - restakeView.incomeLabel.text = "" - restakeView.priceLabel.text = "" - payoutView.amountLabel.text = "" - payoutView.incomeLabel.text = "" - payoutView.priceLabel.text = "" - } - - restakeView.setNeedsLayout() - payoutView.setNeedsLayout() - } - - private func applyRewardDestinationType(from viewModel: RewardDestinationViewModelProtocol) { - let restakeView = rootView.restakeOptionView - let payoutView = rootView.payoutOptionView - let accountView = rootView.accountView - - switch viewModel.type { - case .restake: - restakeView.isChoosen = true - payoutView.isChoosen = false - - rootView.setAccountShown(false) - case let .payout(details): - restakeView.isChoosen = false - payoutView.isChoosen = true - - rootView.setAccountShown(true) - accountView.bind(viewModel: details) - } - } - - @objc private func actionRestake() { - if !rootView.restakeOptionView.isChoosen { - presenter.selectRestakeDestination() - } - } - - @objc private func actionPayout() { - if !rootView.payoutOptionView.isChoosen { - presenter.selectPayoutDestination() - } - } - - @objc private func actionLearnPayout() { - presenter.selectLearnMore() - } - - @objc private func actionProceed() { - presenter.proceed() - } - - @objc private func actionSelectPayoutAccount() { - presenter.selectPayoutAccount() - } -} - -extension StakingAmountViewController: StakingAmountViewProtocol { - func didReceiveAsset(viewModel: LocalizableResource) { - assetViewModel = viewModel - applyAsset() - } - - func didReceiveRewardDestination(viewModel: LocalizableResource) { - rewardDestinationViewModel = viewModel - applyRewardDestinationViewModel() - } - - func didReceiveFee(viewModel: LocalizableResource?) { - feeViewModel = viewModel - applyFee() - - updateActionButton() - } - - func didReceiveInput(viewModel: LocalizableResource) { - let locale = localizationManager?.selectedLocale ?? Locale.current - let concreteViewModel = viewModel.value(for: locale) - - amountInputViewModel?.observable.remove(observer: self) - - amountInputViewModel = concreteViewModel - - rootView.amountInputView.bind(inputViewModel: concreteViewModel) - concreteViewModel.observable.add(observer: self) - - updateActionButton() - updateRewardDestination() - } - - func didCompletionAccountSelection() { - rootView.accountView.actionControl.deactivate(animated: true) - } -} - -extension StakingAmountViewController: AmountInputAccessoryViewDelegate { - func didSelect(on _: AmountInputAccessoryView, percentage: Float) { - rootView.amountInputView.textField.resignFirstResponder() - - presenter.selectAmountPercentage(percentage) - } - - func didSelectDone(on _: AmountInputAccessoryView) { - rootView.amountInputView.textField.resignFirstResponder() - } -} - -extension StakingAmountViewController: AmountInputViewModelObserver { - func amountInputDidChange() { - updateActionButton() - updateRewardDestination() - - let amount = amountInputViewModel?.decimalAmount ?? 0.0 - presenter.updateAmount(amount) - } -} - -extension StakingAmountViewController: Localizable { - func applyLocalization() { - if isViewLoaded { - setupLocalization() - view.setNeedsLayout() - } - } -} diff --git a/novawallet/Modules/Staking/StakingAmount/StakingAmountViewFactory.swift b/novawallet/Modules/Staking/StakingAmount/StakingAmountViewFactory.swift deleted file mode 100644 index f4295c0d2e..0000000000 --- a/novawallet/Modules/Staking/StakingAmount/StakingAmountViewFactory.swift +++ /dev/null @@ -1,126 +0,0 @@ -import Foundation -import SoraKeystore -import RobinHood -import SoraFoundation -import SubstrateSdk - -final class StakingAmountViewFactory { - static func createView( - with amount: Decimal?, - stakingState: StakingSharedState - ) -> StakingAmountViewProtocol? { - let chainAsset = stakingState.stakingOption.chainAsset - - guard - let metaAccount = SelectedWalletSettings.shared.value, - let currencyManager = CurrencyManager.shared, - let chainAccount = metaAccount.fetchMetaChainAccount(for: chainAsset.chain.accountRequest()) else { - return nil - } - - guard let interactor = createInteractor(state: stakingState) else { - return nil - } - - let wireframe = StakingAmountWireframe(stakingState: stakingState) - - let assetInfo = chainAsset.assetDisplayInfo - let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) - let balanceViewModelFactory = BalanceViewModelFactory( - targetAssetInfo: assetInfo, - priceAssetInfoFactory: priceAssetInfoFactory - ) - - let dataValidatingFactory = StakingDataValidatingFactory( - presentable: wireframe, - balanceFactory: balanceViewModelFactory - ) - - let rewardDestViewModelFactory = RewardDestinationViewModelFactory( - balanceViewModelFactory: balanceViewModelFactory - ) - - let presenter = StakingAmountPresenter( - wireframe: wireframe, - interactor: interactor, - amount: amount, - selectedAccount: chainAccount, - assetInfo: assetInfo, - rewardDestViewModelFactory: rewardDestViewModelFactory, - balanceViewModelFactory: balanceViewModelFactory, - dataValidatingFactory: dataValidatingFactory, - applicationConfig: ApplicationConfig.shared, - logger: Logger.shared - ) - - let view = StakingAmountViewController( - presenter: presenter, - localizationManager: LocalizationManager.shared - ) - - interactor.presenter = presenter - presenter.view = view - dataValidatingFactory.view = view - - return view - } - - private static func createInteractor( - state: StakingSharedState - ) -> StakingAmountInteractor? { - let chainAsset = state.stakingOption.chainAsset - - guard - let metaAccount = SelectedWalletSettings.shared.value, - let selectedAccount = metaAccount.fetch(for: chainAsset.chain.accountRequest()), - let rewardCalculationService = state.rewardCalculationService, - let validatorService = state.eraValidatorService, - let networkInfoOperationFactory = try? state.createNetworkInfoOperationFactory( - for: chainAsset.chain - ), - let currencyManager = CurrencyManager.shared else { - return nil - } - - let chainRegistry = ChainRegistryFacade.sharedRegistry - - guard - let runtimeService = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId), - let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId) else { - return nil - } - - let operationManager = OperationManagerFacade.sharedManager - - let facade = UserDataStorageFacade.shared - - let accountRepository = AccountRepositoryFactory(storageFacade: facade).createMetaAccountRepository( - for: nil, - sortDescriptors: [NSSortDescriptor.accountsByOrder] - ) - - let extrinsicService = ExtrinsicServiceFactory( - runtimeRegistry: runtimeService, - engine: connection, - operationManager: operationManager - ).createService(account: selectedAccount, chain: chainAsset.chain) - - let interactor = StakingAmountInteractor( - selectedAccount: selectedAccount, - chainAsset: chainAsset, - stakingLocalSubscriptionFactory: state.stakingLocalSubscriptionFactory, - walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, - priceLocalSubscriptionFactory: PriceProviderFactory.shared, - repository: accountRepository, - extrinsicService: extrinsicService, - runtimeService: runtimeService, - rewardService: rewardCalculationService, - networkInfoOperationFactory: networkInfoOperationFactory, - eraValidatorService: validatorService, - operationManager: operationManager, - currencyManager: currencyManager - ) - - return interactor - } -} diff --git a/novawallet/Modules/Staking/StakingAmount/StakingAmountWireframe.swift b/novawallet/Modules/Staking/StakingAmount/StakingAmountWireframe.swift deleted file mode 100644 index 40ad5683e5..0000000000 --- a/novawallet/Modules/Staking/StakingAmount/StakingAmountWireframe.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation -import SoraFoundation - -final class StakingAmountWireframe: StakingAmountWireframeProtocol { - let stakingState: StakingSharedState - - init(stakingState: StakingSharedState) { - self.stakingState = stakingState - } - - func presentAccountSelection( - _ accounts: [MetaChainAccountResponse], - selectedAccount: MetaChainAccountResponse, - delegate: ModalPickerViewControllerDelegate, - from view: StakingAmountViewProtocol?, - context: AnyObject? - ) { - let title = LocalizableResource { locale in - R.string.localizable - .stakingRewardPayoutAccount(preferredLanguages: locale.rLanguages) - } - - guard let picker = ModalPickerFactory.createPickerList( - accounts, - selectedAccount: selectedAccount, - title: title, - delegate: delegate, - context: context - ) else { - return - } - - view?.controller.present(picker, animated: true, completion: nil) - } - - func proceed(from view: StakingAmountViewProtocol?, state: InitiatedBonding) { - guard let validatorsView = SelectValidatorsStartViewFactory.createInitiatedBondingView( - with: state, - stakingState: stakingState - ) else { - return - } - - view?.controller.navigationController?.pushViewController( - validatorsView.controller, - animated: true - ) - } - - func close(view: StakingAmountViewProtocol?) { - view?.controller.presentingViewController?.dismiss(animated: true, completion: nil) - } -} diff --git a/novawallet/Modules/Staking/StakingBondMore/StakingBondMoreInteractor.swift b/novawallet/Modules/Staking/StakingBondMore/StakingBondMoreInteractor.swift index 7f85c9d6b0..c8bccb5fff 100644 --- a/novawallet/Modules/Staking/StakingBondMore/StakingBondMoreInteractor.swift +++ b/novawallet/Modules/Staking/StakingBondMore/StakingBondMoreInteractor.swift @@ -63,7 +63,7 @@ final class StakingBondMoreInteractor: AccountFetching { extension StakingBondMoreInteractor: StakingBondMoreInteractorInputProtocol { func setup() { if let address = selectedAccount.toAddress() { - stashItemProvider = subscribeStashItemProvider(for: address) + stashItemProvider = subscribeStashItemProvider(for: address, chainId: chainAsset.chain.chainId) } if let priceId = chainAsset.asset.priceId { diff --git a/novawallet/Modules/Staking/StakingBondMore/StakingBondMoreViewFactory.swift b/novawallet/Modules/Staking/StakingBondMore/StakingBondMoreViewFactory.swift index 7b34ebac75..0b200637a7 100644 --- a/novawallet/Modules/Staking/StakingBondMore/StakingBondMoreViewFactory.swift +++ b/novawallet/Modules/Staking/StakingBondMore/StakingBondMoreViewFactory.swift @@ -4,7 +4,7 @@ import RobinHood import SubstrateSdk struct StakingBondMoreViewFactory { - static func createView(from state: StakingSharedState) -> StakingBondMoreViewProtocol? { + static func createView(from state: RelaychainStakingSharedStateProtocol) -> StakingBondMoreViewProtocol? { let chainAsset = state.stakingOption.chainAsset guard @@ -51,7 +51,7 @@ struct StakingBondMoreViewFactory { private static func createInteractor( selectedAccount: ChainAccountResponse, - state: StakingSharedState + state: RelaychainStakingSharedStateProtocol ) -> StakingBondMoreInteractor? { let chainRegistry = ChainRegistryFacade.sharedRegistry @@ -81,7 +81,7 @@ struct StakingBondMoreViewFactory { chainAsset: chainAsset, accountRepositoryFactory: accountRepositoryFactory, extrinsicServiceFactory: extrinsicServiceFactory, - stakingLocalSubscriptionFactory: state.stakingLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.localSubscriptionFactory, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, feeProxy: feeProxy, diff --git a/novawallet/Modules/Staking/StakingBondMore/StakingBondMoreWireframe.swift b/novawallet/Modules/Staking/StakingBondMore/StakingBondMoreWireframe.swift index 4cb2fc0875..68b12e07eb 100644 --- a/novawallet/Modules/Staking/StakingBondMore/StakingBondMoreWireframe.swift +++ b/novawallet/Modules/Staking/StakingBondMore/StakingBondMoreWireframe.swift @@ -1,9 +1,9 @@ import Foundation final class StakingBondMoreWireframe: StakingBondMoreWireframeProtocol { - let state: StakingSharedState + let state: RelaychainStakingSharedStateProtocol - init(state: StakingSharedState) { + init(state: RelaychainStakingSharedStateProtocol) { self.state = state } diff --git a/novawallet/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationInteractor.swift b/novawallet/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationInteractor.swift index 1dd4cfeaeb..1d3167196f 100644 --- a/novawallet/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationInteractor.swift +++ b/novawallet/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationInteractor.swift @@ -70,7 +70,7 @@ final class StakingBondMoreConfirmationInteractor: AccountFetching { extension StakingBondMoreConfirmationInteractor: StakingBondMoreConfirmationInteractorInputProtocol { func setup() { if let address = selectedAccount.toAddress() { - stashItemProvider = subscribeStashItemProvider(for: address) + stashItemProvider = subscribeStashItemProvider(for: address, chainId: chainAsset.chain.chainId) } if let priceId = chainAsset.asset.priceId { diff --git a/novawallet/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationViewFactory.swift b/novawallet/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationViewFactory.swift index 5c765b595f..043f6ea17a 100644 --- a/novawallet/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationViewFactory.swift +++ b/novawallet/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationViewFactory.swift @@ -6,7 +6,7 @@ import RobinHood struct StakingBondMoreConfirmViewFactory { static func createView( from amount: Decimal, - state: StakingSharedState + state: RelaychainStakingSharedStateProtocol ) -> StakingBondMoreConfirmationViewProtocol? { guard let interactor = createInteractor(for: state), let currencyManager = CurrencyManager.shared else { @@ -69,7 +69,7 @@ struct StakingBondMoreConfirmViewFactory { } private static func createInteractor( - for state: StakingSharedState + for state: RelaychainStakingSharedStateProtocol ) -> StakingBondMoreConfirmationInteractor? { let chainAsset = state.stakingOption.chainAsset @@ -104,7 +104,7 @@ struct StakingBondMoreConfirmViewFactory { accountRepositoryFactory: accountRepositoryFactory, extrinsicServiceFactory: extrinsicServiceFactory, signingWrapperFactory: SigningWrapperFactory(), - stakingLocalSubscriptionFactory: state.stakingLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.localSubscriptionFactory, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, feeProxy: ExtrinsicFeeProxy(), diff --git a/novawallet/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationWireframe.swift b/novawallet/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationWireframe.swift index 17481093d2..dea8a58f98 100644 --- a/novawallet/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationWireframe.swift +++ b/novawallet/Modules/Staking/StakingBondMoreConfirmation/StakingBondMoreConfirmationWireframe.swift @@ -1,8 +1,8 @@ final class StakingBondMoreConfirmationWireframe: StakingBondMoreConfirmationWireframeProtocol, ModalAlertPresenting { - let state: StakingSharedState + let state: RelaychainStakingSharedStateProtocol - init(state: StakingSharedState) { + init(state: RelaychainStakingSharedStateProtocol) { self.state = state } diff --git a/novawallet/Modules/Staking/StakingMain/NominationPools/StakingMainPresenterFactory+NominationPools.swift b/novawallet/Modules/Staking/StakingMain/NominationPools/StakingMainPresenterFactory+NominationPools.swift new file mode 100644 index 0000000000..2ff98e4bdd --- /dev/null +++ b/novawallet/Modules/Staking/StakingMain/NominationPools/StakingMainPresenterFactory+NominationPools.swift @@ -0,0 +1,79 @@ +import Foundation +import SoraFoundation + +extension StakingMainPresenterFactory { + func createNominationPoolsPresenter( + for chainAsset: ChainAsset, + view: StakingMainViewProtocol + ) -> StakingNPoolsPresenter? { + guard + let consensus = ConsensusType(asset: chainAsset.asset), + let state = try? sharedStateFactory.createNominationPools(for: chainAsset, consensus: consensus), + let currencyManager = CurrencyManager.shared, + let interactor = createNominationPoolsInteractor( + state: state, + currencyManager: currencyManager + ) else { + return nil + } + + let wireframe = StakingNPoolsWireframe(state: state) + + let priceInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) + + let infoViewModelFactory = NetworkInfoViewModelFactory(priceAssetInfoFactory: priceInfoFactory) + + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: chainAsset.assetDisplayInfo, + priceAssetInfoFactory: priceInfoFactory + ) + + let stateViewModelFactory = StakingNPoolsViewModelFactory(balanceViewModelFactory: balanceViewModelFactory) + + let presenter = StakingNPoolsPresenter( + interactor: interactor, + wireframe: wireframe, + infoViewModelFactory: infoViewModelFactory, + stateViewModelFactory: stateViewModelFactory, + quantityFormatter: NumberFormatter.quantity.localizableResource(), + chainAsset: state.chainAsset, + localizationManager: LocalizationManager.shared, + logger: Logger.shared + ) + + presenter.view = view + interactor.presenter = presenter + + return presenter + } + + func createNominationPoolsInteractor( + state: NPoolsStakingSharedStateProtocol, + currencyManager: CurrencyManagerProtocol + ) -> StakingNPoolsInteractor? { + let chainId = state.chainAsset.chain.chainId + let accountRequest = state.chainAsset.chain.accountRequest() + + guard + let selectedAccount = SelectedWalletSettings.shared.value?.fetchMetaChainAccount(for: accountRequest), + let runtimeService = ChainRegistryFacade.sharedRegistry.getRuntimeProvider(for: chainId), + let connection = ChainRegistryFacade.sharedRegistry.getConnection(for: chainId) else { + return nil + } + + let operationQueue = OperationManagerFacade.sharedDefaultQueue + + return .init( + state: state, + selectedAccount: selectedAccount, + npoolsOperationFactory: NominationPoolsOperationFactory(operationQueue: operationQueue), + connection: connection, + runtimeCodingService: runtimeService, + priceLocalSubscriptionFactory: PriceProviderFactory.shared, + eventCenter: EventCenter.shared, + applicationHandler: ApplicationHandler(), + currencyManager: currencyManager, + operationQueue: operationQueue + ) + } +} diff --git a/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsError.swift b/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsError.swift new file mode 100644 index 0000000000..70117b971c --- /dev/null +++ b/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsError.swift @@ -0,0 +1,12 @@ +import Foundation + +enum StakingNPoolsError: Error { + case stateSetup(Error) + case subscription(Error, String) + case totalActiveStake(Error) + case stakingDuration(Error) + case activePools(Error) + case eraCountdown(Error) + case claimableRewards(Error) + case totalRewards(Error) +} diff --git a/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsInteractor.swift b/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsInteractor.swift new file mode 100644 index 0000000000..ef44b7a258 --- /dev/null +++ b/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsInteractor.swift @@ -0,0 +1,610 @@ +import Foundation +import BigInt +import RobinHood +import SoraFoundation +import SubstrateSdk + +final class StakingNPoolsInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, + StakingDurationFetching, NominationPoolsDataProviding { + weak var presenter: StakingNPoolsInteractorOutputProtocol? + + let state: NPoolsStakingSharedStateProtocol + let selectedAccount: MetaChainAccountResponse + let npoolsOperationFactory: NominationPoolsOperationFactoryProtocol + let connection: JSONRPCEngine + let runtimeCodingService: RuntimeCodingServiceProtocol + let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol + let eventCenter: EventCenterProtocol + let applicationHandler: ApplicationHandlerProtocol + let operationQueue: OperationQueue + + var stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol { + state.relaychainLocalSubscriptionFactory + } + + private var minJoinBondProvider: AnyDataProvider? + private var lastPoolIdProvider: AnyDataProvider? + private var poolMemberProvider: AnyDataProvider? + private var bondedPoolProvider: AnyDataProvider? + private var metadataProvider: AnyDataProvider? + private var subpoolsProvider: AnyDataProvider? + private var rewardPoolProvider: AnyDataProvider? + private var poolLedgerProvider: AnyDataProvider? + private var poolNominationProvider: AnyDataProvider? + private var activeEraProvider: AnyDataProvider? + private var claimableRewardProvider: AnySingleValueProvider? + private var totalRewardProvider: AnySingleValueProvider? + private var priceProvider: StreamableProvider? + + private var activeStakeCancellable: CancellableCall? + private var durationCancellable: CancellableCall? + private var bondedAccountIdCancellable: CancellableCall? + private var activePoolsCancellable: CancellableCall? + private var eraCountdownCancellable: CancellableCall? + + private var lastPoolId: NominationPools.PoolId? + private var currentPoolId: NominationPools.PoolId? + private var currentPoolRewardCounter: BigUInt? + private var currentMemberRewardCounter: BigUInt? + private var poolAccountId: AccountId? + private var totalRewardsPeriod: StakingRewardFiltersPeriod? + + var npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol { + state.npLocalSubscriptionFactory + } + + var asset: AssetModel { + state.chainAsset.asset + } + + var chain: ChainModel { + state.chainAsset.chain + } + + var chainId: ChainModel.Id { + state.chainAsset.chain.chainId + } + + var accountId: AccountId { + selectedAccount.chainAccount.accountId + } + + init( + state: NPoolsStakingSharedStateProtocol, + selectedAccount: MetaChainAccountResponse, + npoolsOperationFactory: NominationPoolsOperationFactoryProtocol, + connection: JSONRPCEngine, + runtimeCodingService: RuntimeCodingServiceProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + eventCenter: EventCenterProtocol, + applicationHandler: ApplicationHandlerProtocol, + currencyManager: CurrencyManagerProtocol, + operationQueue: OperationQueue + ) { + self.state = state + self.selectedAccount = selectedAccount + self.npoolsOperationFactory = npoolsOperationFactory + self.runtimeCodingService = runtimeCodingService + self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory + self.connection = connection + self.eventCenter = eventCenter + self.applicationHandler = applicationHandler + self.operationQueue = operationQueue + self.currencyManager = currencyManager + } + + deinit { + state.throttle() + + clearOperations() + } + + func clearOperations() { + clear(cancellable: &activeStakeCancellable) + clear(cancellable: &durationCancellable) + clear(cancellable: &bondedAccountIdCancellable) + clear(cancellable: &activePoolsCancellable) + clear(cancellable: &eraCountdownCancellable) + } + + func setupBaseProviders() { + bondedPoolProvider = nil + metadataProvider = nil + subpoolsProvider = nil + rewardPoolProvider = nil + poolLedgerProvider = nil + poolNominationProvider = nil + + lastPoolId = nil + currentPoolId = nil + poolAccountId = nil + + minJoinBondProvider = subscribeMinJoinBond(for: chainId) + lastPoolIdProvider = subscribeLastPoolId(for: chainId) + poolMemberProvider = subscribePoolMember(for: accountId, chainId: chainId) + activeEraProvider = subscribeActiveEra(for: chainId) + } + + func setupCurrencyProvider() { + guard let priceId = asset.priceId else { + presenter?.didReceive(price: nil) + return + } + + priceProvider = subscribeToPrice(for: priceId, currency: selectedCurrency) + } + + func setupProviders(for poolId: NominationPools.PoolId) { + bondedPoolProvider = subscribeBondedPool(for: poolId, chainId: chainId) + metadataProvider = subscribePoolMetadata(for: poolId, chainId: chainId) + subpoolsProvider = subscribeSubPools(for: poolId, chainId: chainId) + rewardPoolProvider = subscribeRewardPool(for: poolId, chainId: chainId) + } + + func setupBondedAccountProviders() { + poolLedgerProvider = nil + poolNominationProvider = nil + + guard let accountId = poolAccountId else { + return + } + + poolLedgerProvider = subscribeLedgerInfo(for: accountId, chainId: chainId) + poolNominationProvider = subscribeNomination(for: accountId, chainId: chainId) + } + + func setupClaimableRewardsProvider() { + guard let poolId = currentPoolId else { + return + } + + claimableRewardProvider = subscribeClaimableRewards( + for: chainId, + poolId: poolId, + accountId: accountId + ) + + if claimableRewardProvider == nil { + presenter?.didReceive(error: .claimableRewards(CommonError.dataCorruption)) + } + } + + func provideStakingDuration() { + clear(cancellable: &durationCancellable) + + let stakingDurationFactory = state.createStakingDurationOperationFactory() + + let wrapper = stakingDurationFactory.createDurationOperation(from: runtimeCodingService) + + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard wrapper === self?.durationCancellable else { + return + } + + self?.durationCancellable = nil + + do { + let duration = try wrapper.targetOperation.extractNoCancellableResultData() + self?.presenter?.didReceive(duration: duration) + } catch { + self?.presenter?.didReceive(error: .stakingDuration(error)) + } + } + } + + durationCancellable = wrapper + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } + + func provideTotalActiveStake() { + guard let lastPoolId = lastPoolId else { + return + } + + clear(cancellable: &activeStakeCancellable) + + let wrapper = npoolsOperationFactory.createPoolsActiveTotalStakeWrapper( + for: lastPoolId, + eraValidatorService: state.eraValidatorService, + runtimeService: runtimeCodingService + ) + + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard wrapper === self?.activeStakeCancellable else { + return + } + + self?.activeStakeCancellable = nil + + do { + let stake = try wrapper.targetOperation.extractNoCancellableResultData() + self?.presenter?.didReceive(totalActiveStake: stake) + } catch { + self?.presenter?.didReceive(error: .totalActiveStake(error)) + } + } + } + + activeStakeCancellable = wrapper + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } + + private func provideBondedAccountId() { + clear(cancellable: &bondedAccountIdCancellable) + + guard let poolId = currentPoolId else { + return + } + + bondedAccountIdCancellable = fetchBondedAccounts( + for: npoolsOperationFactory, + poolIds: { [poolId] }, + runtimeService: runtimeCodingService, + operationQueue: operationQueue, + completion: { [weak self] result in + self?.bondedAccountIdCancellable = nil + + switch result { + case let .success(accountIds): + if let accountId = accountIds[poolId] { + self?.presenter?.didReceive(poolBondedAccountId: accountId) + + self?.poolAccountId = accountId + self?.setupBondedAccountProviders() + } + case let .failure(error): + self?.presenter?.didReceive(error: .subscription(error, "bondedAccountId")) + } + } + ) + } + + private func provideActivePools() { + clear(cancellable: &activePoolsCancellable) + + let poolsOperation = state.activePoolsService.fetchInfoOperation() + + let mapOperation = ClosureOperation> { + let activePools = try poolsOperation.extractNoCancellableResultData() + return Set(activePools.pools.map(\.poolId)) + } + + mapOperation.addDependency(poolsOperation) + + let wrapper = CompoundOperationWrapper(targetOperation: mapOperation, dependencies: [poolsOperation]) + + mapOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard wrapper === self?.activePoolsCancellable else { + return + } + + self?.activePoolsCancellable = nil + + do { + let activePools = try mapOperation.extractNoCancellableResultData() + self?.presenter?.didReceive(activePools: activePools) + } catch { + self?.presenter?.didReceive(error: .activePools(error)) + } + } + } + + activePoolsCancellable = wrapper + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } + + private func provideEraCountdown() { + clear(cancellable: &eraCountdownCancellable) + + let factory = state.createEraCountdownOperationFactory(for: operationQueue) + + let wrapper = factory.fetchCountdownOperationWrapper(for: connection, runtimeService: runtimeCodingService) + + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard wrapper === self?.eraCountdownCancellable else { + return + } + + self?.eraCountdownCancellable = nil + + do { + let eraCountdown = try wrapper.targetOperation.extractNoCancellableResultData() + self?.presenter?.didReceive(eraCountdown: eraCountdown) + } catch { + self?.presenter?.didReceive(error: .eraCountdown(error)) + } + } + } + + eraCountdownCancellable = wrapper + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } +} + +extension StakingNPoolsInteractor: StakingNPoolsInteractorInputProtocol { + func setup() { + do { + try state.setup(for: accountId) + + setupBaseProviders() + setupCurrencyProvider() + provideStakingDuration() + provideActivePools() + provideEraCountdown() + + eventCenter.add(observer: self, dispatchIn: .main) + applicationHandler.delegate = self + } catch { + presenter?.didReceive(error: .stateSetup(error)) + } + } + + func setupTotalRewards(filter: StakingRewardFiltersPeriod) { + clear(singleValueProvider: &totalRewardProvider) + + totalRewardsPeriod = filter + + if let address = try? accountId.toAddress(using: chain.chainFormat) { + if let rewardApi = chain.externalApis?.staking()?.first { + let totalRewardInterval = filter.interval + totalRewardProvider = subscribePoolTotalReward( + for: address, + startTimestamp: totalRewardInterval.startTimestamp, + endTimestamp: totalRewardInterval.endTimestamp, + api: rewardApi, + assetPrecision: state.chainAsset.assetDisplayInfo.assetPrecision + ) + } else { + let zeroReward = TotalRewardItem( + address: address, + amount: AmountDecimal(value: 0) + ) + presenter?.didReceive(totalRewards: zeroReward) + } + } + } + + func remakeSubscriptions() { + setupBaseProviders() + setupCurrencyProvider() + } + + func retryActiveStake() { + provideTotalActiveStake() + } + + func retryStakingDuration() { + provideStakingDuration() + } + + func retryActivePools() { + provideActivePools() + } + + func retryEraCountdown() { + provideEraCountdown() + } + + func retryClaimableRewards() { + setupClaimableRewardsProvider() + } +} + +extension StakingNPoolsInteractor: NPoolsLocalStorageSubscriber, NPoolsLocalSubscriptionHandler { + func handleMinJoinBond(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(optMinJoinBond): + presenter?.didReceive(minStake: optMinJoinBond) + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "minJoinBond")) + } + } + + func handleLastPoolId(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(optLastPoolId): + lastPoolId = optLastPoolId + provideTotalActiveStake() + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "lastPoolId")) + } + } + + func handlePoolMember( + result: Result, + accountId _: AccountId, + chainId _: ChainModel.Id + ) { + switch result { + case let .success(optPoolMember): + presenter?.didReceive(poolMember: optPoolMember) + + if let poolId = optPoolMember?.poolId, currentPoolId != poolId { + self.currentPoolId = poolId + + setupProviders(for: poolId) + setupClaimableRewardsProvider() + provideBondedAccountId() + } + + if currentMemberRewardCounter != optPoolMember?.lastRecordedRewardCounter { + currentMemberRewardCounter = optPoolMember?.lastRecordedRewardCounter + + claimableRewardProvider?.refresh() + } + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "poolMember")) + } + } + + func handleBondedPool( + result: Result, + poolId _: NominationPools.PoolId, + chainId _: ChainModel.Id + ) { + switch result { + case let .success(optBondedPool): + presenter?.didReceive(bondedPool: optBondedPool) + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "bondedPool")) + } + } + + func handlePoolMetadata( + result: Result, + poolId _: NominationPools.PoolId, + chainId _: ChainModel.Id + ) { + switch result { + case let .success(optPoolMetadata): + presenter?.didReceive(poolMetadata: optPoolMetadata) + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "poolMetadata")) + } + } + + func handleSubPools( + result: Result, + poolId _: NominationPools.PoolId, + chainId _: ChainModel.Id + ) { + switch result { + case let .success(optSubPools): + presenter?.didReceive(subPools: optSubPools) + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "subPools")) + } + } + + func handleRewardPool( + result: Result, + poolId: NominationPools.PoolId, + chainId _: ChainModel.Id + ) { + guard currentPoolId == poolId else { + return + } + + if case let .success(rewardPool) = result, rewardPool?.lastRecordedRewardCounter != currentPoolRewardCounter { + self.currentPoolRewardCounter = rewardPool?.lastRecordedRewardCounter + + claimableRewardProvider?.refresh() + } + } + + func handleClaimableRewards( + result: Result, + chainId _: ChainModel.Id, + poolId: NominationPools.PoolId, + accountId _: AccountId + ) { + guard currentPoolId == poolId else { + return + } + + switch result { + case let .success(rewards): + presenter?.didRecieve(claimableRewards: rewards) + case let .failure(error): + presenter?.didReceive(error: .claimableRewards(error)) + } + } + + func handlePoolTotalReward( + result: Result, + for _: AccountAddress, + startTimestamp: Int64?, + endTimestamp: Int64?, + api _: LocalChainExternalApi + ) { + guard + let interval = totalRewardsPeriod?.interval, + startTimestamp == interval.startTimestamp, + endTimestamp == interval.endTimestamp else { + return + } + + switch result { + case let .success(totalRewards): + presenter?.didReceive(totalRewards: totalRewards) + case let .failure(error): + presenter?.didReceive(error: .totalRewards(error)) + } + } +} + +extension StakingNPoolsInteractor: StakingLocalStorageSubscriber, StakingLocalSubscriptionHandler { + func handleActiveEra(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(optActiveEra): + presenter?.didReceive(activeEra: optActiveEra) + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "activeEra")) + } + } + + func handleNomination(result: Result, accountId _: AccountId, chainId _: ChainModel.Id) { + switch result { + case let .success(optNomination): + presenter?.didReceive(poolNomination: optNomination) + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "poolNomination")) + } + } + + func handleLedgerInfo(result: Result, accountId _: AccountId, chainId _: ChainModel.Id) { + switch result { + case let .success(optLedger): + presenter?.didReceive(poolLedger: optLedger) + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "stakingLedger")) + } + } +} + +extension StakingNPoolsInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { + func handlePrice(result: Result, priceId _: AssetModel.PriceId) { + switch result { + case let .success(optPrice): + presenter?.didReceive(price: optPrice) + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "price")) + } + } +} + +extension StakingNPoolsInteractor: EventVisitorProtocol { + func processEraStakersInfoChanged(event _: EraStakersInfoChanged) { + provideTotalActiveStake() + provideActivePools() + } + + func processBlockTimeChanged(event _: BlockTimeChanged) { + provideStakingDuration() + provideEraCountdown() + } +} + +extension StakingNPoolsInteractor: SelectedCurrencyDepending { + func applyCurrency() { + guard presenter != nil else { + return + } + + setupCurrencyProvider() + } +} + +extension StakingNPoolsInteractor: ApplicationHandlerDelegate { + func didReceiveDidBecomeActive(notification _: Notification) { + priceProvider?.refresh() + } +} diff --git a/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsPresenter.swift b/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsPresenter.swift new file mode 100644 index 0000000000..760769210b --- /dev/null +++ b/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsPresenter.swift @@ -0,0 +1,378 @@ +import Foundation +import SoraFoundation +import BigInt + +final class StakingNPoolsPresenter { + weak var view: StakingMainViewProtocol? + + let interactor: StakingNPoolsInteractorInputProtocol + let wireframe: StakingNPoolsWireframeProtocol + let infoViewModelFactory: NetworkInfoViewModelFactoryProtocol + let stateViewModelFactory: StakingNPoolsViewModelFactoryProtocol + let quantityFormatter: LocalizableResource + let chainAsset: ChainAsset + let logger: LoggerProtocol + + private var totalActiveStake: BigUInt? + private var minStake: BigUInt? + private var activeEra: ActiveEraInfo? + private var poolMember: NominationPools.PoolMember? + private var bondedPool: NominationPools.BondedPool? + private var subPools: NominationPools.SubPools? + private var poolLedger: StakingLedger? + private var poolNomination: Nomination? + private var poolBondedAccountId: AccountId? + private var activePools: Set? + private var duration: StakingDuration? + private var claimableRewards: BigUInt? + private var eraCountdown: EraCountdown? + private var priceData: PriceData? + private var totalRewardsFilter: StakingRewardFiltersPeriod? + private var totalRewards: TotalRewardItem? + private var poolMetadata: UncertainStorage = .undefined + + private lazy var displayViewModelFactory = DisplayAddressViewModelFactory() + + init( + interactor: StakingNPoolsInteractorInputProtocol, + wireframe: StakingNPoolsWireframeProtocol, + infoViewModelFactory: NetworkInfoViewModelFactoryProtocol, + stateViewModelFactory: StakingNPoolsViewModelFactoryProtocol, + quantityFormatter: LocalizableResource, + chainAsset: ChainAsset, + localizationManager: LocalizationManagerProtocol, + logger: LoggerProtocol + ) { + self.interactor = interactor + self.wireframe = wireframe + self.infoViewModelFactory = infoViewModelFactory + self.stateViewModelFactory = stateViewModelFactory + self.quantityFormatter = quantityFormatter + self.chainAsset = chainAsset + self.logger = logger + self.localizationManager = localizationManager + } + + private func provideStatics() { + view?.didReceiveStatics(viewModel: StakingNominationPoolsStatics()) + } + + private func provideStakingInfo() { + let params = NPoolsDetailsInfoParams( + totalActiveStake: totalActiveStake, + minStake: minStake, + duration: duration + ) + + let viewModel = infoViewModelFactory.createNPoolsStakingInfoViewModel( + for: params, + chainAsset: chainAsset, + priceData: priceData, + locale: selectedLocale + ) + + view?.didRecieveNetworkStakingInfo(viewModel: viewModel) + } + + private func provideState() { + let params = StakingNPoolsViewModelParams( + poolMember: poolMember, + bondedPool: bondedPool, + subPools: subPools, + poolLedger: poolLedger, + poolNomination: poolNomination, + activePools: activePools, + activeEra: activeEra, + eraCountdown: eraCountdown, + totalRewards: totalRewards, + totalRewardsFilter: totalRewardsFilter, + claimableRewards: claimableRewards + ) + + let viewModel = stateViewModelFactory.createState(for: params, chainAsset: chainAsset, price: priceData) + + view?.didReceiveStakingState(viewModel: viewModel) + } + + private func provideYourPool() { + guard let view = view else { + return + } + + let locale = view.selectedLocale + + if + let poolMember = poolMember, + let poolBondedAccountId = poolBondedAccountId, + case let .defined(poolMetadata) = poolMetadata { + let poolNumber = quantityFormatter.value(for: locale).string(from: NSNumber(value: poolMember.poolId)) + let title = R.string.localizable.stakingYourPoolFormat( + poolNumber ?? "", + preferredLanguages: locale.rLanguages + ) + + let selectedPool = NominationPools.SelectedPool( + poolId: poolMember.poolId, + bondedAccountId: poolBondedAccountId, + metadata: poolMetadata, + maxApy: nil + ) + + let displayAddress = displayViewModelFactory.createViewModel(from: selectedPool, chainAsset: chainAsset) + + let entity = StakingSelectedEntityViewModel(title: title, loadingAddress: .loaded(value: displayAddress)) + view.didReceiveSelectedEntity(entity) + + } else { + let entity = StakingSelectedEntityViewModel( + title: R.string.localizable.stakingYourPoolTitle(preferredLanguages: locale.rLanguages), + loadingAddress: .loading + ) + + view.didReceiveSelectedEntity(entity) + } + } + + private func updateView() { + provideStatics() + provideStakingInfo() + provideState() + provideYourPool() + } +} + +extension StakingNPoolsPresenter: StakingMainChildPresenterProtocol { + func setup() { + updateView() + interactor.setup() + } + + func performRedeemAction() { + wireframe.showRedeem(from: view) + } + + func performRebondAction() { + logger.warning("Not possible action for nomination pools") + } + + func performClaimRewards() { + wireframe.showClaimRewards(from: view) + } + + func performSelectedEntityAction() { + guard + let address = try? poolBondedAccountId?.toAddress(using: chainAsset.chain.chainFormat), + let view = view else { + return + } + + wireframe.presentAccountOptions( + from: view, + address: address, + chain: chainAsset.chain, + locale: view.selectedLocale + ) + } + + func performManageAction(_ action: StakingManageOption) { + switch action { + case .stakeMore: + wireframe.showStakeMore(from: view) + case .unstake: + wireframe.showUnstake(from: view) + default: + logger.warning("Unsupported action: \(action)") + } + } + + func performAlertAction(_ alert: StakingAlert) { + switch alert { + case .redeemUnbonded: + performRedeemAction() + case .waitingNextEra: + break + default: + logger.warning("Unsupported alert: \(alert)") + } + } + + func selectPeriod(_ filter: StakingRewardFiltersPeriod) { + totalRewardsFilter = filter + interactor.setupTotalRewards(filter: filter) + + provideState() + } +} + +extension StakingNPoolsPresenter: StakingNPoolsInteractorOutputProtocol { + func didReceive(minStake: BigUInt?) { + self.minStake = minStake + + provideStakingInfo() + } + + func didReceive(duration: StakingDuration) { + self.duration = duration + + provideStakingInfo() + } + + func didReceive(totalActiveStake: BigUInt) { + self.totalActiveStake = totalActiveStake + + provideStakingInfo() + } + + func didReceive(activeEra: ActiveEraInfo?) { + logger.debug("Active era: \(String(describing: activeEra))") + + self.activeEra = activeEra + + provideState() + } + + func didReceive(poolLedger: StakingLedger?) { + logger.debug("Pool Ledger: \(String(describing: poolLedger))") + + self.poolLedger = poolLedger + + provideState() + } + + func didReceive(poolNomination: Nomination?) { + logger.debug("Pool nomination: \(String(describing: poolNomination))") + + self.poolNomination = poolNomination + + provideState() + } + + func didReceive(poolMember: NominationPools.PoolMember?) { + logger.debug("Pool member: \(String(describing: poolMember))") + + self.poolMember = poolMember + + provideState() + provideYourPool() + } + + func didReceive(bondedPool: NominationPools.BondedPool?) { + logger.debug("Bonded pool: \(String(describing: bondedPool))") + + self.bondedPool = bondedPool + + provideState() + } + + func didReceive(subPools: NominationPools.SubPools?) { + logger.debug("SubPools: \(String(describing: subPools))") + + self.subPools = subPools + + provideState() + } + + func didReceive(poolBondedAccountId: AccountId) { + logger.debug("Pool account id: \(String(describing: poolBondedAccountId))") + + self.poolBondedAccountId = poolBondedAccountId + + provideYourPool() + } + + func didReceive(activePools: Set) { + logger.debug("Active pools: \(String(describing: activePools.count))") + + self.activePools = activePools + + provideState() + } + + func didReceive(eraCountdown: EraCountdown) { + logger.debug("Era countdown: \(String(describing: eraCountdown))") + + self.eraCountdown = eraCountdown + + provideState() + } + + func didReceive(price: PriceData?) { + priceData = price + + provideStakingInfo() + provideState() + } + + func didRecieve(claimableRewards: BigUInt?) { + logger.debug("Claimable rewards: \(String(describing: claimableRewards))") + + self.claimableRewards = claimableRewards + + provideState() + } + + func didReceive(totalRewards: TotalRewardItem?) { + logger.debug("Total rewards: \(String(describing: totalRewards))") + + self.totalRewards = totalRewards + + provideState() + } + + func didReceive(poolMetadata: Data?) { + logger.debug("Pool metadata: \(String(describing: String(data: poolMetadata ?? Data(), encoding: .utf8)))") + + self.poolMetadata = .defined(poolMetadata) + + provideYourPool() + } + + func didReceive(error: StakingNPoolsError) { + logger.error("Did receive error: \(error)") + + switch error { + case .stateSetup: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.setup() + } + case .subscription: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.remakeSubscriptions() + } + case .totalActiveStake: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryActiveStake() + } + case .stakingDuration: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryStakingDuration() + } + case .activePools: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryActivePools() + } + case .eraCountdown: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryEraCountdown() + } + case .claimableRewards: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryClaimableRewards() + } + case .totalRewards: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + if let filter = self?.totalRewardsFilter { + self?.interactor.setupTotalRewards(filter: filter) + } + } + } + } +} + +extension StakingNPoolsPresenter: Localizable { + func applyLocalization() { + if let view = view, view.isSetup { + updateView() + } + } +} diff --git a/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsProtocols.swift b/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsProtocols.swift new file mode 100644 index 0000000000..5150bd4f7f --- /dev/null +++ b/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsProtocols.swift @@ -0,0 +1,41 @@ +import Foundation +import BigInt + +protocol StakingNPoolsInteractorInputProtocol: AnyObject { + func setup() + func setupTotalRewards(filter: StakingRewardFiltersPeriod) + func remakeSubscriptions() + func retryActiveStake() + func retryStakingDuration() + func retryActivePools() + func retryEraCountdown() + func retryClaimableRewards() +} + +protocol StakingNPoolsInteractorOutputProtocol: AnyObject { + func didReceive(totalActiveStake: BigUInt) + func didReceive(minStake: BigUInt?) + func didReceive(activeEra: ActiveEraInfo?) + func didReceive(poolLedger: StakingLedger?) + func didReceive(poolNomination: Nomination?) + func didReceive(poolMember: NominationPools.PoolMember?) + func didReceive(bondedPool: NominationPools.BondedPool?) + func didReceive(subPools: NominationPools.SubPools?) + func didRecieve(claimableRewards: BigUInt?) + func didReceive(totalRewards: TotalRewardItem?) + func didReceive(poolBondedAccountId: AccountId) + func didReceive(poolMetadata: Data?) + func didReceive(activePools: Set) + func didReceive(duration: StakingDuration) + func didReceive(eraCountdown: EraCountdown) + func didReceive(price: PriceData?) + func didReceive(error: StakingNPoolsError) +} + +protocol StakingNPoolsWireframeProtocol: AlertPresentable, ErrorPresentable, AddressOptionsPresentable, + CommonRetryable { + func showStakeMore(from view: StakingMainViewProtocol?) + func showUnstake(from view: StakingMainViewProtocol?) + func showRedeem(from view: StakingMainViewProtocol?) + func showClaimRewards(from view: StakingMainViewProtocol?) +} diff --git a/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsWireframe.swift b/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsWireframe.swift new file mode 100644 index 0000000000..03da85722c --- /dev/null +++ b/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsWireframe.swift @@ -0,0 +1,47 @@ +import Foundation + +final class StakingNPoolsWireframe: StakingNPoolsWireframeProtocol { + let state: NPoolsStakingSharedStateProtocol + + init(state: NPoolsStakingSharedStateProtocol) { + self.state = state + } + + func showStakeMore(from view: StakingMainViewProtocol?) { + guard let stakeMoreView = NominationPoolBondMoreSetupViewFactory.createView(state: state) else { + return + } + let navigationController = ImportantFlowViewFactory.createNavigation(from: stakeMoreView.controller) + view?.controller.present(navigationController, animated: true, completion: nil) + } + + func showUnstake(from view: StakingMainViewProtocol?) { + guard let unstakeView = NPoolsUnstakeSetupViewFactory.createView(for: state) else { + return + } + + let navigationController = ImportantFlowViewFactory.createNavigation(from: unstakeView.controller) + + view?.controller.present(navigationController, animated: true) + } + + func showRedeem(from view: StakingMainViewProtocol?) { + guard let redeemView = NPoolsRedeemViewFactory.createView(for: state) else { + return + } + + let navigationController = ImportantFlowViewFactory.createNavigation(from: redeemView.controller) + + view?.controller.present(navigationController, animated: true) + } + + func showClaimRewards(from view: StakingMainViewProtocol?) { + guard let claimRewardsView = NPoolsClaimRewardsViewFactory.createView(for: state) else { + return + } + + let navigationController = ImportantFlowViewFactory.createNavigation(from: claimRewardsView.controller) + + view?.controller.present(navigationController, animated: true) + } +} diff --git a/novawallet/Modules/Staking/StakingMain/NominationPools/ViewModel/StakingNPoolsViewModelFactory+Alerts.swift b/novawallet/Modules/Staking/StakingMain/NominationPools/ViewModel/StakingNPoolsViewModelFactory+Alerts.swift new file mode 100644 index 0000000000..d153b10f11 --- /dev/null +++ b/novawallet/Modules/Staking/StakingMain/NominationPools/ViewModel/StakingNPoolsViewModelFactory+Alerts.swift @@ -0,0 +1,44 @@ +import Foundation + +extension StakingNPoolsViewModelFactory { + func createStakingAlerts( + for params: StakingNPoolsViewModelParams, + status: NominationViewStatus, + chainAsset: ChainAsset + ) -> [StakingAlert] { + [ + findRedeemUnbondedAlert(for: params, chainAsset: chainAsset), + findWaitingNextEraAlert(nominationStatus: status) + ].compactMap { $0 } + } + + private func findRedeemUnbondedAlert( + for params: StakingNPoolsViewModelParams, + chainAsset: ChainAsset + ) -> StakingAlert? { + guard + let era = params.activeEra?.index, + let poolMember = params.poolMember, + let subPools = params.subPools + else { return nil } + + let redeemableAmount = subPools.redeemableBalance(for: poolMember, in: era) + + guard redeemableAmount > 0 else { + return nil + } + + let localizedString = balanceViewModelFactory.amountFromValue( + redeemableAmount.decimal(precision: chainAsset.asset.precision) + ) + + return .redeemUnbonded(localizedString) + } + + private func findWaitingNextEraAlert(nominationStatus: NominationViewStatus) -> StakingAlert? { + if case NominationViewStatus.waiting = nominationStatus { + return .waitingNextEra + } + return nil + } +} diff --git a/novawallet/Modules/Staking/StakingMain/NominationPools/ViewModel/StakingNPoolsViewModelFactory.swift b/novawallet/Modules/Staking/StakingMain/NominationPools/ViewModel/StakingNPoolsViewModelFactory.swift new file mode 100644 index 0000000000..b5409b90e3 --- /dev/null +++ b/novawallet/Modules/Staking/StakingMain/NominationPools/ViewModel/StakingNPoolsViewModelFactory.swift @@ -0,0 +1,233 @@ +import Foundation +import SoraFoundation +import BigInt + +struct StakingNPoolsViewModelParams { + let poolMember: NominationPools.PoolMember? + let bondedPool: NominationPools.BondedPool? + let subPools: NominationPools.SubPools? + let poolLedger: StakingLedger? + let poolNomination: Nomination? + let activePools: Set? + let activeEra: ActiveEraInfo? + let eraCountdown: EraCountdownDisplayProtocol? + let totalRewards: TotalRewardItem? + let totalRewardsFilter: StakingRewardFiltersPeriod? + let claimableRewards: BigUInt? +} + +protocol StakingNPoolsViewModelFactoryProtocol { + func createState( + for params: StakingNPoolsViewModelParams, + chainAsset: ChainAsset, + price: PriceData? + ) -> StakingViewState +} + +final class StakingNPoolsViewModelFactory { + let balanceViewModelFactory: BalanceViewModelFactoryProtocol + + private let calendar = Calendar.current + + init(balanceViewModelFactory: BalanceViewModelFactoryProtocol) { + self.balanceViewModelFactory = balanceViewModelFactory + } + + private func createStakeViewModel( + for params: StakingNPoolsViewModelParams, + chainAsset: ChainAsset, + price: PriceData? + ) -> LocalizableResource? { + guard + let poolMember = params.poolMember, + let poolLedger = params.poolLedger, + let bondedPool = params.bondedPool else { + return nil + } + + let amount = NominationPools.pointsToBalance( + for: poolMember.points, + totalPoints: bondedPool.points, + poolBalance: poolLedger.active + ).decimal(precision: chainAsset.asset.precision) + + return balanceViewModelFactory.balanceFromPrice(amount, priceData: price) + } + + private func createNominationStatus(for params: StakingNPoolsViewModelParams) -> NominationViewStatus { + guard let activePools = params.activePools, let poolMember = params.poolMember else { + return .undefined + } + + guard poolMember.points > 0 else { + return .inactive + } + + guard !activePools.contains(poolMember.poolId) else { + return .active + } + + guard let nomination = params.poolNomination else { + return .inactive + } + + let poolState = Multistaking.NominationPoolState( + poolMember: poolMember, + era: params.activeEra, + ledger: params.poolLedger, + nomination: nomination, + bondedPool: params.bondedPool + ) + + guard let onchainState = Multistaking.DashboardItemOnchainState.from(nominationPoolState: poolState) else { + return .inactive + } + + switch onchainState { + case .active: + // we previously found that pool id still not in active pools list and not waiting + return .inactive + case .bonded: + return .inactive + case .waiting: + return .waiting(eraCountdown: params.eraCountdown, nominationEra: nomination.submittedIn) + } + } + + private func createNominationViewModel( + for params: StakingNPoolsViewModelParams, + status: NominationViewStatus, + chainAsset: ChainAsset, + price: PriceData? + ) -> LocalizableResource { + let localizedStakeViewModel = createStakeViewModel( + for: params, + chainAsset: chainAsset, + price: price + ) + + return LocalizableResource { locale in + let stakeViewModel = localizedStakeViewModel?.value(for: locale) + + return .init( + totalStakedAmount: stakeViewModel?.amount ?? "", + totalStakedPrice: stakeViewModel?.price ?? "", + status: status, + hasPrice: chainAsset.asset.priceId != nil + ) + } + } + + private func createUnbondingViewModel( + from params: StakingNPoolsViewModelParams, + chainAsset: ChainAsset + ) -> StakingUnbondingViewModel? { + guard let poolMember = params.poolMember, let subPools = params.subPools else { + return nil + } + + let poolsByEra = subPools.getPoolsByEra() + + let viewModels = poolMember + .unbondingEras + .sorted(by: { $0.key.value < $1.key.value }) + .map { unbondingKeyValue in + let eraIndex = unbondingKeyValue.key.value + let points = unbondingKeyValue.value.value + + let pool = poolsByEra[eraIndex] ?? subPools.noEra + + let unbondingAmount = NominationPools.pointsToBalance( + for: points, + totalPoints: pool.points, + poolBalance: pool.balance + ).decimal(precision: chainAsset.asset.precision) + + let unbondingAmountString = balanceViewModelFactory.amountFromValue(unbondingAmount) + + return StakingUnbondingItemViewModel( + amount: unbondingAmountString, + unbondingEra: eraIndex + ) + } + + return StakingUnbondingViewModel(eraCountdown: params.eraCountdown, items: viewModels) + } + + private func createRewardsViewModel( + from params: StakingNPoolsViewModelParams, + chainAsset: ChainAsset, + price: PriceData? + ) -> LocalizableResource { + let localizedTotalRewards = params.totalRewards.map { rewards in + balanceViewModelFactory.balanceFromPrice(rewards.amount.decimalValue, priceData: price) + } + + let localizedClaimableRewards = params.claimableRewards.map { rewards in + balanceViewModelFactory.balanceFromPrice( + rewards.decimal(precision: chainAsset.asset.precision), + priceData: price + ) + } + + let localizedFilter = params.totalRewardsFilter.map { $0.title(calendar: calendar) } + + let canClaimRewards = (params.claimableRewards ?? 0) > 0 + + return LocalizableResource { locale in + let totalRewards = localizedTotalRewards?.value(for: locale) + let claimableReward = localizedClaimableRewards?.value(for: locale) + let claimableRewardViewModel = claimableReward.map { + StakingRewardViewModel.ClaimableRewards(balance: $0, canClaim: canClaimRewards) + } + + let filter = localizedFilter?.value(for: locale) + + return StakingRewardViewModel( + totalRewards: totalRewards.map { .loaded(value: $0) } ?? .loading, + claimableRewards: claimableRewardViewModel.map { .loaded(value: $0) } ?? .loading, + graphics: R.image.imageStakingTypePool(), + filter: filter, + hasPrice: chainAsset.asset.hasPrice + ) + } + } +} + +extension StakingNPoolsViewModelFactory: StakingNPoolsViewModelFactoryProtocol { + func createState( + for params: StakingNPoolsViewModelParams, + chainAsset: ChainAsset, + price: PriceData? + ) -> StakingViewState { + let status = createNominationStatus(for: params) + + let nominationViewModel = createNominationViewModel( + for: params, + status: status, + chainAsset: chainAsset, + price: price + ) + + let unbondingViewModel = createUnbondingViewModel( + from: params, + chainAsset: chainAsset + ) + + let alerts = createStakingAlerts(for: params, status: status, chainAsset: chainAsset) + + let rewards = createRewardsViewModel( + from: params, + chainAsset: chainAsset, + price: price + ) + + return .nominator( + viewModel: nominationViewModel, + alerts: alerts, + reward: rewards, + unbondings: unbondingViewModel, + actions: [.stakeMore, .unstake] + ) + } +} diff --git a/novawallet/Modules/Staking/StakingMain/NominationPools/ViewModel/StakingNominationPoolsStatics.swift b/novawallet/Modules/Staking/StakingMain/NominationPools/ViewModel/StakingNominationPoolsStatics.swift new file mode 100644 index 0000000000..219b4fe17a --- /dev/null +++ b/novawallet/Modules/Staking/StakingMain/NominationPools/ViewModel/StakingNominationPoolsStatics.swift @@ -0,0 +1,9 @@ +import Foundation + +struct StakingNominationPoolsStatics: StakingMainStaticViewModelProtocol { + var canCancelUnbonding: Bool { false } + + func networkInfoTitle(for locale: Locale) -> String { + R.string.localizable.stakingPoolNetworkInfo(preferredLanguages: locale.rLanguages) + } +} diff --git a/novawallet/Modules/Staking/StakingMain/Parachain/StakingMainPresenterFactory+Parachain.swift b/novawallet/Modules/Staking/StakingMain/Parachain/StakingMainPresenterFactory+Parachain.swift index bb64e2c5e5..a53e7b1761 100644 --- a/novawallet/Modules/Staking/StakingMain/Parachain/StakingMainPresenterFactory+Parachain.swift +++ b/novawallet/Modules/Staking/StakingMain/Parachain/StakingMainPresenterFactory+Parachain.swift @@ -8,7 +8,9 @@ extension StakingMainPresenterFactory { for stakingOption: Multistaking.ChainAssetOption, view: StakingMainViewProtocol ) -> StakingParachainPresenter? { - let sharedState = createParachainSharedState(for: stakingOption) + guard let sharedState = try? sharedStateFactory.createParachain(for: stakingOption) else { + return nil + } // MARK: - Interactor @@ -44,45 +46,17 @@ extension StakingMainPresenterFactory { return presenter } - func createParachainInteractor(state: ParachainStakingSharedState) -> StakingParachainInteractor? { + func createParachainInteractor(state: ParachainStakingSharedStateProtocol) -> StakingParachainInteractor? { guard let currencyManager = CurrencyManager.shared else { return nil } - let chainRegistry = ChainRegistryFacade.sharedRegistry - let storageFacade = SubstrateDataStorageFacade.shared + let operationQueue = OperationManagerFacade.sharedDefaultQueue let operationManager = OperationManager(operationQueue: operationQueue) + let eventCenter = EventCenter.shared let logger = Logger.shared - let repositoryFactory = SubstrateRepositoryFactory() - let repository = repositoryFactory.createChainStorageItemRepository() - - let stakingAccountService = ParachainStaking.AccountSubscriptionService( - chainRegistry: chainRegistry, - repository: repository, - syncOperationManager: operationManager, - repositoryOperationManager: operationManager, - logger: logger - ) - - let stakingAssetService = ParachainStaking.StakingRemoteSubscriptionService( - chainRegistry: chainRegistry, - repository: repository, - syncOperationManager: operationManager, - repositoryOperationManager: operationManager, - logger: logger - ) - - let serviceFactory = ParachainStakingServiceFactory( - stakingProviderFactory: state.stakingLocalSubscriptionFactory, - chainRegisty: chainRegistry, - storageFacade: storageFacade, - eventCenter: eventCenter, - operationQueue: operationQueue, - logger: logger - ) - let networkInfoFactory = ParaStkNetworkInfoOperationFactory() let chainAsset = state.stakingOption.chainAsset @@ -104,15 +78,13 @@ extension StakingMainPresenterFactory { identityOperationFactory: IdentityOperationFactory(requestFactory: storageRequestFactory) ) + let applicationHandler = ApplicationHandler() + return StakingParachainInteractor( selectedWalletSettings: SelectedWalletSettings.shared, sharedState: state, - chainRegistry: ChainRegistryFacade.sharedRegistry, - stakingAssetSubscriptionService: stakingAssetService, - stakingAccountSubscriptionService: stakingAccountService, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, - stakingServiceFactory: serviceFactory, networkInfoFactory: networkInfoFactory, durationOperationFactory: durationFactory, scheduledRequestsFactory: ParachainStaking.ScheduledRequestsQueryFactory(operationQueue: operationQueue), @@ -126,33 +98,4 @@ extension StakingMainPresenterFactory { logger: logger ) } - - func createParachainSharedState( - for stakingOption: Multistaking.ChainAssetOption - ) -> ParachainStakingSharedState { - let storageFacade = SubstrateDataStorageFacade.shared - - let stakingLocalSubscriptionFactory = ParachainStakingLocalSubscriptionFactory( - chainRegistry: ChainRegistryFacade.sharedRegistry, - storageFacade: storageFacade, - operationManager: OperationManagerFacade.sharedManager, - logger: Logger.shared - ) - - let generalLocalSubscriptionFactory = GeneralStorageSubscriptionFactory( - chainRegistry: ChainRegistryFacade.sharedRegistry, - storageFacade: storageFacade, - operationManager: OperationManagerFacade.sharedManager, - logger: Logger.shared - ) - - return ParachainStakingSharedState( - stakingOption: stakingOption, - collatorService: nil, - rewardCalculationService: nil, - blockTimeService: nil, - stakingLocalSubscriptionFactory: stakingLocalSubscriptionFactory, - generalLocalSubscriptionFactory: generalLocalSubscriptionFactory - ) - } } diff --git a/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainInteractor+InputProtocol.swift b/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainInteractor+InputProtocol.swift index 86bffaac0d..0171c9fda7 100644 --- a/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainInteractor+InputProtocol.swift +++ b/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainInteractor+InputProtocol.swift @@ -3,8 +3,32 @@ import SoraFoundation extension StakingParachainInteractor: StakingParachainInteractorInputProtocol { func setup() { - createInitialServices() - continueSetup() + setupSelectedAccount() + setupSharedState() + + provideSelectedChainAsset() + provideSelectedAccount() + + performBlockNumberSubscription() + performRoundInfoSubscription() + performPriceSubscription() + performAssetBalanceSubscription() + performDelegatorSubscription() + performTotalRewardSubscription() + performYieldBoostTasksSubscription() + + let collatorService = sharedState.collatorService + let rewardCalculationService = sharedState.rewardCalculationService + let blockTimeService = sharedState.blockTimeService + + provideRewardCalculator(from: rewardCalculationService) + provideSelectedCollatorsInfo(from: collatorService) + provideNetworkInfo(for: collatorService, rewardService: rewardCalculationService) + provideDurationInfo(for: blockTimeService) + + eventCenter.add(observer: self, dispatchIn: .main) + + applicationHandler.delegate = self } func fetchDelegations(for collators: [AccountId]) { @@ -23,12 +47,8 @@ extension StakingParachainInteractor: StakingParachainInteractorInputProtocol { return } - guard - let collatorService = sharedState.collatorService, - let rewardService = sharedState.rewardCalculationService else { - presenter?.didReceiveError(CommonError.dataCorruption) - return - } + let collatorService = sharedState.collatorService + let rewardService = sharedState.rewardCalculationService let wrapper = collatorsOperationFactory.selectedCollatorsInfoOperation( for: collators, @@ -81,11 +101,8 @@ extension StakingParachainInteractor: StakingParachainInteractorInputProtocol { extension StakingParachainInteractor: EventVisitorProtocol { func processEraStakersInfoChanged(event _: EraStakersInfoChanged) { - guard - let collatorService = sharedState.collatorService, - let rewardCalculationService = sharedState.rewardCalculationService else { - return - } + let collatorService = sharedState.collatorService + let rewardCalculationService = sharedState.rewardCalculationService provideSelectedCollatorsInfo(from: collatorService) provideRewardCalculator(from: rewardCalculationService) @@ -93,11 +110,7 @@ extension StakingParachainInteractor: EventVisitorProtocol { } func processBlockTimeChanged(event _: BlockTimeChanged) { - guard let blockTimeService = sharedState.blockTimeService else { - return - } - - provideDurationInfo(for: blockTimeService) + provideDurationInfo(for: sharedState.blockTimeService) } } diff --git a/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainInteractor+Subscription.swift b/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainInteractor+Subscription.swift index 8ac8e05b2b..1b985ec65c 100644 --- a/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainInteractor+Subscription.swift +++ b/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainInteractor+Subscription.swift @@ -1,62 +1,6 @@ import Foundation extension StakingParachainInteractor { - func clearChainRemoteSubscription(for chainId: ChainModel.Id) { - if let chainSubscriptionId = chainSubscriptionId { - stakingAssetSubscriptionService.detachFromGlobalData( - for: chainSubscriptionId, - chainId: chainId, - queue: nil, - closure: nil - ) - - self.chainSubscriptionId = nil - } - } - - func setupChainRemoteSubscription() { - let chainId = selectedChainAsset.chain.chainId - - chainSubscriptionId = stakingAssetSubscriptionService.attachToGlobalData( - for: chainId, - queue: nil, - closure: nil - ) - } - - func clearAccountRemoteSubscription() { - let chainId = selectedChainAsset.chain.chainId - - if - let accountSubscriptionId = accountSubscriptionId, - let accountId = selectedAccount?.chainAccount.accountId { - stakingAccountSubscriptionService.detachFromAccountData( - for: accountSubscriptionId, - chainId: chainId, - accountId: accountId, - queue: nil, - closure: nil - ) - - self.accountSubscriptionId = nil - } - } - - func setupAccountRemoteSubscription() { - let chainId = selectedChainAsset.chain.chainId - - guard let accountId = selectedAccount?.chainAccount.accountId else { - return - } - - accountSubscriptionId = stakingAccountSubscriptionService.attachToAccountData( - for: chainId, - accountId: accountId, - queue: nil, - closure: nil - ) - } - func performPriceSubscription() { guard let priceId = selectedChainAsset.asset.priceId else { presenter?.didReceivePrice(nil) diff --git a/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainInteractor.swift b/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainInteractor.swift index cc05800e1b..21efab0816 100644 --- a/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainInteractor.swift +++ b/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainInteractor.swift @@ -13,16 +13,16 @@ final class StakingParachainInteractor: AnyProviderAutoCleaning, AnyCancellableC sharedState.generalLocalSubscriptionFactory } + var chainRegistry: ChainRegistryProtocol { + sharedState.chainRegistry + } + let selectedWalletSettings: SelectedWalletSettings - let sharedState: ParachainStakingSharedState - let chainRegistry: ChainRegistryProtocol - let stakingAssetSubscriptionService: StakingRemoteSubscriptionServiceProtocol - let stakingAccountSubscriptionService: ParachainStakingAccountSubscriptionServiceProtocol + let sharedState: ParachainStakingSharedStateProtocol let scheduledRequestsFactory: ParaStkScheduledRequestsQueryFactoryProtocol let collatorsOperationFactory: ParaStkCollatorsOperationFactoryProtocol let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol - let stakingServiceFactory: ParachainStakingServiceFactoryProtocol let networkInfoFactory: ParaStkNetworkInfoOperationFactoryProtocol let durationOperationFactory: ParaStkDurationOperationFactoryProtocol let yieldBoostSupport: ParaStkYieldBoostSupportProtocol @@ -32,8 +32,6 @@ final class StakingParachainInteractor: AnyProviderAutoCleaning, AnyCancellableC let applicationHandler: ApplicationHandlerProtocol let logger: LoggerProtocol? - var chainSubscriptionId: UUID? - var accountSubscriptionId: UUID? var collatorsInfoCancellable: CancellableCall? var rewardCalculatorCancellable: CancellableCall? var networkInfoCancellable: CancellableCall? @@ -55,13 +53,9 @@ final class StakingParachainInteractor: AnyProviderAutoCleaning, AnyCancellableC init( selectedWalletSettings: SelectedWalletSettings, - sharedState: ParachainStakingSharedState, - chainRegistry: ChainRegistryProtocol, - stakingAssetSubscriptionService: StakingRemoteSubscriptionServiceProtocol, - stakingAccountSubscriptionService: ParachainStakingAccountSubscriptionServiceProtocol, + sharedState: ParachainStakingSharedStateProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, - stakingServiceFactory: ParachainStakingServiceFactoryProtocol, networkInfoFactory: ParaStkNetworkInfoOperationFactoryProtocol, durationOperationFactory: ParaStkDurationOperationFactoryProtocol, scheduledRequestsFactory: ParaStkScheduledRequestsQueryFactoryProtocol, @@ -76,12 +70,8 @@ final class StakingParachainInteractor: AnyProviderAutoCleaning, AnyCancellableC ) { self.selectedWalletSettings = selectedWalletSettings self.sharedState = sharedState - self.chainRegistry = chainRegistry - self.stakingAssetSubscriptionService = stakingAssetSubscriptionService - self.stakingAccountSubscriptionService = stakingAccountSubscriptionService self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory - self.stakingServiceFactory = stakingServiceFactory self.networkInfoFactory = networkInfoFactory self.durationOperationFactory = durationOperationFactory self.scheduledRequestsFactory = scheduledRequestsFactory @@ -96,14 +86,9 @@ final class StakingParachainInteractor: AnyProviderAutoCleaning, AnyCancellableC } deinit { - clearChainRemoteSubscription(for: selectedChainAsset.chain.chainId) - - clearAccountRemoteSubscription() clearCancellable() - sharedState.collatorService?.throttle() - sharedState.rewardCalculationService?.throttle() - sharedState.blockTimeService?.throttle() + sharedState.throttle() } func clearCancellable() { @@ -122,69 +107,8 @@ final class StakingParachainInteractor: AnyProviderAutoCleaning, AnyCancellableC ) } - func createInitialServices() { - let chainAsset = sharedState.stakingOption.chainAsset - - do { - let chainId = chainAsset.chain.chainId - let collatorsService = try stakingServiceFactory.createSelectedCollatorsService( - for: chainId - ) - - let rewardCalculatorService = try stakingServiceFactory.createRewardCalculatorService( - for: chainId, - stakingType: chainAsset.asset.stakings?.first ?? .unsupported, - assetPrecision: Int16(chainAsset.asset.precision), - collatorService: collatorsService - ) - - let blockTimeService = try stakingServiceFactory.createBlockTimeService( - for: chainId - ) - - sharedState.replaceCollatorService(collatorsService) - sharedState.replaceRewardCalculatorService(rewardCalculatorService) - sharedState.replaceBlockTimeService(blockTimeService) - } catch { - logger?.error("Couldn't create shared state") - presenter?.didReceiveError(error) - } - } - - func continueSetup() { - setupSelectedAccount() - setupChainRemoteSubscription() - setupAccountRemoteSubscription() - - sharedState.collatorService?.setup() - sharedState.rewardCalculationService?.setup() - sharedState.blockTimeService?.setup() - - provideSelectedChainAsset() - provideSelectedAccount() - - guard - let collatorService = sharedState.collatorService, - let rewardCalculationService = sharedState.rewardCalculationService, - let blockTimeService = sharedState.blockTimeService else { - return - } - - performBlockNumberSubscription() - performRoundInfoSubscription() - performPriceSubscription() - performAssetBalanceSubscription() - performDelegatorSubscription() - performTotalRewardSubscription() - performYieldBoostTasksSubscription() - - provideRewardCalculator(from: rewardCalculationService) - provideSelectedCollatorsInfo(from: collatorService) - provideNetworkInfo(for: collatorService, rewardService: rewardCalculationService) - provideDurationInfo(for: blockTimeService) - - eventCenter.add(observer: self, dispatchIn: .main) - - applicationHandler.delegate = self + func setupSharedState() { + let accountId = selectedAccount?.chainAccount.accountId + sharedState.setup(for: accountId) } } diff --git a/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainPresenter.swift b/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainPresenter.swift index b582b0d3f3..86093b08b3 100644 --- a/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainPresenter.swift +++ b/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainPresenter.swift @@ -44,11 +44,13 @@ final class StakingParachainPresenter { from: networkInfo, duration: optCommonData?.stakingDuration, chainAsset: chainAsset, - price: optCommonData?.price + price: optCommonData?.price, + locale: view?.selectedLocale ?? Locale.current ) + view?.didRecieveNetworkStakingInfo(viewModel: viewModel) } else { - view?.didRecieveNetworkStakingInfo(viewModel: nil) + view?.didRecieveNetworkStakingInfo(viewModel: NetworkStakingInfoViewModel.allLoading) } } @@ -138,47 +140,20 @@ extension StakingParachainPresenter: StakingMainChildPresenterProtocol { interactor.setup() } - func performMainAction() { - wireframe.showStakeTokens( - from: view, - initialDelegator: nil, - initialScheduledRequests: nil, - delegationIdentities: nil - ) - } - - func performRewardInfoAction() { - guard - let state = stateMachine.viewState(using: { (state: ParachainStaking.BaseState) in state }), - let rewardCalculator = state.commonData.calculatorEngine, - let asset = state.commonData.chainAsset?.asset else { - return - } - - let maxReward = rewardCalculator.calculateMaxReturn(for: .year) - let avgReward = rewardCalculator.calculateAvgReturn(for: .year) - - wireframe.showRewardDetails(from: view, maxReward: maxReward, avgReward: avgReward, symbol: asset.symbol) - } - - func performChangeValidatorsAction() { + func performStakeMoreAction() { wireframe.showYourCollators(from: view) } - func performSetupValidatorsForBondedAction() { - // no support at this point + func performRedeemAction() { + wireframe.showRedeemTokens(from: view) } - func performRebag() { - // no support at this point + func performClaimRewards() { + // not needed action for parachain staking } - func performStakeMoreAction() { - wireframe.showYourCollators(from: view) - } - - func performRedeemAction() { - wireframe.showRedeemTokens(from: view) + func performSelectedEntityAction() { + // no support for selected entity } func performRebondAction() { @@ -234,6 +209,17 @@ extension StakingParachainPresenter: StakingMainChildPresenterProtocol { } } + func performAlertAction(_ alert: StakingAlert) { + switch alert { + case .nominatorLowStake: + performStakeMoreAction() + case .redeemUnbonded: + performRedeemAction() + case .rebag, .waitingNextEra, .bondedSetValidators, .nominatorChangeValidators, .nominatorAllOversubscribed: + break + } + } + func selectPeriod(_ filter: StakingRewardFiltersPeriod) { stateMachine.state.process(totalRewardFilter: filter) interactor.update(totalRewardFilter: filter) diff --git a/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainProtocols.swift b/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainProtocols.swift index ba0e7679b6..14ae26795d 100644 --- a/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainProtocols.swift +++ b/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainProtocols.swift @@ -28,13 +28,6 @@ protocol StakingParachainInteractorOutputProtocol: AnyObject { } protocol StakingParachainWireframeProtocol: AlertPresentable, ErrorPresentable, ParachainStakingErrorPresentable { - func showRewardDetails( - from view: ControllerBackedProtocol?, - maxReward: Decimal, - avgReward: Decimal, - symbol: String - ) - func showStakeTokens( from view: ControllerBackedProtocol?, initialDelegator: ParachainStaking.Delegator?, diff --git a/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainWireframe.swift b/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainWireframe.swift index 5f645c1e8c..ba4983da2b 100644 --- a/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainWireframe.swift +++ b/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainWireframe.swift @@ -2,29 +2,14 @@ import Foundation import SoraFoundation final class StakingParachainWireframe { - let state: ParachainStakingSharedState + let state: ParachainStakingSharedStateProtocol - init(state: ParachainStakingSharedState) { + init(state: ParachainStakingSharedStateProtocol) { self.state = state } } extension StakingParachainWireframe: StakingParachainWireframeProtocol { - func showRewardDetails( - from view: ControllerBackedProtocol?, - maxReward: Decimal, - avgReward: Decimal, - symbol: String - ) { - let infoVew = ModalInfoFactory.createParaStkRewardDetails( - for: maxReward, - avgReward: avgReward, - symbol: symbol - ) - - view?.controller.present(infoVew, animated: true, completion: nil) - } - func showStakeTokens( from view: ControllerBackedProtocol?, initialDelegator: ParachainStaking.Delegator?, diff --git a/novawallet/Modules/Staking/StakingMain/Parachain/StateMachine/ParaStkStateMachineProtocols.swift b/novawallet/Modules/Staking/StakingMain/Parachain/StateMachine/ParaStkStateMachineProtocols.swift index 072e3cb56d..d7204da28e 100644 --- a/novawallet/Modules/Staking/StakingMain/Parachain/StateMachine/ParaStkStateMachineProtocols.swift +++ b/novawallet/Modules/Staking/StakingMain/Parachain/StateMachine/ParaStkStateMachineProtocols.swift @@ -2,7 +2,6 @@ import Foundation protocol ParaStkStateVisitorProtocol { func visit(state: ParachainStaking.InitState) - func visit(state: ParachainStaking.NoStakingState) func visit(state: ParachainStaking.DelegatorState) } diff --git a/novawallet/Modules/Staking/StakingMain/Parachain/StateMachine/States/ParachainStakingDelegatorState.swift b/novawallet/Modules/Staking/StakingMain/Parachain/StateMachine/States/ParachainStakingDelegatorState.swift index c9ac0b6475..89ecfc4a8d 100644 --- a/novawallet/Modules/Staking/StakingMain/Parachain/StateMachine/States/ParachainStakingDelegatorState.swift +++ b/novawallet/Modules/Staking/StakingMain/Parachain/StateMachine/States/ParachainStakingDelegatorState.swift @@ -30,7 +30,7 @@ extension ParachainStaking { stateMachine?.transit(to: self) } else { - let noStakingState = ParachainStaking.NoStakingState( + let noStakingState = ParachainStaking.InitState( stateMachine: stateMachine, commonData: commonData ) diff --git a/novawallet/Modules/Staking/StakingMain/Parachain/StateMachine/States/ParachainStakingInitState.swift b/novawallet/Modules/Staking/StakingMain/Parachain/StateMachine/States/ParachainStakingInitState.swift index 6c471d5eae..5ac1994eda 100644 --- a/novawallet/Modules/Staking/StakingMain/Parachain/StateMachine/States/ParachainStakingInitState.swift +++ b/novawallet/Modules/Staking/StakingMain/Parachain/StateMachine/States/ParachainStakingInitState.swift @@ -18,10 +18,7 @@ extension ParachainStaking { stateMachine?.transit(to: delegatorState) } else { - let noStakingState = ParachainStaking.NoStakingState( - stateMachine: stateMachine, - commonData: commonData - ) + let noStakingState = self stateMachine?.transit(to: noStakingState) } diff --git a/novawallet/Modules/Staking/StakingMain/Parachain/StateMachine/States/ParachainStakingNoStakingState.swift b/novawallet/Modules/Staking/StakingMain/Parachain/StateMachine/States/ParachainStakingNoStakingState.swift deleted file mode 100644 index 8440b4987d..0000000000 --- a/novawallet/Modules/Staking/StakingMain/Parachain/StateMachine/States/ParachainStakingNoStakingState.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation - -extension ParachainStaking { - final class NoStakingState: ParachainStaking.BaseState { - override func accept(visitor: ParaStkStateVisitorProtocol) { - visitor.visit(state: self) - } - - override func process(delegatorState: ParachainStaking.Delegator?) { - if let delegatorState = delegatorState { - let delegatorState = ParachainStaking.DelegatorState( - stateMachine: stateMachine, - commonData: commonData, - delegatorState: delegatorState, - scheduledRequests: nil, - delegations: nil - ) - - stateMachine?.transit(to: delegatorState) - } else { - stateMachine?.transit(to: self) - } - } - } -} diff --git a/novawallet/Modules/Staking/StakingMain/Parachain/ViewModel/ParaStkNetworkInfoViewModelFactory.swift b/novawallet/Modules/Staking/StakingMain/Parachain/ViewModel/ParaStkNetworkInfoViewModelFactory.swift index 0afb721b8f..ec7a8fe53a 100644 --- a/novawallet/Modules/Staking/StakingMain/Parachain/ViewModel/ParaStkNetworkInfoViewModelFactory.swift +++ b/novawallet/Modules/Staking/StakingMain/Parachain/ViewModel/ParaStkNetworkInfoViewModelFactory.swift @@ -7,8 +7,9 @@ protocol ParaStkNetworkInfoViewModelFactoryProtocol { from model: ParachainStaking.NetworkInfo, duration: ParachainStakingDuration?, chainAsset: ChainAsset, - price: PriceData? - ) -> LocalizableResource + price: PriceData?, + locale: Locale + ) -> NetworkStakingInfoViewModel } extension ParachainStaking { @@ -98,47 +99,46 @@ extension ParachainStaking { from model: ParachainStaking.NetworkInfo, duration: ParachainStakingDuration?, chainAsset: ChainAsset, - price: PriceData? - ) -> LocalizableResource { + price: PriceData?, + locale: Locale + ) -> NetworkStakingInfoViewModel { let assetDisplayInfo = chainAsset.assetDisplayInfo let balanceViewModelFactory = BalanceViewModelFactory( targetAssetInfo: assetDisplayInfo, priceAssetInfoFactory: priceAssetInfoFactory ) - let localizedTotalStake = createTotalStakeViewModel( + let totalStake = createTotalStakeViewModel( with: model, displayInfo: assetDisplayInfo, priceData: price, balanceViewModelFactory: balanceViewModelFactory - ) + ).value(for: locale) - let localizedMinimalStake = createMinimalStakeViewModel( + let minimalStake = createMinimalStakeViewModel( with: model, displayInfo: assetDisplayInfo, priceData: price, balanceViewModelFactory: balanceViewModelFactory - ) + ).value(for: locale) - let nominatorsCount = createActiveNominatorsViewModel(with: model) + let nominatorsCount = createActiveNominatorsViewModel(with: model).value(for: locale) - let localizedUnstakingPeriod = createUnstakingPeriodViewModel( + let unstakingPeriod = createUnstakingPeriodViewModel( duration: duration + ).value(for: locale) + + let stakingPeriod = R.string.localizable.stakingNetworkInfoStakingPeriodValue( + preferredLanguages: locale.rLanguages ) - return LocalizableResource { locale in - let stakingPeriod = R.string.localizable.stakingNetworkInfoStakingPeriodValue( - preferredLanguages: locale.rLanguages - ) - - return NetworkStakingInfoViewModel( - totalStake: localizedTotalStake.value(for: locale), - minimalStake: localizedMinimalStake.value(for: locale), - activeNominators: nominatorsCount.value(for: locale), - stakingPeriod: stakingPeriod, - lockUpPeriod: localizedUnstakingPeriod.value(for: locale) - ) - } + return .init( + totalStake: .loaded(value: totalStake), + minimalStake: .loaded(value: minimalStake), + activeNominators: .loaded(value: nominatorsCount), + stakingPeriod: .loaded(value: stakingPeriod), + lockUpPeriod: .loaded(value: unstakingPeriod) + ) } } } diff --git a/novawallet/Modules/Staking/StakingMain/Parachain/ViewModel/ParaStkStateViewModelFactory.swift b/novawallet/Modules/Staking/StakingMain/Parachain/ViewModel/ParaStkStateViewModelFactory.swift index b42cc1718d..85bd108346 100644 --- a/novawallet/Modules/Staking/StakingMain/Parachain/ViewModel/ParaStkStateViewModelFactory.swift +++ b/novawallet/Modules/Staking/StakingMain/Parachain/ViewModel/ParaStkStateViewModelFactory.swift @@ -1,5 +1,6 @@ import Foundation import SoraFoundation +import BigInt protocol ParaStkStateViewModelFactoryProtocol { func createViewModel(from state: ParaStkStateProtocol) -> StakingViewState @@ -15,13 +16,18 @@ final class ParaStkStateViewModelFactory { } private func createDelegationStatus( - for collatorStatuses: [ParaStkDelegationStatus]?, + for activeStake: BigUInt, + collatorStatuses: [ParaStkDelegationStatus]?, commonData: ParachainStaking.CommonData ) -> NominationViewStatus { guard let statuses = collatorStatuses, let roundInfo = commonData.roundInfo else { return .undefined } + guard activeStake > 0 else { + return .inactive + } + if statuses.contains(where: { $0 == .rewarded }) { return .active } else if statuses.contains(where: { $0 == .pending }) { @@ -34,29 +40,6 @@ final class ParaStkStateViewModelFactory { } } - private func createEstimationViewModel( - chainAsset: ChainAsset, - commonData: ParachainStaking.CommonData - ) throws -> StakingEstimationViewModel { - guard let calculator = commonData.calculatorEngine else { - return StakingEstimationViewModel(tokenSymbol: chainAsset.asset.symbol, reward: nil) - } - - let monthlyReturn = calculator.calculateMaxReturn(for: .month) - let yearlyReturn = calculator.calculateMaxReturn(for: .year) - - let percentageFormatter = NumberFormatter.percentBase.localizableResource() - - let reward = LocalizableResource { locale in - PeriodRewardViewModel( - monthly: percentageFormatter.value(for: locale).stringFromDecimal(monthlyReturn) ?? "", - yearly: percentageFormatter.value(for: locale).stringFromDecimal(yearlyReturn) ?? "" - ) - } - - return StakingEstimationViewModel(tokenSymbol: chainAsset.asset.symbol, reward: reward) - } - private func createDelegationViewModel( for chainAsset: ChainAsset, commonData: ParachainStaking.CommonData, @@ -143,26 +126,22 @@ final class ParaStkStateViewModelFactory { calendar: self.calendar ) }?.value(for: locale) - if let price = reward.price { - return StakingRewardViewModel( - amount: .loaded(reward.amount), - price: .loaded(price), - filter: filter - ) - } else { - return StakingRewardViewModel( - amount: .loaded(reward.amount), - price: nil, - filter: filter - ) - } + return StakingRewardViewModel( + totalRewards: .loaded(value: reward), + claimableRewards: nil, + graphics: R.image.imageStakingTypeDirect(), + filter: filter, + hasPrice: chainAsset.asset.hasPrice + ) } } else { return LocalizableResource { _ in StakingRewardViewModel( - amount: .loading, - price: .loading, - filter: nil + totalRewards: .loading, + claimableRewards: nil, + graphics: R.image.imageStakingTypeDirect(), + filter: nil, + hasPrice: chainAsset.asset.hasPrice ) } } @@ -215,23 +194,6 @@ extension ParaStkStateViewModelFactory: ParaStkStateVisitorProtocol { lastViewModel = .undefined } - func visit(state: ParachainStaking.NoStakingState) { - guard let chainAsset = state.commonData.chainAsset else { - lastViewModel = .undefined - return - } - - guard let rewardViewModel = try? createEstimationViewModel( - chainAsset: chainAsset, - commonData: state.commonData - ) else { - lastViewModel = .undefined - return - } - - lastViewModel = .noStash(viewModel: rewardViewModel, alerts: []) - } - func visit(state: ParachainStaking.DelegatorState) { guard let chainAsset = state.commonData.chainAsset, @@ -250,7 +212,8 @@ extension ParaStkStateViewModelFactory: ParaStkStateVisitorProtocol { } let delegationStatus = createDelegationStatus( - for: collatorsStatuses, + for: state.delegatorState.staked, + collatorStatuses: collatorsStatuses, commonData: state.commonData ) diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingMainPresenterFactory+Relaychain.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingMainPresenterFactory+Relaychain.swift index d3157b75e1..62ea1ec070 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingMainPresenterFactory+Relaychain.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingMainPresenterFactory+Relaychain.swift @@ -7,15 +7,14 @@ import SoraKeystore extension StakingMainPresenterFactory { func createRelaychainPresenter( for stakingOption: Multistaking.ChainAssetOption, - view: StakingMainViewProtocol, - consensus: ConsensusType + view: StakingMainViewProtocol ) -> StakingRelaychainPresenter? { - let sharedState = createRelaychainSharedState(for: stakingOption, consensus: consensus) - // MARK: - Interactor - guard let interactor = createRelaychainInteractor(state: sharedState), - let currencyManager = CurrencyManager.shared else { + guard + let sharedState = try? sharedStateFactory.createRelaychain(for: stakingOption), + let interactor = createRelaychainInteractor(state: sharedState), + let currencyManager = CurrencyManager.shared else { return nil } @@ -56,13 +55,13 @@ extension StakingMainPresenterFactory { return presenter } - // swiftlint:disable:next function_body_length func createRelaychainInteractor( - state: StakingSharedState + state: RelaychainStakingSharedStateProtocol ) -> StakingRelaychainInteractor? { guard let currencyManager = CurrencyManager.shared else { return nil } + let operationManager = OperationManagerFacade.sharedManager let logger = Logger.shared @@ -72,58 +71,23 @@ extension StakingMainPresenterFactory { logger: logger ) - let substrateRepositoryFactory = SubstrateRepositoryFactory( - storageFacade: SubstrateDataStorageFacade.shared - ) - - let chainItemRepository = substrateRepositoryFactory.createChainStorageItemRepository() - - let stakingRemoteSubscriptionService = StakingRemoteSubscriptionService( - chainRegistry: ChainRegistryFacade.sharedRegistry, - repository: chainItemRepository, - syncOperationManager: operationManager, - repositoryOperationManager: operationManager, - logger: logger - ) - - let serviceFactory = StakingServiceFactory( - chainRegisty: ChainRegistryFacade.sharedRegistry, - storageFacade: SubstrateDataStorageFacade.shared, - eventCenter: EventCenter.shared, - operationQueue: OperationManagerFacade.sharedDefaultQueue, - logger: logger + let networkInfoFactory = state.createNetworkInfoOperationFactory( + for: OperationManagerFacade.sharedDefaultQueue ) - let substrateDataProviderFactory = SubstrateDataProviderFactory( - facade: SubstrateDataStorageFacade.shared, - operationManager: operationManager - ) - - let childSubscriptionFactory = ChildSubscriptionFactory( - storageFacade: SubstrateDataStorageFacade.shared, - operationManager: operationManager, - eventCenter: EventCenter.shared, - logger: logger - ) - - let stakingAccountUpdatingService = StakingAccountUpdatingService( - chainRegistry: ChainRegistryFacade.sharedRegistry, - substrateRepositoryFactory: substrateRepositoryFactory, - substrateDataProviderFactory: substrateDataProviderFactory, - childSubscriptionFactory: childSubscriptionFactory, - operationQueue: OperationManagerFacade.sharedDefaultQueue + let eraCountdownFactory = state.createEraCountdownOperationFactory( + for: OperationManagerFacade.sharedDefaultQueue ) return StakingRelaychainInteractor( selectedWalletSettings: SelectedWalletSettings.shared, sharedState: state, chainRegistry: ChainRegistryFacade.sharedRegistry, - stakingRemoteSubscriptionService: stakingRemoteSubscriptionService, - stakingAccountUpdatingService: stakingAccountUpdatingService, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, - stakingServiceFactory: serviceFactory, accountProviderFactory: accountProviderFactory, + networkInfoOperationFactory: networkInfoFactory, + eraCountdownOperationFactory: eraCountdownFactory, eventCenter: EventCenter.shared, operationManager: operationManager, applicationHandler: applicationHandler, @@ -131,27 +95,4 @@ extension StakingMainPresenterFactory { logger: logger ) } - - private func createRelaychainSharedState( - for stakingOption: Multistaking.ChainAssetOption, - consensus: ConsensusType - ) -> StakingSharedState { - let storageFacade = SubstrateDataStorageFacade.shared - - let stakingLocalSubscriptionFactory = StakingLocalSubscriptionFactory( - chainRegistry: ChainRegistryFacade.sharedRegistry, - storageFacade: storageFacade, - operationManager: OperationManagerFacade.sharedManager, - logger: Logger.shared - ) - - return StakingSharedState( - consensus: consensus, - stakingOption: stakingOption, - eraValidatorService: nil, - rewardCalculationService: nil, - blockTimeService: nil, - stakingLocalSubscriptionFactory: stakingLocalSubscriptionFactory - ) - } } diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainInteractor+InputProtocol.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainInteractor+InputProtocol.swift index 8122f05cc7..f9fce84c4e 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainInteractor+InputProtocol.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainInteractor+InputProtocol.swift @@ -2,81 +2,45 @@ import Foundation import SoraFoundation extension StakingRelaychainInteractor: StakingRelaychainInteractorInputProtocol { - private func continueSetup() { - setupSelectedAccountAndChainAsset() - setupChainRemoteSubscription() - setupAccountRemoteSubscription() - - sharedState.eraValidatorService?.setup() - sharedState.rewardCalculationService?.setup() - sharedState.blockTimeService?.setup() - - provideNewChain() - provideSelectedAccount() - - guard - let chainId = selectedChainAsset?.chain.chainId, - let runtimeService = chainRegistry.getRuntimeProvider(for: chainId), - let eraValidatorService = sharedState.eraValidatorService, - let rewardCalculationService = sharedState.rewardCalculationService else { - return - } + func setup() { + do { + setupSelectedAccountAndChainAsset() - provideMaxNominatorsPerValidator(from: runtimeService) + try sharedState.setup(for: selectedAccount?.accountId) - performPriceSubscription() - performAccountInfoSubscription() - performStashControllerSubscription() - performNominatorLimitsSubscription() - performBagListParamsSubscription() + provideNewChain() + provideSelectedAccount() - provideRewardCalculator(from: rewardCalculationService) - provideEraStakersInfo(from: eraValidatorService) + guard + let chainId = selectedChainAsset?.chain.chainId, + let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + return + } - provideNetworkStakingInfo() + let eraValidatorService = sharedState.eraValidatorService + let rewardCalculationService = sharedState.rewardCalculatorService - eventCenter.add(observer: self, dispatchIn: .main) + provideMaxNominatorsPerValidator(from: runtimeService) - applicationHandler.delegate = self - } + performPriceSubscription() + performAccountInfoSubscription() + performStashControllerSubscription() + performNominatorLimitsSubscription() + performBagListParamsSubscription() - private func createInitialServices() { - let chainAsset = stakingOption.chainAsset + provideRewardCalculator(from: rewardCalculationService) + provideEraStakersInfo(from: eraValidatorService) - do { - let blockTimeService = try stakingServiceFactory.createBlockTimeService( - for: chainAsset.chain.chainId, - consensus: sharedState.consensus - ) + provideNetworkStakingInfo() - sharedState.replaceBlockTimeService(blockTimeService) + eventCenter.add(observer: self, dispatchIn: .main) - let eraValidatorService = try stakingServiceFactory.createEraValidatorService( - for: chainAsset.chain.chainId - ) - - let stakingDurationFactory = try sharedState.createStakingDurationOperationFactory(for: chainAsset.chain) - - let rewardCalculatorService = try stakingServiceFactory.createRewardCalculatorService( - for: chainAsset, - stakingType: chainAsset.asset.stakings?.first ?? .unsupported, - stakingLocalSubscriptionFactory: sharedState.stakingLocalSubscriptionFactory, - stakingDurationFactory: stakingDurationFactory, - validatorService: eraValidatorService - ) - - sharedState.replaceEraValidatorService(eraValidatorService) - sharedState.replaceRewardCalculatorService(rewardCalculatorService) + applicationHandler.delegate = self } catch { - logger?.error("Couldn't create shared state") + logger?.error("Can't setup state: \(error)") } } - func setup() { - createInitialServices() - continueSetup() - } - func update(totalRewardFilter: StakingRewardFiltersPeriod) { totalRewardInterval = totalRewardFilter.interval performTotalRewardSubscription() @@ -85,11 +49,8 @@ extension StakingRelaychainInteractor: StakingRelaychainInteractorInputProtocol extension StakingRelaychainInteractor: EventVisitorProtocol { func processEraStakersInfoChanged(event _: EraStakersInfoChanged) { - guard - let eraValidatorService = sharedState.eraValidatorService, - let rewardCalculationService = sharedState.rewardCalculationService else { - return - } + let eraValidatorService = sharedState.eraValidatorService + let rewardCalculationService = sharedState.rewardCalculatorService provideNetworkStakingInfo() provideEraStakersInfo(from: eraValidatorService) diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainInteractor+Subscription.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainInteractor+Subscription.swift index ef0775ef15..6b1880023d 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainInteractor+Subscription.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainInteractor+Subscription.swift @@ -105,12 +105,12 @@ extension StakingRelaychainInteractor { } func performStashControllerSubscription() { - guard let address = selectedAccount?.toAddress() else { + guard let address = selectedAccount?.toAddress(), let chain = selectedChainAsset?.chain else { handle(stashItem: nil) return } - stashControllerProvider = subscribeStashItemProvider(for: address) + stashControllerProvider = subscribeStashItemProvider(for: address, chainId: chain.chainId) } func subscribeToControllerAccount(address: AccountAddress, chain: ChainModel) { diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainInteractor.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainInteractor.swift index 21486a03ae..d4ec71ecc2 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainInteractor.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainInteractor.swift @@ -7,29 +7,28 @@ final class StakingRelaychainInteractor: RuntimeConstantFetching, AnyCancellable weak var presenter: StakingRelaychainInteractorOutputProtocol? var stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol { - sharedState.stakingLocalSubscriptionFactory + sharedState.localSubscriptionFactory } var stakingOption: Multistaking.ChainAssetOption { sharedState.stakingOption } - let selectedWalletSettings: SelectedWalletSettings - let sharedState: StakingSharedState let chainRegistry: ChainRegistryProtocol - let stakingRemoteSubscriptionService: StakingRemoteSubscriptionServiceProtocol - let stakingAccountUpdatingService: StakingAccountUpdatingServiceProtocol + + let selectedWalletSettings: SelectedWalletSettings + let sharedState: RelaychainStakingSharedStateProtocol let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol - let stakingServiceFactory: StakingServiceFactoryProtocol let accountProviderFactory: AccountProviderFactoryProtocol let eventCenter: EventCenterProtocol let operationManager: OperationManagerProtocol let applicationHandler: ApplicationHandlerProtocol + let networkInfoOperationFactory: NetworkStakingInfoOperationFactoryProtocol + let eraCountdownOperationFactory: EraCountdownOperationFactoryProtocol let logger: LoggerProtocol? var selectedAccount: ChainAccountResponse? var selectedChainAsset: ChainAsset? - private var chainSubscriptionId: UUID? private var maxNominatorsPerValidatorCancellable: CancellableCall? private var eraStakersInfoCancellable: CancellableCall? private var networkInfoCancellable: CancellableCall? @@ -57,14 +56,13 @@ final class StakingRelaychainInteractor: RuntimeConstantFetching, AnyCancellable init( selectedWalletSettings: SelectedWalletSettings, - sharedState: StakingSharedState, + sharedState: RelaychainStakingSharedStateProtocol, chainRegistry: ChainRegistryProtocol, - stakingRemoteSubscriptionService: StakingRemoteSubscriptionServiceProtocol, - stakingAccountUpdatingService: StakingAccountUpdatingServiceProtocol, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, - stakingServiceFactory: StakingServiceFactoryProtocol, accountProviderFactory: AccountProviderFactoryProtocol, + networkInfoOperationFactory: NetworkStakingInfoOperationFactoryProtocol, + eraCountdownOperationFactory: EraCountdownOperationFactoryProtocol, eventCenter: EventCenterProtocol, operationManager: OperationManagerProtocol, applicationHandler: ApplicationHandlerProtocol, @@ -72,13 +70,12 @@ final class StakingRelaychainInteractor: RuntimeConstantFetching, AnyCancellable logger: LoggerProtocol? = nil ) { self.selectedWalletSettings = selectedWalletSettings - self.sharedState = sharedState self.chainRegistry = chainRegistry - self.stakingRemoteSubscriptionService = stakingRemoteSubscriptionService - self.stakingAccountUpdatingService = stakingAccountUpdatingService + self.sharedState = sharedState self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory - self.stakingServiceFactory = stakingServiceFactory + self.networkInfoOperationFactory = networkInfoOperationFactory + self.eraCountdownOperationFactory = eraCountdownOperationFactory self.accountProviderFactory = accountProviderFactory self.eventCenter = eventCenter self.operationManager = operationManager @@ -88,16 +85,9 @@ final class StakingRelaychainInteractor: RuntimeConstantFetching, AnyCancellable } deinit { - if let selectedChainAsset = selectedChainAsset { - clearChainRemoteSubscription(for: selectedChainAsset.chain.chainId) - } + sharedState.throttle() - clearAccountRemoteSubscription() clearCancellable() - - sharedState.eraValidatorService?.throttle() - sharedState.rewardCalculationService?.throttle() - sharedState.blockTimeService?.throttle() } func clearCancellable() { @@ -119,54 +109,6 @@ final class StakingRelaychainInteractor: RuntimeConstantFetching, AnyCancellable selectedChainAsset = chainAsset } - func clearChainRemoteSubscription(for chainId: ChainModel.Id) { - if let chainSubscriptionId = chainSubscriptionId { - stakingRemoteSubscriptionService.detachFromGlobalData( - for: chainSubscriptionId, - chainId: chainId, - queue: nil, - closure: nil - ) - - self.chainSubscriptionId = nil - } - } - - func setupChainRemoteSubscription() { - guard let chainId = selectedChainAsset?.chain.chainId else { - return - } - - chainSubscriptionId = stakingRemoteSubscriptionService.attachToGlobalData( - for: chainId, - queue: nil, - closure: nil - ) - } - - func clearAccountRemoteSubscription() { - stakingAccountUpdatingService.clearSubscription() - } - - func setupAccountRemoteSubscription() { - guard - let chainId = selectedChainAsset?.chain.chainId, - let accountId = selectedAccount?.accountId, - let chainFormat = selectedChainAsset?.chain.chainFormat else { - return - } - - do { - try stakingAccountUpdatingService.setupSubscription( - for: accountId, - chainId: chainId, - chainFormat: chainFormat - ) - } catch { - logger?.error("Could setup staking account subscription") - } - } - func provideSelectedAccount() { guard let address = selectedAccount?.toAddress() else { return @@ -254,111 +196,91 @@ final class StakingRelaychainInteractor: RuntimeConstantFetching, AnyCancellable } func provideNetworkStakingInfo() { - do { - clear(cancellable: &networkInfoCancellable) + clear(cancellable: &networkInfoCancellable) - guard let chain = selectedChainAsset?.chain else { - return - } + guard let chain = selectedChainAsset?.chain else { + return + } - let networkInfoFactory = try sharedState.createNetworkInfoOperationFactory(for: chain) + let eraValidatorService = sharedState.eraValidatorService - let chainId = chain.chainId + let chainId = chain.chainId - guard - let runtimeService = chainRegistry.getRuntimeProvider(for: chainId), - let eraValidatorService = sharedState.eraValidatorService else { - presenter?.didReceive(networkStakingInfoError: ChainRegistryError.runtimeMetadaUnavailable) - return - } + guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + presenter?.didReceive(networkStakingInfoError: ChainRegistryError.runtimeMetadaUnavailable) + return + } - let wrapper = networkInfoFactory.networkStakingOperation( - for: eraValidatorService, - runtimeService: runtimeService - ) - - wrapper.targetOperation.completionBlock = { [weak self] in - DispatchQueue.main.async { - guard self?.networkInfoCancellable === wrapper else { - return - } - - self?.networkInfoCancellable = nil - - do { - let info = try wrapper.targetOperation.extractNoCancellableResultData() - self?.presenter?.didReceive(networkStakingInfo: info) - } catch { - self?.presenter?.didReceive(networkStakingInfoError: error) - } + let wrapper = networkInfoOperationFactory.networkStakingOperation( + for: eraValidatorService, + runtimeService: runtimeService + ) + + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard self?.networkInfoCancellable === wrapper else { + return } - } - networkInfoCancellable = wrapper + self?.networkInfoCancellable = nil - operationManager.enqueue(operations: wrapper.allOperations, in: .transient) - } catch { - presenter?.didReceive(networkStakingInfoError: error) + do { + let info = try wrapper.targetOperation.extractNoCancellableResultData() + self?.presenter?.didReceive(networkStakingInfo: info) + } catch { + self?.presenter?.didReceive(networkStakingInfoError: error) + } + } } + + networkInfoCancellable = wrapper + + operationManager.enqueue(operations: wrapper.allOperations, in: .transient) } func fetchEraCompletionTime() { - do { - clear(cancellable: &eraCompletionTimeCancellable) + clear(cancellable: &eraCompletionTimeCancellable) - guard let chain = selectedChainAsset?.chain else { - return - } + guard let chain = selectedChainAsset?.chain else { + return + } - let chainId = chain.chainId + let chainId = chain.chainId - guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { - presenter?.didReceive(eraCountdownResult: .failure(ChainRegistryError.runtimeMetadaUnavailable)) - return - } + guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + presenter?.didReceive(eraCountdownResult: .failure(ChainRegistryError.runtimeMetadaUnavailable)) + return + } - guard let connection = chainRegistry.getConnection(for: chainId) else { - presenter?.didReceive(eraCountdownResult: .failure(ChainRegistryError.connectionUnavailable)) - return - } + guard let connection = chainRegistry.getConnection(for: chainId) else { + presenter?.didReceive(eraCountdownResult: .failure(ChainRegistryError.connectionUnavailable)) + return + } + + let operationWrapper = eraCountdownOperationFactory.fetchCountdownOperationWrapper( + for: connection, + runtimeService: runtimeService + ) + + operationWrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard self?.eraCompletionTimeCancellable === operationWrapper else { + return + } + + self?.eraCompletionTimeCancellable = nil - let storageRequestFactory = StorageRequestFactory( - remoteFactory: StorageKeyFactory(), - operationManager: operationManager - ) - - let eraCountdownOperationFactory = try sharedState.createEraCountdownOperationFactory( - for: chain, - storageRequestFactory: storageRequestFactory - ) - - let operationWrapper = eraCountdownOperationFactory.fetchCountdownOperationWrapper( - for: connection, - runtimeService: runtimeService - ) - - operationWrapper.targetOperation.completionBlock = { [weak self] in - DispatchQueue.main.async { - guard self?.eraCompletionTimeCancellable === operationWrapper else { - return - } - - self?.eraCompletionTimeCancellable = nil - - do { - let result = try operationWrapper.targetOperation.extractNoCancellableResultData() - self?.presenter?.didReceive(eraCountdownResult: .success(result)) - } catch { - self?.presenter?.didReceive(eraCountdownResult: .failure(error)) - } + do { + let result = try operationWrapper.targetOperation.extractNoCancellableResultData() + self?.presenter?.didReceive(eraCountdownResult: .success(result)) + } catch { + self?.presenter?.didReceive(eraCountdownResult: .failure(error)) } } + } - eraCompletionTimeCancellable = operationWrapper + eraCompletionTimeCancellable = operationWrapper - operationManager.enqueue(operations: operationWrapper.allOperations, in: .transient) - } catch { - presenter?.didReceive(eraCountdownResult: .failure(error)) - } + operationManager.enqueue(operations: operationWrapper.allOperations, in: .transient) } } diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainPresenter.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainPresenter.swift index 23c9f96ea2..c518fd7ce5 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainPresenter.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainPresenter.swift @@ -82,11 +82,12 @@ final class StakingRelaychainPresenter { with: networkStakingInfo, chainAsset: chainAsset, params: params, - priceData: commonData?.price + priceData: commonData?.price, + locale: view?.selectedLocale ?? Locale.current ) view?.didRecieveNetworkStakingInfo(viewModel: networkStakingInfoViewModel) } else { - view?.didRecieveNetworkStakingInfo(viewModel: nil) + view?.didRecieveNetworkStakingInfo(viewModel: NetworkStakingInfoViewModel.allLoading) } } @@ -241,42 +242,6 @@ extension StakingRelaychainPresenter: StakingMainChildPresenterProtocol { interactor.setup() } - func performMainAction() { - guard let commonData = stateMachine - .viewState(using: { (state: BaseStakingState) in state })?.commonData else { - return - } - - let locale = view?.localizationManager?.selectedLocale ?? Locale.current - - let nomination = stateMachine.viewState( - using: { (state: NominatorState) in state } - )?.nomination - - DataValidationRunner(validators: [ - dataValidatingFactory.maxNominatorsCountNotApplied( - counterForNominators: commonData.counterForNominators, - maxNominatorsCount: commonData.maxNominatorsCount, - hasExistingNomination: nomination != nil, - locale: locale - ) - ]).runValidation { [weak self] in - self?.wireframe.showSetupAmount(from: self?.view) - } - } - - func performRewardInfoAction() { - guard let rewardCalculator = stateMachine - .viewState(using: { (state: BaseStakingState) in state })?.commonData.calculatorEngine else { - return - } - - let maxReward = rewardCalculator.calculateMaxReturn(isCompound: true, period: .year) - let avgReward = rewardCalculator.calculateAvgReturn(isCompound: true, period: .year) - - wireframe.showRewardDetails(from: view, maxReward: maxReward, avgReward: avgReward) - } - func performChangeValidatorsAction() { wireframe.showNominatorValidators(from: view) } @@ -293,6 +258,10 @@ extension StakingRelaychainPresenter: StakingMainChildPresenterProtocol { handleStakeMore() } + func performClaimRewards() { + // not needed action for relaychain staking + } + func performRedeemAction() { guard let view = view else { return } let selectedLocale = view.localizationManager?.selectedLocale @@ -373,12 +342,33 @@ extension StakingRelaychainPresenter: StakingMainChildPresenterProtocol { let stashAddress = validatorState.stashItem.stash wireframe.showYourValidatorInfo(stashAddress, from: view) } - case .yieldBoost: - // not supported yet for relaychain staking + default: + logger?.warning("Unsupported action: \(action)") + } + } + + func performAlertAction(_ alert: StakingAlert) { + switch alert { + case .bondedSetValidators: + performSetupValidatorsForBondedAction() + case .nominatorChangeValidators, .nominatorAllOversubscribed: + performChangeValidatorsAction() + case .nominatorLowStake: + performStakeMoreAction() + case .redeemUnbonded: + performRedeemAction() + case .rebag: + performRebag() + case .waitingNextEra: + // no action break } } + func performSelectedEntityAction() { + // no support for selected entity + } + func selectPeriod(_ filter: StakingRewardFiltersPeriod) { stateMachine.state.process(totalRewardFilter: filter) interactor.update(totalRewardFilter: filter) diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainProtocols.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainProtocols.swift index 8593405630..ed5e3bdd2d 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainProtocols.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainProtocols.swift @@ -45,15 +45,11 @@ protocol StakingRelaychainInteractorOutputProtocol: AnyObject { } protocol StakingRelaychainWireframeProtocol: AlertPresentable, ErrorPresentable, StakingErrorPresentable { - func showSetupAmount(from view: StakingMainViewProtocol?) - func proceedToSelectValidatorsStart( from view: StakingMainViewProtocol?, existingBonding: ExistingBonding ) - func showRewardDetails(from view: ControllerBackedProtocol?, maxReward: Decimal, avgReward: Decimal) - func showRewardPayoutsForNominator(from view: ControllerBackedProtocol?, stashAddress: AccountAddress) func showRewardPayoutsForValidator(from view: ControllerBackedProtocol?, stashAddress: AccountAddress) func showNominatorValidators(from view: ControllerBackedProtocol?) diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainWireframe.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainWireframe.swift index bb86fc0134..82b1e9642b 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainWireframe.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainWireframe.swift @@ -1,24 +1,14 @@ import Foundation final class StakingRelaychainWireframe { - let state: StakingSharedState + let state: RelaychainStakingSharedStateProtocol - init(state: StakingSharedState) { + init(state: RelaychainStakingSharedStateProtocol) { self.state = state } } extension StakingRelaychainWireframe: StakingRelaychainWireframeProtocol { - func showSetupAmount(from view: StakingMainViewProtocol?) { - guard let amountView = StakingAmountViewFactory.createView(with: nil, stakingState: state) else { - return - } - - let navigationController = ImportantFlowViewFactory.createNavigation(from: amountView.controller) - - view?.controller.present(navigationController, animated: true, completion: nil) - } - func proceedToSelectValidatorsStart( from view: StakingMainViewProtocol?, existingBonding: ExistingBonding @@ -37,12 +27,6 @@ extension StakingRelaychainWireframe: StakingRelaychainWireframeProtocol { view?.controller.present(navigationController, animated: true, completion: nil) } - func showRewardDetails(from view: ControllerBackedProtocol?, maxReward: Decimal, avgReward: Decimal) { - let infoVew = ModalInfoFactory.createRewardDetails(for: maxReward, avgReward: avgReward) - - view?.controller.present(infoVew, animated: true, completion: nil) - } - func showRewardPayoutsForNominator(from view: ControllerBackedProtocol?, stashAddress: AccountAddress) { guard let rewardPayoutsView = StakingRewardPayoutsViewFactory .createViewForNominator(for: state, stashAddress: stashAddress) else { return } diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/StakingStateMachineProtocols.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/StakingStateMachineProtocols.swift index ebbe26215b..a94e62d9fe 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/StakingStateMachineProtocols.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/StakingStateMachineProtocols.swift @@ -3,7 +3,6 @@ import BigInt protocol StakingStateVisitorProtocol { func visit(state: InitialStakingState) - func visit(state: NoStashState) func visit(state: StashState) func visit(state: PendingBondedState) func visit(state: BondedState) diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/BaseStashNextState.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/BaseStashNextState.swift index 5df747a30f..cf54590636 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/BaseStashNextState.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/BaseStashNextState.swift @@ -44,7 +44,7 @@ class BaseStashNextState: BaseStakingState { bagListNode: nil ) } else { - newState = NoStashState( + newState = InitialStakingState( stateMachine: stateMachine, commonData: commonData ) diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/InitialStakingState.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/InitialStakingState.swift index f81ac57458..0b095d3ede 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/InitialStakingState.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/InitialStakingState.swift @@ -22,10 +22,7 @@ final class InitialStakingState: BaseStakingState { bagListNode: nil ) } else { - newState = NoStashState( - stateMachine: stateMachine, - commonData: commonData - ) + newState = self } stateMachine.transit(to: newState) diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/NoStashState.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/NoStashState.swift deleted file mode 100644 index fbbe9f8ae5..0000000000 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/NoStashState.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation - -final class NoStashState: BaseStakingState { - init( - stateMachine: StakingStateMachineProtocol, - commonData: StakingStateCommonData - ) { - super.init(stateMachine: stateMachine, commonData: commonData) - } - - override func accept(visitor: StakingStateVisitorProtocol) { - visitor.visit(state: self) - } - - override func process(stashItem: StashItem?) { - if let stashItem = stashItem { - guard let stateMachine = stateMachine else { - return - } - - let newState = StashState( - stateMachine: stateMachine, - commonData: commonData, - stashItem: stashItem, - ledgerInfo: nil, - totalReward: nil, - bagListNode: nil - ) - - stateMachine.transit(to: newState) - } else { - stateMachine?.transit(to: self) - } - } -} diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/NominatorState+Status.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/NominatorState+Status.swift index f9c2aa5e09..cbe9cd5432 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/NominatorState+Status.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/NominatorState+Status.swift @@ -9,6 +9,10 @@ extension NominatorState { } do { + guard ledgerInfo.active > 0 else { + return .inactive + } + let accountId = try stashItem.stash.toAccountId() let allNominators = eraStakers.validators.map(\.exposure.others) diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/StashState.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/StashState.swift index 6d558802bd..c3c501cc75 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/StashState.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/StashState.swift @@ -37,7 +37,7 @@ final class StashState: BaseStakingState { return } - let newState = NoStashState( + let newState = InitialStakingState( stateMachine: stateMachine, commonData: commonData ) diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/ValidatorState+Status.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/ValidatorState+Status.swift index 0d4092f6d2..88426d4a11 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/ValidatorState+Status.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/StateMachine/States/ValidatorState+Status.swift @@ -9,6 +9,10 @@ extension ValidatorState { } do { + guard ledgerInfo.active > 0 else { + return .inactive(era: eraStakers.activeEra) + } + let accountId = try stashItem.stash.toAccountId() if eraStakers.validators diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/ViewModel/NetworkInfoViewModelFactory.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/ViewModel/NetworkInfoViewModelFactory.swift index 96a65bedb8..6124ee57cb 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/ViewModel/NetworkInfoViewModelFactory.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/ViewModel/NetworkInfoViewModelFactory.swift @@ -8,13 +8,27 @@ struct NetworkInfoViewModelParams { let votersCount: UInt32? } +struct NPoolsDetailsInfoParams { + let totalActiveStake: BigUInt? + let minStake: BigUInt? + let duration: StakingDuration? +} + protocol NetworkInfoViewModelFactoryProtocol { func createNetworkStakingInfoViewModel( with networkStakingInfo: NetworkStakingInfo, chainAsset: ChainAsset, params: NetworkInfoViewModelParams, - priceData: PriceData? - ) -> LocalizableResource + priceData: PriceData?, + locale: Locale + ) -> NetworkStakingInfoViewModel + + func createNPoolsStakingInfoViewModel( + for params: NPoolsDetailsInfoParams, + chainAsset: ChainAsset, + priceData: PriceData?, + locale: Locale + ) -> NetworkStakingInfoViewModel } final class NetworkInfoViewModelFactory { @@ -107,12 +121,14 @@ final class NetworkInfoViewModelFactory { private func createLockUpPeriodViewModel( with networkStakingInfo: NetworkStakingInfo ) -> LocalizableResource { + networkStakingInfo.stakingDuration.localizableUnlockingString + } + + private func createStakingPeriod() -> LocalizableResource { LocalizableResource { locale in - let formattedString = networkStakingInfo.stakingDuration.unlocking.localizedDaysHours( - for: locale + R.string.localizable.stakingNetworkInfoStakingPeriodValue( + preferredLanguages: locale.rLanguages ) - - return "~\(formattedString)" } } } @@ -122,38 +138,70 @@ extension NetworkInfoViewModelFactory: NetworkInfoViewModelFactoryProtocol { with networkStakingInfo: NetworkStakingInfo, chainAsset: ChainAsset, params: NetworkInfoViewModelParams, - priceData: PriceData? - ) -> LocalizableResource { - let localizedTotalStake = createTotalStakeViewModel( + priceData: PriceData?, + locale: Locale + ) -> NetworkStakingInfoViewModel { + let totalStake = createTotalStakeViewModel( with: networkStakingInfo, chainAsset: chainAsset, priceData: priceData - ) + ).value(for: locale) - let localizedMinimalStake = createMinimalStakeViewModel( + let minimalStake = createMinimalStakeViewModel( with: networkStakingInfo, chainAsset: chainAsset, minNominatorBond: params.minNominatorBond, votersCount: params.votersCount, priceData: priceData - ) + ).value(for: locale) - let nominatorsCount = createActiveNominatorsViewModel(with: networkStakingInfo) + let nominatorsCount = createActiveNominatorsViewModel(with: networkStakingInfo).value(for: locale) - let localizedLockUpPeriod = createLockUpPeriodViewModel(with: networkStakingInfo) + let lockUpPeriod = createLockUpPeriodViewModel(with: networkStakingInfo).value(for: locale) - return LocalizableResource { locale in - let stakingPeriod = R.string.localizable.stakingNetworkInfoStakingPeriodValue( - preferredLanguages: locale.rLanguages + let stakingPeriod = createStakingPeriod().value(for: locale) + + return .init( + totalStake: .loaded(value: totalStake), + minimalStake: .loaded(value: minimalStake), + activeNominators: .loaded(value: nominatorsCount), + stakingPeriod: .loaded(value: stakingPeriod), + lockUpPeriod: .loaded(value: lockUpPeriod) + ) + } + + func createNPoolsStakingInfoViewModel( + for params: NPoolsDetailsInfoParams, + chainAsset: ChainAsset, + priceData: PriceData?, + locale: Locale + ) -> NetworkStakingInfoViewModel { + let totalStake = params.totalActiveStake.map { totalStake in + createStakeViewModel( + stake: totalStake, + chainAsset: chainAsset, + priceData: priceData ) + }?.value(for: locale) - return NetworkStakingInfoViewModel( - totalStake: localizedTotalStake.value(for: locale), - minimalStake: localizedMinimalStake.value(for: locale), - activeNominators: nominatorsCount.value(for: locale), - stakingPeriod: stakingPeriod, - lockUpPeriod: localizedLockUpPeriod.value(for: locale) + let minimalStake = params.minStake.map { minStake in + createStakeViewModel( + stake: minStake, + chainAsset: chainAsset, + priceData: priceData ) - } + }?.value(for: locale) + + let lockUpPeriod = params.duration?.localizableUnlockingString.value(for: locale) + + let stakingPeriod = createStakingPeriod().value(for: locale) + + return .init( + totalStake: totalStake.map { .loaded(value: $0) } ?? .loading, + minimalStake: minimalStake.map { .loaded(value: $0) } ?? .loading, + activeNominators: nil, + stakingPeriod: .loaded(value: stakingPeriod), + lockUpPeriod: lockUpPeriod.map { .loaded(value: $0) } ?? .loading + ) } } diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/ViewModel/StakingRelaychainStatics.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/ViewModel/StakingRelaychainStatics.swift index 9fbd189268..fe7b30428b 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/ViewModel/StakingRelaychainStatics.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/ViewModel/StakingRelaychainStatics.swift @@ -1,22 +1,3 @@ import Foundation -struct StakingRelaychainStatics: StakingMainStaticViewModelProtocol { - func networkInfoActiveNominators(for locale: Locale) -> String { - R.string.localizable.stakingMainActiveNominatorsTitle( - preferredLanguages: locale.rLanguages - ) - } - - func actionsYourValidators(for locale: Locale) -> String { - R.string.localizable.stakingYourValidatorsTitle( - preferredLanguages: locale.rLanguages - ) - } - - func waitingNextEra(for timeString: String, locale: Locale) -> String { - R.string.localizable.stakingWaitingNextEraFormat( - timeString, - preferredLanguages: locale.rLanguages - ).uppercased() - } -} +struct StakingRelaychainStatics: StakingMainStaticViewModelProtocol {} diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/ViewModel/StakingStateViewModelFactory+Alerts.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/ViewModel/StakingStateViewModelFactory+Alerts.swift index b301492adb..09cee35953 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/ViewModel/StakingStateViewModelFactory+Alerts.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/ViewModel/StakingStateViewModelFactory+Alerts.swift @@ -26,10 +26,6 @@ extension StakingStateViewModelFactory { ].compactMap { $0 } } - func stakingAlertsNoStashState(_: NoStashState) -> [StakingAlert] { - [] - } - private func findRedeemUnbondedAlert( commonData: StakingStateCommonData, ledgerInfo: StakingLedger diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/ViewModel/StakingStateViewModelFactory.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/ViewModel/StakingStateViewModelFactory.swift index e52d9616da..90a2c35737 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/ViewModel/StakingStateViewModelFactory.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/ViewModel/StakingStateViewModelFactory.swift @@ -81,26 +81,22 @@ final class StakingStateViewModelFactory { calendar: self.calendar ) }?.value(for: locale) - if let price = reward.price { - return StakingRewardViewModel( - amount: .loaded(reward.amount), - price: .loaded(price), - filter: filter - ) - } else { - return StakingRewardViewModel( - amount: .loaded(reward.amount), - price: nil, - filter: filter - ) - } + return StakingRewardViewModel( + totalRewards: .loaded(value: reward), + claimableRewards: nil, + graphics: R.image.imageStakingTypeDirect(), + filter: filter, + hasPrice: chainAsset.asset.hasPrice + ) } } else { return LocalizableResource { _ in StakingRewardViewModel( - amount: .loading, - price: .loading, - filter: nil + totalRewards: .loading, + claimableRewards: nil, + graphics: R.image.imageStakingTypeDirect(), + filter: nil, + hasPrice: chainAsset.asset.hasPrice ) } } @@ -158,32 +154,6 @@ final class StakingStateViewModelFactory { } } - private func createEstimationViewModel( - chainAsset: ChainAsset, - commonData: StakingStateCommonData - ) throws -> StakingEstimationViewModel { - guard let calculator = commonData.calculatorEngine else { - return StakingEstimationViewModel(tokenSymbol: chainAsset.asset.symbol, reward: nil) - } - - let monthlyReturn = calculator.calculateMaxReturn(isCompound: true, period: .month) - let yearlyReturn = calculator.calculateMaxReturn(isCompound: true, period: .year) - - let percentageFormatter = NumberFormatter.percentBase.localizableResource() - - let reward = LocalizableResource { locale in - PeriodRewardViewModel( - monthly: percentageFormatter.value(for: locale).stringFromDecimal(monthlyReturn) ?? "", - yearly: percentageFormatter.value(for: locale).stringFromDecimal(yearlyReturn) ?? "" - ) - } - - return StakingEstimationViewModel( - tokenSymbol: chainAsset.asset.symbol, - reward: reward - ) - } - private func createUnbondingViewModel( from stakingLedger: StakingLedger, chainAsset: ChainAsset, @@ -227,26 +197,6 @@ extension StakingStateViewModelFactory: StakingStateVisitorProtocol { lastViewModel = .undefined } - func visit(state: NoStashState) { - logger?.debug("No stash state") - - guard let chainAsset = state.commonData.chainAsset else { - lastViewModel = .undefined - return - } - - updateCacheForChainAsset(chainAsset) - - do { - let viewModel = try createEstimationViewModel(chainAsset: chainAsset, commonData: state.commonData) - - let alerts = stakingAlertsNoStashState(state) - lastViewModel = .noStash(viewModel: viewModel, alerts: alerts) - } catch { - lastViewModel = .undefined - } - } - func visit(state: StashState) { logger?.debug("Stash state") diff --git a/novawallet/Modules/Staking/StakingMain/StakingMainPresenter.swift b/novawallet/Modules/Staking/StakingMain/StakingMainPresenter.swift index 6ac602e80a..872062555f 100644 --- a/novawallet/Modules/Staking/StakingMain/StakingMainPresenter.swift +++ b/novawallet/Modules/Staking/StakingMain/StakingMainPresenter.swift @@ -11,8 +11,6 @@ final class StakingMainPresenter { let viewModelFactory: StakingMainViewModelFactoryProtocol let stakingOption: Multistaking.ChainAssetOption let logger: LoggerProtocol? - let wallet: MetaAccountModel - let accountManagementFilter: AccountManagementFilterProtocol private var childPresenter: StakingMainChildPresenterProtocol? private var period: StakingRewardFiltersPeriod? @@ -20,18 +18,14 @@ final class StakingMainPresenter { init( interactor: StakingMainInteractorInputProtocol, wireframe: StakingMainWireframeProtocol, - wallet: MetaAccountModel, stakingOption: Multistaking.ChainAssetOption, - accountManagementFilter: AccountManagementFilterProtocol, childPresenterFactory: StakingMainPresenterFactoryProtocol, viewModelFactory: StakingMainViewModelFactoryProtocol, logger: LoggerProtocol? ) { self.interactor = interactor self.wireframe = wireframe - self.wallet = wallet self.stakingOption = stakingOption - self.accountManagementFilter = accountManagementFilter self.childPresenterFactory = childPresenterFactory self.viewModelFactory = viewModelFactory self.logger = logger @@ -65,65 +59,6 @@ extension StakingMainPresenter: StakingMainPresenterProtocol { interactor.setup() } - func performMainAction() { - let chain = stakingOption.chainAsset.chain - - if wallet.fetchMetaChainAccount(for: chain.accountRequest()) != nil { - childPresenter?.performMainAction() - } else if accountManagementFilter.canAddAccount(to: wallet, chain: chain) { - guard let view = view else { - return - } - - let locale = view.selectedLocale - - let message = R.string.localizable.commonChainAccountMissingMessageFormat( - chain.name, - preferredLanguages: locale.rLanguages - ) - - wireframe.presentAddAccount( - from: view, - chainName: chain.name, - message: message, - locale: locale - ) { [weak self] in - guard let wallet = self?.wallet else { - return - } - - self?.wireframe.showWalletDetails(from: self?.view, wallet: wallet) - } - } else { - guard let view = view, let locale = view.localizationManager?.selectedLocale else { - return - } - - wireframe.presentNoAccountSupport( - from: view, - walletType: wallet.type, - chainName: chain.name, - locale: locale - ) - } - } - - func performRewardInfoAction() { - childPresenter?.performRewardInfoAction() - } - - func performChangeValidatorsAction() { - childPresenter?.performChangeValidatorsAction() - } - - func performSetupValidatorsForBondedAction() { - childPresenter?.performSetupValidatorsForBondedAction() - } - - func performStakeMoreAction() { - childPresenter?.performStakeMoreAction() - } - func performRedeemAction() { childPresenter?.performRedeemAction() } @@ -132,8 +67,8 @@ extension StakingMainPresenter: StakingMainPresenterProtocol { childPresenter?.performRebondAction() } - func performRebag() { - childPresenter?.performRebag() + func performClaimRewards() { + childPresenter?.performClaimRewards() } func networkInfoViewDidChangeExpansion(isExpanded: Bool) { @@ -144,6 +79,14 @@ extension StakingMainPresenter: StakingMainPresenterProtocol { childPresenter?.performManageAction(action) } + func performAlertAction(_ alert: StakingAlert) { + childPresenter?.performAlertAction(alert) + } + + func performSelectedEntityAction() { + childPresenter?.performSelectedEntityAction() + } + func selectPeriod() { wireframe.showPeriodSelection( from: view, diff --git a/novawallet/Modules/Staking/StakingMain/StakingMainPresenterFactory.swift b/novawallet/Modules/Staking/StakingMain/StakingMainPresenterFactory.swift index c0ee9d69ff..42f7c332bf 100644 --- a/novawallet/Modules/Staking/StakingMain/StakingMainPresenterFactory.swift +++ b/novawallet/Modules/Staking/StakingMain/StakingMainPresenterFactory.swift @@ -11,9 +11,11 @@ protocol StakingMainPresenterFactoryProtocol { final class StakingMainPresenterFactory { let applicationHandler: ApplicationHandlerProtocol + let sharedStateFactory: StakingSharedStateFactoryProtocol - init(applicationHandler: ApplicationHandlerProtocol) { + init(applicationHandler: ApplicationHandlerProtocol, sharedStateFactory: StakingSharedStateFactoryProtocol) { self.applicationHandler = applicationHandler + self.sharedStateFactory = sharedStateFactory } } @@ -23,14 +25,12 @@ extension StakingMainPresenterFactory: StakingMainPresenterFactoryProtocol { view: StakingMainViewProtocol ) -> StakingMainChildPresenterProtocol? { switch stakingOption.type { - case .relaychain: - return createRelaychainPresenter(for: stakingOption, view: view, consensus: .babe) - case .auraRelaychain: - return createRelaychainPresenter(for: stakingOption, view: view, consensus: .auraGeneral) + case .relaychain, .auraRelaychain, .azero: + return createRelaychainPresenter(for: stakingOption, view: view) case .parachain, .turing: return createParachainPresenter(for: stakingOption, view: view) - case .azero: - return createRelaychainPresenter(for: stakingOption, view: view, consensus: .auraAzero) + case .nominationPools: + return createNominationPoolsPresenter(for: stakingOption.chainAsset, view: view) case .unsupported: return nil } diff --git a/novawallet/Modules/Staking/StakingMain/StakingMainProtocols.swift b/novawallet/Modules/Staking/StakingMain/StakingMainProtocols.swift index 5a40c9d572..b137430064 100644 --- a/novawallet/Modules/Staking/StakingMain/StakingMainProtocols.swift +++ b/novawallet/Modules/Staking/StakingMain/StakingMainProtocols.swift @@ -5,25 +5,23 @@ import BigInt protocol StakingMainViewProtocol: ControllerBackedProtocol, Localizable { func didReceive(viewModel: StakingMainViewModel) - func didRecieveNetworkStakingInfo(viewModel: LocalizableResource?) + func didRecieveNetworkStakingInfo(viewModel: NetworkStakingInfoViewModel) func didReceiveStakingState(viewModel: StakingViewState) func expandNetworkInfoView(_ isExpanded: Bool) func didReceiveStatics(viewModel: StakingMainStaticViewModelProtocol) + func didReceiveSelectedEntity(_ entity: StakingSelectedEntityViewModel) func didEditRewardFilters() } protocol StakingMainPresenterProtocol: AnyObject { func setup() - func performMainAction() - func performRewardInfoAction() - func performChangeValidatorsAction() - func performSetupValidatorsForBondedAction() - func performStakeMoreAction() func performRedeemAction() func performRebondAction() - func performRebag() + func performClaimRewards() func networkInfoViewDidChangeExpansion(isExpanded: Bool) func performManageAction(_ action: StakingManageOption) + func performAlertAction(_ alert: StakingAlert) + func performSelectedEntityAction() func selectPeriod() } @@ -50,14 +48,11 @@ protocol StakingMainWireframeProtocol: AlertPresentable, NoAccountSupportPresent protocol StakingMainChildPresenterProtocol: AnyObject { func setup() - func performMainAction() - func performRewardInfoAction() - func performChangeValidatorsAction() - func performSetupValidatorsForBondedAction() - func performStakeMoreAction() func performRedeemAction() func performRebondAction() - func performRebag() + func performClaimRewards() func performManageAction(_ action: StakingManageOption) + func performAlertAction(_ alert: StakingAlert) + func performSelectedEntityAction() func selectPeriod(_ period: StakingRewardFiltersPeriod) } diff --git a/novawallet/Modules/Staking/StakingMain/StakingMainViewController.swift b/novawallet/Modules/Staking/StakingMain/StakingMainViewController.swift index 441b919182..a5b487a9d0 100644 --- a/novawallet/Modules/Staking/StakingMain/StakingMainViewController.swift +++ b/novawallet/Modules/Staking/StakingMain/StakingMainViewController.swift @@ -26,6 +26,9 @@ final class StakingMainViewController: UIViewController, AdaptiveDesignable, Vie private var actionsView: StakingActionsView? private var unbondingsView: StakingUnbondingsView? + private var selectedEntityView: StackTableView? + private var selectedEntityCell: StackAddressCell? + private var stateContainerView: UIView? private var stateView: LocalizableView? @@ -60,8 +63,8 @@ final class StakingMainViewController: UIViewController, AdaptiveDesignable, Vie override func viewDidLoad() { super.viewDidLoad() - setupNetworkInfoView() setupAlertsView() + setupNetworkInfoView() setupScrollView() setupLocalization() presenter.setup() @@ -77,6 +80,8 @@ final class StakingMainViewController: UIViewController, AdaptiveDesignable, Vie } rewardView?.didAppearSkeleton() + + selectedEntityCell?.didAppearSkeleton() } override func viewDidDisappear(_ animated: Bool) { @@ -89,6 +94,8 @@ final class StakingMainViewController: UIViewController, AdaptiveDesignable, Vie } rewardView?.didDisappearSkeleton() + + selectedEntityCell?.didDisappearSkeleton() } override func viewDidLayoutSubviews() { @@ -101,19 +108,78 @@ final class StakingMainViewController: UIViewController, AdaptiveDesignable, Vie } rewardView?.didUpdateSkeletonLayout() + + selectedEntityCell?.didUpdateSkeletonLayout() } // MARK: - Private functions - @objc - private func rewardPeriodAction() { + @objc private func rewardPeriodAction() { presenter.selectPeriod() } + @objc private func claimRewardsAction() { + presenter.performClaimRewards() + } + + @objc private func selectedEntityAction() { + presenter.performSelectedEntityAction() + } + private func setupScrollView() { scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 8, right: 0) } + private func setupEntityView(for viewModel: StakingSelectedEntityViewModel) { + let entityView: StackTableView + + if let selectedEntityView = selectedEntityView { + entityView = selectedEntityView + } else { + let containerView = UIView() + + entityView = StackTableView() + + if let beforeView = networkInfoContainerView { + stackView.insertArranged(view: containerView, before: beforeView) + } else { + stackView.addArrangedSubview(containerView) + } + + stackView.setCustomSpacing(8, after: containerView) + + containerView.snp.makeConstraints { make in + make.width.equalToSuperview() + } + + containerView.addSubview(entityView) + entityView.snp.makeConstraints { make in + make.top.bottom.equalToSuperview() + make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) + } + + selectedEntityView = entityView + } + + entityView.clear() + + entityView.contentInsets = UIEdgeInsets(top: 4.0, left: 16.0, bottom: 8.0, right: 16.0) + + let tableHeader = StackTableHeaderCell() + tableHeader.titleLabel.text = viewModel.title + tableHeader.titleLabel.apply(style: .regularSubhedlineSecondary) + entityView.addArrangedSubview(tableHeader) + + let addressCell = StackAddressCell() + entityView.addArrangedSubview(addressCell) + + selectedEntityCell = addressCell + + addressCell.bind(viewModel: viewModel.loadingAddress) + + addressCell.addTarget(self, action: #selector(selectedEntityAction), for: .touchUpInside) + } + private func setupNetworkInfoView() { let defaultFrame = CGRect(origin: .zero, size: CGSize(width: 343.0, height: 296)) let networkInfoView = NetworkInfoView(frame: defaultFrame) @@ -130,7 +196,7 @@ final class StakingMainViewController: UIViewController, AdaptiveDesignable, Vie applyConstraints(for: networkInfoContainerView, innerView: networkInfoView) - stackView.insertArrangedSubview(networkInfoContainerView, at: 0) + stackView.addArrangedSubview(networkInfoContainerView) } private func setupAlertsView() { @@ -139,7 +205,7 @@ final class StakingMainViewController: UIViewController, AdaptiveDesignable, Vie applyConstraints(for: alertsContainerView, innerView: alertsView) - stackView.insertArranged(view: alertsContainerView, after: networkInfoContainerView) + stackView.insertArrangedSubview(alertsContainerView, at: 0) alertsView.delegate = self } @@ -149,11 +215,9 @@ final class StakingMainViewController: UIViewController, AdaptiveDesignable, Vie return } - let defaultFrame = CGRect(origin: .zero, size: CGSize(width: 343, height: 116.0)) - let containerView = UIView(frame: defaultFrame) - containerView.translatesAutoresizingMaskIntoConstraints = false + let containerView = UIView() - let rewardView = StakingRewardView(frame: defaultFrame) + let rewardView = StakingRewardView() rewardView.locale = localizationManager?.selectedLocale ?? Locale.current rewardView.filterView.control.addTarget( self, @@ -189,11 +253,13 @@ final class StakingMainViewController: UIViewController, AdaptiveDesignable, Vie newActionsView.locale = selectedLocale newActionsView.delegate = self newActionsView.statics = staticsViewModel - stackView.addArrangedSubview(newActionsView) + stackView.insertArranged(view: newActionsView, before: networkInfoContainerView) newActionsView.snp.makeConstraints { make in make.width.equalToSuperview() } + stackView.setCustomSpacing(8.0, after: newActionsView) + actionsView = newActionsView } @@ -213,10 +279,14 @@ final class StakingMainViewController: UIViewController, AdaptiveDesignable, Vie newUnbondingsView.locale = selectedLocale newUnbondingsView.delegate = self + if let canCancel = staticsViewModel?.canCancelUnbonding { + newUnbondingsView.canCancel = canCancel + } + if let stateView = stateContainerView { stackView.insertArranged(view: newUnbondingsView, after: stateView) } else { - stackView.addArrangedSubview(newUnbondingsView) + stackView.insertArranged(view: newUnbondingsView, before: networkInfoContainerView) } newUnbondingsView.snp.makeConstraints { make in @@ -289,20 +359,6 @@ final class StakingMainViewController: UIViewController, AdaptiveDesignable, Vie return stateView } - private func setupRewardEstimationViewIfNeeded() -> RewardEstimationView? { - if let rewardView = stateView as? RewardEstimationView { - return rewardView - } - - let size = CGSize(width: 343, height: 202.0) - let stateView = setupView { RewardEstimationView(frame: CGRect(origin: .zero, size: size)) } - - stateView?.locale = localizationManager?.selectedLocale ?? Locale.current - stateView?.delegate = self - - return stateView - } - private func setupNominatorViewIfNeeded() -> NominatorStateView? { if let nominatorView = stateView as? NominatorStateView { return nominatorView @@ -334,17 +390,6 @@ final class StakingMainViewController: UIViewController, AdaptiveDesignable, Vie nominatorView?.bind(viewModel: viewModel) } - private func applyBonded(viewModel: StakingEstimationViewModel) { - let rewardView = setupRewardEstimationViewIfNeeded() - rewardView?.bind(viewModel: viewModel) - } - - private func applyNoStash(viewModel: StakingEstimationViewModel) { - let rewardView = setupRewardEstimationViewIfNeeded() - rewardView?.bind(viewModel: viewModel) - scrollView.layoutIfNeeded() - } - private func applyValidator(viewModel: LocalizableResource) { let validatorView = setupValidatorViewIfNeeded() validatorView?.bind(viewModel: viewModel) @@ -359,6 +404,12 @@ final class StakingMainViewController: UIViewController, AdaptiveDesignable, Vie private func applyStakingReward(viewModel: LocalizableResource) { setupStakingRewardViewIfNeeded() rewardView?.bind(viewModel: viewModel) + + rewardView?.claimButton?.addTarget( + self, + action: #selector(claimRewardsAction), + for: .touchUpInside + ) } } @@ -382,20 +433,12 @@ extension StakingMainViewController: Localizable { } } -extension StakingMainViewController: RewardEstimationViewDelegate { - func rewardEstimationDidStartAction(_: RewardEstimationView) { - presenter.performMainAction() - } - - func rewardEstimationDidRequestInfo(_: RewardEstimationView) { - presenter.performRewardInfoAction() +extension StakingMainViewController: StakingMainViewProtocol { + func didReceiveSelectedEntity(_ entity: StakingSelectedEntityViewModel) { + setupEntityView(for: entity) } -} -extension StakingMainViewController: StakingMainViewProtocol { - func didRecieveNetworkStakingInfo( - viewModel: LocalizableResource? - ) { + func didRecieveNetworkStakingInfo(viewModel: NetworkStakingInfoViewModel) { networkInfoView.bind(viewModel: viewModel) } @@ -413,17 +456,6 @@ extension StakingMainViewController: StakingMainViewProtocol { switch viewModel { case .undefined: clearStateView() - clearStakingRewardViewIfNeeded() - updateActionsView(for: nil) - updateUnbondingsView(for: nil) - case let .noStash(viewModel, alerts): - applyNoStash(viewModel: viewModel) - applyAlerts(alerts) - - if !hasSameTypes { - expandNetworkInfoView(true) - } - clearStakingRewardViewIfNeeded() updateActionsView(for: nil) updateUnbondingsView(for: nil) @@ -471,6 +503,7 @@ extension StakingMainViewController: StakingMainViewProtocol { networkInfoView.statics = viewModel actionsView?.statics = viewModel + unbondingsView?.canCancel = viewModel.canCancelUnbonding if let stateView = stateView as? StakingStateView { stateView.statics = viewModel @@ -494,20 +527,7 @@ extension StakingMainViewController: NetworkInfoViewDelegate { extension StakingMainViewController: AlertsViewDelegate { func didSelectStakingAlert(_ alert: StakingAlert) { - switch alert { - case .nominatorChangeValidators, .nominatorAllOversubscribed: - presenter.performChangeValidatorsAction() - case .bondedSetValidators: - presenter.performSetupValidatorsForBondedAction() - case .nominatorLowStake: - presenter.performStakeMoreAction() - case .redeemUnbonded: - presenter.performRedeemAction() - case .rebag: - presenter.performRebag() - case .waitingNextEra: - break - } + presenter.performAlertAction(alert) } } diff --git a/novawallet/Modules/Staking/StakingMain/StakingMainViewFactory.swift b/novawallet/Modules/Staking/StakingMain/StakingMainViewFactory.swift index ffa4e2a0a4..ae7f455865 100644 --- a/novawallet/Modules/Staking/StakingMain/StakingMainViewFactory.swift +++ b/novawallet/Modules/Staking/StakingMain/StakingMainViewFactory.swift @@ -14,13 +14,25 @@ enum StakingMainViewFactory { let applicationHandler = SecurityLayerService.shared.applicationHandlingProxy .addApplicationHandler() + let sharedStateFactory = StakingSharedStateFactory( + storageFacade: SubstrateDataStorageFacade.shared, + chainRegistry: ChainRegistryFacade.sharedRegistry, + eventCenter: EventCenter.shared, + syncOperationQueue: OperationManagerFacade.sharedDefaultQueue, + repositoryOperationQueue: OperationManagerFacade.sharedDefaultQueue, + logger: Logger.shared + ) + + let childPresenterFactory = StakingMainPresenterFactory( + applicationHandler: applicationHandler, + sharedStateFactory: sharedStateFactory + ) + let presenter = StakingMainPresenter( interactor: interactor, wireframe: wireframe, - wallet: SelectedWalletSettings.shared.value, stakingOption: stakingOption, - accountManagementFilter: AccountManagementFilter(), - childPresenterFactory: StakingMainPresenterFactory(applicationHandler: applicationHandler), + childPresenterFactory: childPresenterFactory, viewModelFactory: StakingMainViewModelFactory(), logger: Logger.shared ) diff --git a/novawallet/Modules/Staking/StakingMain/View/NetworkInfoView.swift b/novawallet/Modules/Staking/StakingMain/View/NetworkInfoView.swift index 990a26e305..a25baf82bb 100644 --- a/novawallet/Modules/Staking/StakingMain/View/NetworkInfoView.swift +++ b/novawallet/Modules/Staking/StakingMain/View/NetworkInfoView.swift @@ -38,8 +38,7 @@ final class NetworkInfoView: UIView { control.imageView.image = R.image.iconArrowUp()?.tinted(with: R.color.colorIconSecondary()!) control.identityIconAngle = CGFloat.pi control.activationIconAngle = 0.0 - control.titleLabel.textColor = R.color.colorTextPrimary() - control.titleLabel.font = .regularSubheadline + control.titleLabel.apply(style: .regularSubhedlineSecondary) control.layoutType = .flexible control.contentInsets = Constants.contentMargins control.horizontalSpacing = 0.0 @@ -97,7 +96,7 @@ final class NetworkInfoView: UIView { var expanded: Bool { titleControl.isActivated } - private var skeletonView: SkrullableView? + var skeletonView: SkrullableView? var locale = Locale.current { didSet { @@ -112,7 +111,7 @@ final class NetworkInfoView: UIView { } } - private var localizableViewModel: LocalizableResource? + private var viewModel: NetworkStakingInfoViewModel? override init(frame: CGRect) { super.init(frame: frame) @@ -129,8 +128,9 @@ final class NetworkInfoView: UIView { override func layoutSubviews() { super.layoutSubviews() - if skeletonView != nil { - setupSkeleton() + if viewModel?.hasLoadingData == true { + updateLoadingState() + skeletonView?.restartSkrulling() } } @@ -148,15 +148,15 @@ final class NetworkInfoView: UIView { applyExpansion(animated: animated) } - func bind(viewModel: LocalizableResource?) { - localizableViewModel = viewModel + func bind(viewModel: NetworkStakingInfoViewModel) { + stopLoadingIfNeeded() - if viewModel != nil { - stopLoadingIfNeeded() + self.viewModel = viewModel - applyViewModel() - } else { - startLoading() + applyViewModel() + + if viewModel.hasLoadingData { + startLoadingIfNeeded() } } @@ -211,43 +211,40 @@ final class NetworkInfoView: UIView { } } - private func applyViewModel() { - guard let viewModel = localizableViewModel else { - return - } + private func applyCell(viewModel: LoadableViewModelState<(String?, String?)>?, to cell: TitleMultiValueView) { + if let loadableViewModel = viewModel { + cell.isHidden = false - let localizedViewModel = viewModel.value(for: locale) + let title = loadableViewModel.value?.0 + let optSubtitle = loadableViewModel.value?.1 - totalStakedView.valueTop.text = localizedViewModel.totalStake?.amount + cell.valueTop.text = title - if let price = localizedViewModel.totalStake?.price, !price.isEmpty { - totalStakedView.valueBottom.text = price + if let subtitle = optSubtitle, !subtitle.isEmpty { + cell.valueBottom.text = subtitle + } else { + cell.resetToSingleValue() + } } else { - totalStakedView.resetToSingleValue() + cell.isHidden = true } + } - minimumStakedView.valueTop.text = localizedViewModel.minimalStake?.amount - - if let price = localizedViewModel.minimalStake?.price, !price.isEmpty { - minimumStakedView.valueBottom.text = price - } else { - minimumStakedView.resetToSingleValue() + private func applyViewModel() { + guard let viewModel = viewModel else { + return } - activeNominatorsView.valueTop.text = localizedViewModel.activeNominators - stakingPeriodView.valueTop.text = localizedViewModel.stakingPeriod - unstakingPeriodView.valueTop.text = localizedViewModel.lockUpPeriod + applyCell(viewModel: viewModel.totalStake?.map(with: { ($0.amount, $0.price) }), to: totalStakedView) + applyCell(viewModel: viewModel.minimalStake?.map(with: { ($0.amount, $0.price) }), to: minimumStakedView) + applyCell(viewModel: viewModel.activeNominators?.map(with: { ($0, nil) }), to: activeNominatorsView) + applyCell(viewModel: viewModel.stakingPeriod?.map(with: { ($0, nil) }), to: stakingPeriodView) + applyCell(viewModel: viewModel.lockUpPeriod?.map(with: { ($0, nil) }), to: unstakingPeriodView) } private func applyLocalization() { let languages = locale.rLanguages - titleControl.titleLabel.text = R.string.localizable.stakingNetworkInfoTitle( - preferredLanguages: languages - ) - - titleControl.invalidateLayout() - totalStakedView.titleLabel.text = R.string.localizable .stakingMainTotalStakedTitle(preferredLanguages: languages) minimumStakedView.titleLabel.text = R.string.localizable @@ -255,11 +252,19 @@ final class NetworkInfoView: UIView { if let statics = statics { activeNominatorsView.titleLabel.text = statics.networkInfoActiveNominators(for: locale) + + titleControl.titleLabel.text = statics.networkInfoTitle(for: locale) } else { activeNominatorsView.titleLabel.text = R.string.localizable .stakingMainActiveNominatorsTitle(preferredLanguages: languages) + + titleControl.titleLabel.text = R.string.localizable.stakingNetworkInfoTitle( + preferredLanguages: languages + ) } + titleControl.invalidateLayout() + stakingPeriodView.titleLabel.text = R.string.localizable.stakingNetworkInfoStakingPeriodTitle( preferredLanguages: languages ) @@ -311,80 +316,55 @@ final class NetworkInfoView: UIView { } } - func startLoading() { - guard skeletonView == nil else { - return - } - - totalStakedView.valueTop.alpha = 0.0 - totalStakedView.valueBottom.alpha = 0.0 - minimumStakedView.valueTop.alpha = 0.0 - minimumStakedView.valueBottom.alpha = 0.0 - activeNominatorsView.valueTop.alpha = 0.0 - stakingPeriodView.valueTop.alpha = 0.0 - unstakingPeriodView.valueTop.alpha = 0.0 + // MARK: Action - setupSkeleton() + @objc func actionToggleExpansion() { + applyExpansion(animated: true) } +} - func stopLoadingIfNeeded() { - guard skeletonView != nil else { - return - } - - skeletonView?.stopSkrulling() - skeletonView?.removeFromSuperview() - skeletonView = nil - - totalStakedView.valueTop.alpha = 1.0 - totalStakedView.valueBottom.alpha = 1.0 - minimumStakedView.valueTop.alpha = 1.0 - minimumStakedView.valueBottom.alpha = 1.0 - activeNominatorsView.valueTop.alpha = 1.0 - stakingPeriodView.valueTop.alpha = 1.0 - unstakingPeriodView.valueTop.alpha = 1.0 +extension NetworkInfoView: SkeletonableView { + var skeletonSuperview: UIView { + contentView } - private func setupSkeleton() { - let spaceSize = CGSize( - width: frame.width, - height: 5 * Constants.rowHeight + Constants.stackViewBottomInset - ) - - guard spaceSize.width > 0, spaceSize.height > 0 else { - return + var hidingViews: [UIView] { + guard let viewModel = viewModel, viewModel.hasLoadingData else { + return [] } - let builder = Skrull( - size: spaceSize, - decorations: [], - skeletons: createSkeletons(for: spaceSize) - ) + var views: [UIView] = [] - let currentSkeletonView: SkrullableView? + if viewModel.totalStake?.isLoading == true { + views.append(totalStakedView.valueTop) + views.append(totalStakedView.valueBottom) + } - if let skeletonView = skeletonView { - currentSkeletonView = skeletonView - builder.updateSkeletons(in: skeletonView) - } else { - let view = builder - .fillSkeletonStart(R.color.colorSkeletonStart()!) - .fillSkeletonEnd(color: R.color.colorSkeletonEnd()!) - .build() - view.autoresizingMask = [] - contentView.insertSubview(view, at: 0) + if viewModel.minimalStake?.isLoading == true { + views.append(minimumStakedView.valueTop) + views.append(minimumStakedView.valueBottom) + } - skeletonView = view + if viewModel.activeNominators?.isLoading == true { + views.append(activeNominatorsView.valueTop) + views.append(activeNominatorsView.valueBottom) + } - view.startSkrulling() + if viewModel.stakingPeriod?.isLoading == true { + views.append(stakingPeriodView.valueTop) + views.append(stakingPeriodView.valueBottom) + } - currentSkeletonView = view + if viewModel.lockUpPeriod?.isLoading == true { + views.append(unstakingPeriodView.valueTop) + views.append(unstakingPeriodView.valueBottom) } - currentSkeletonView?.frame = CGRect(origin: .zero, size: spaceSize) + return views } - private func createSkeletons(for spaceSize: CGSize) -> [Skeletonable] { + // swiftlint:disable:next function_body_length + func createSkeletons(for spaceSize: CGSize) -> [Skeletonable] { let bigRowSize = CGSize(width: 72.0, height: 12.0) let smallRowSize = CGSize(width: 57.0, height: 6.0) @@ -405,69 +385,85 @@ final class NetworkInfoView: UIView { y: Constants.rowHeight / 2.0 - bigRowSize.height / 2.0 ) - return [ - SingleSkeleton.createRow( - on: totalStakedView, - containerView: contentView, - spaceSize: spaceSize, - offset: doubleValueBigOffset, - size: bigRowSize - ), - - SingleSkeleton.createRow( - on: totalStakedView, - containerView: contentView, - spaceSize: spaceSize, - offset: doubleValueSmallOffset, - size: smallRowSize - ), - - SingleSkeleton.createRow( - on: minimumStakedView, - containerView: contentView, - spaceSize: spaceSize, - offset: doubleValueBigOffset, - size: bigRowSize - ), - - SingleSkeleton.createRow( - on: minimumStakedView, - containerView: contentView, - spaceSize: spaceSize, - offset: doubleValueSmallOffset, - size: smallRowSize - ), - - SingleSkeleton.createRow( - on: activeNominatorsView, - containerView: contentView, - spaceSize: spaceSize, - offset: singleValueOffset, - size: bigRowSize - ), - - SingleSkeleton.createRow( - on: stakingPeriodView, - containerView: contentView, - spaceSize: spaceSize, - offset: singleValueOffset, - size: bigRowSize - ), - - SingleSkeleton.createRow( - on: unstakingPeriodView, - containerView: contentView, - spaceSize: spaceSize, - offset: singleValueOffset, - size: bigRowSize + var skeletons: [Skeletonable] = [] + + if viewModel?.totalStake?.isLoading == true { + skeletons.append(contentsOf: [ + SingleSkeleton.createRow( + on: totalStakedView, + containerView: contentView, + spaceSize: spaceSize, + offset: doubleValueBigOffset, + size: bigRowSize + ), + + SingleSkeleton.createRow( + on: totalStakedView, + containerView: contentView, + spaceSize: spaceSize, + offset: doubleValueSmallOffset, + size: smallRowSize + ) + ]) + } + + if viewModel?.minimalStake?.isLoading == true { + skeletons.append(contentsOf: [ + SingleSkeleton.createRow( + on: minimumStakedView, + containerView: contentView, + spaceSize: spaceSize, + offset: doubleValueBigOffset, + size: bigRowSize + ), + + SingleSkeleton.createRow( + on: minimumStakedView, + containerView: contentView, + spaceSize: spaceSize, + offset: doubleValueSmallOffset, + size: smallRowSize + ) + ]) + } + + if viewModel?.activeNominators?.isLoading == true { + skeletons.append( + SingleSkeleton.createRow( + on: activeNominatorsView, + containerView: contentView, + spaceSize: spaceSize, + offset: singleValueOffset, + size: bigRowSize + ) ) - ] - } + } - // MARK: Action + if viewModel?.stakingPeriod?.isLoading == true { + skeletons.append( + SingleSkeleton.createRow( + on: stakingPeriodView, + containerView: contentView, + spaceSize: spaceSize, + offset: singleValueOffset, + size: bigRowSize + ) + ) + } - @objc func actionToggleExpansion() { - applyExpansion(animated: true) + if viewModel?.lockUpPeriod?.isLoading == true { + skeletons.append( + SingleSkeleton.createRow( + on: unstakingPeriodView, + containerView: contentView, + spaceSize: spaceSize, + offset: singleValueOffset, + size: bigRowSize + ) + ) + } + + return skeletons } } @@ -477,8 +473,7 @@ extension NetworkInfoView: SkeletonLoadable { } func didAppearSkeleton() { - skeletonView?.stopSkrulling() - skeletonView?.startSkrulling() + skeletonView?.restartSkrulling() } func didUpdateSkeletonLayout() { @@ -486,6 +481,7 @@ extension NetworkInfoView: SkeletonLoadable { return } - setupSkeleton() + updateLoadingState() + skeletonView?.restartSkrulling() } } diff --git a/novawallet/Modules/Staking/StakingMain/View/StakingClaimableRewardView.swift b/novawallet/Modules/Staking/StakingMain/View/StakingClaimableRewardView.swift new file mode 100644 index 0000000000..19da12d3e3 --- /dev/null +++ b/novawallet/Modules/Staking/StakingMain/View/StakingClaimableRewardView.swift @@ -0,0 +1,150 @@ +import UIKit +import SoraUI + +final class StakingClaimableRewardView: UIView { + let backgroundView: BlockBackgroundView = .create { view in + view.sideLength = 10 + } + + let contentView: GenericTitleValueView = .create { view in + view.titleView.valueTop.apply(style: .regularSubhedlinePrimary) + view.titleView.valueTop.textAlignment = .left + view.titleView.valueBottom.apply(style: .footnoteSecondary) + view.titleView.valueBottom.textAlignment = .left + view.titleView.spacing = 0.0 + + view.valueView.applyDefaultStyle() + view.valueView.contentInsets = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12) + } + + var rewardView: MultiValueView { + contentView.titleView + } + + var actionButton: TriangularedButton { + contentView.valueView + } + + var skeletonView: SkrullableView? + + private var isLoading: Bool = false + + override var intrinsicContentSize: CGSize { + CGSize(width: UIView.noIntrinsicMetric, height: 70) + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .clear + + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func bind(viewModel: LoadableViewModelState) { + stopLoadingIfNeeded() + + switch viewModel { + case let .loaded(claimableViewModel), let .cached(claimableViewModel): + rewardView.bind( + topValue: claimableViewModel.balance.amount, + bottomValue: claimableViewModel.balance.price + ) + + if claimableViewModel.canClaim { + actionButton.applyEnabledStyle() + actionButton.isEnabled = true + } else { + actionButton.applyTranslucentDisabledStyle() + actionButton.isEnabled = false + } + + case .loading: + startLoadingIfNeeded() + } + } + + override func layoutSubviews() { + super.layoutSubviews() + + if isLoading { + updateLoadingState() + skeletonView?.restartSkrulling() + } + } + + private func setupLayout() { + addSubview(backgroundView) + backgroundView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + addSubview(contentView) + contentView.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(16) + } + } +} + +extension StakingClaimableRewardView: SkeletonableView { + var skeletonSuperview: UIView { + self + } + + var hidingViews: [UIView] { + [rewardView.valueTop, rewardView.valueBottom, actionButton] + } + + func createSkeletons(for spaceSize: CGSize) -> [Skeletonable] { + [ + SingleSkeleton.createRow( + on: self, + containerView: self, + spaceSize: spaceSize, + offset: CGPoint(x: 16, y: 21), + size: CGSize(width: 90, height: 10) + ), + SingleSkeleton.createRow( + on: self, + containerView: self, + spaceSize: spaceSize, + offset: CGPoint(x: 16, y: 41), + size: CGSize(width: 43, height: 8) + ) + ] + } + + func didStartSkeleton() { + isLoading = true + } + + func didStopSkeleton() { + isLoading = false + } +} + +extension StakingClaimableRewardView: SkeletonLoadable { + func didDisappearSkeleton() { + if isLoading { + skeletonView?.stopSkrulling() + } + } + + func didAppearSkeleton() { + if isLoading { + skeletonView?.restartSkrulling() + } + } + + func didUpdateSkeletonLayout() { + if isLoading { + updateLoadingState() + skeletonView?.restartSkrulling() + } + } +} diff --git a/novawallet/Modules/Staking/StakingMain/View/StakingRewardView.swift b/novawallet/Modules/Staking/StakingMain/View/StakingRewardView.swift index 53beee3e03..ebea9420dd 100644 --- a/novawallet/Modules/Staking/StakingMain/View/StakingRewardView.swift +++ b/novawallet/Modules/Staking/StakingMain/View/StakingRewardView.swift @@ -2,52 +2,35 @@ import UIKit import SoraUI import SoraFoundation -struct StakingRewardSkeletonOptions: OptionSet { - typealias RawValue = UInt8 - - static let reward = StakingRewardSkeletonOptions(rawValue: 1 << 0) - static let price = StakingRewardSkeletonOptions(rawValue: 1 << 1) +final class StakingRewardView: UIView { + let backgroundView: UIView = .create { view in + view.backgroundColor = R.color.colorRewardsBackground() + view.layer.cornerRadius = 12 + view.clipsToBounds = true + } - let rawValue: UInt8 + let borderView: RoundedView = .create { view in + view.applyStrokedBackgroundStyle() - init(rawValue: RawValue) { - self.rawValue = rawValue + view.cornerRadius = 12 + view.strokeColor = R.color.colorContainerBorder()! + view.strokeWidth = 1 } -} -final class StakingRewardView: UIView { - let backgroundView: UIImageView = { - let view = UIImageView() - view.contentMode = .scaleAspectFill - view.layer.cornerRadius = 12.0 - view.clipsToBounds = true - view.image = R.image.imageStakingReward() - return view - }() - - let titleLabel: UILabel = { - let label = UILabel() - label.textColor = R.color.colorTextSecondary() - label.font = .regularSubheadline - return label - }() - - let filterView = BorderedActionControlView() - - let rewardView: MultiValueView = { - let view = MultiValueView() - view.valueTop.textColor = R.color.colorTextPrimary() - view.valueTop.textAlignment = .left - view.valueTop.font = .boldTitle2 - view.valueBottom.textColor = R.color.colorTextSecondary() - view.valueBottom.textAlignment = .left - view.valueBottom.font = .regularSubheadline - view.spacing = 4.0 - return view - }() - - private var skeletonView: SkrullableView? - private var skeletonOptions: StakingRewardSkeletonOptions? + let graphicsView = UIImageView() + + let totalRewardView = StakingTotalRewardView() + private var claimableRewardView: StakingClaimableRewardView? + + var claimButton: TriangularedButton? { claimableRewardView?.actionButton } + var filterView: BorderedActionControlView { totalRewardView.filterView } + + let stackView: UIStackView = .create { view in + view.axis = .vertical + view.spacing = 16 + view.isLayoutMarginsRelativeArrangement = true + view.layoutMargins = UIEdgeInsets(top: 20, left: 16, bottom: 12, right: 16) + } private var viewModel: LocalizableResource? @@ -65,23 +48,11 @@ final class StakingRewardView: UIView { setupLocalization() } - override var intrinsicContentSize: CGSize { - CGSize(width: UIView.noIntrinsicMetric, height: 116.0) - } - @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func layoutSubviews() { - super.layoutSubviews() - - if let options = skeletonOptions { - setupSkeleton(for: options) - } - } - func bind(viewModel: LocalizableResource) { self.viewModel = viewModel applyViewModel() @@ -89,40 +60,60 @@ final class StakingRewardView: UIView { private func applyViewModel() { guard let viewModel = viewModel?.value(for: locale) else { - rewardView.bind(topValue: "", bottomValue: nil) - setupSkeleton(for: [.reward, .price]) + totalRewardView.bind(totalRewards: .loading, filter: nil, hasPrice: true) + clearClaimableRewardsView() + graphicsView.image = nil return } - let title = viewModel.amount.value ?? "" - let price: String? = viewModel.price?.value - rewardView.bind(topValue: title, bottomValue: price) + graphicsView.image = viewModel.graphics - var newSkeletonOptions: StakingRewardSkeletonOptions = [] + totalRewardView.bind( + totalRewards: viewModel.totalRewards, + filter: viewModel.filter, + hasPrice: viewModel.hasPrice + ) - if title.isEmpty { - newSkeletonOptions.insert(.reward) - newSkeletonOptions.insert(.price) + if let claimableRewards = viewModel.claimableRewards { + setupClaimableRewardsViewIfNeeded() + claimableRewardView?.bind(viewModel: claimableRewards) + } else { + clearClaimableRewardsView() } - if let price = price, price.isEmpty { - newSkeletonOptions.insert(.price) - } + invalidateIntrinsicContentSize() + setNeedsLayout() + } - if let filter = viewModel.filter { - filterView.isHidden = false - filterView.bind(title: filter) - } else { - filterView.isHidden = true + func setupClaimableRewardsViewIfNeeded() { + guard claimableRewardView == nil else { + return } - setupSkeleton(for: newSkeletonOptions) + let view = StakingClaimableRewardView() + stackView.addArrangedSubview(view) + claimableRewardView = view + + setupClaimButtonLocalization() + } + + func clearClaimableRewardsView() { + claimableRewardView?.removeFromSuperview() + claimableRewardView = nil } private func setupLocalization() { let languages = locale.rLanguages - titleLabel.text = R.string.localizable.stakingRewardsTitle(preferredLanguages: languages) + totalRewardView.titleLabel.text = R.string.localizable.stakingRewardsTitle(preferredLanguages: languages) + setupClaimButtonLocalization() + } + + private func setupClaimButtonLocalization() { + claimButton?.imageWithTitleView?.title = R.string.localizable.stakingClaimRewards( + preferredLanguages: locale.rLanguages + ) + claimButton?.invalidateLayout() } private func setupLayout() { @@ -131,124 +122,38 @@ final class StakingRewardView: UIView { make.edges.equalToSuperview() } - addSubview(titleLabel) - titleLabel.snp.makeConstraints { make in - make.leading.equalToSuperview().inset(UIConstants.horizontalInset) - make.top.equalToSuperview().inset(20.0) + backgroundView.addSubview(graphicsView) + graphicsView.snp.makeConstraints { make in + make.top.right.equalToSuperview() } - addSubview(filterView) - filterView.snp.makeConstraints { make in - make.leading.equalTo(titleLabel.snp.trailing).offset(8.0) - make.trailing.lessThanOrEqualToSuperview().inset(UIConstants.horizontalInset) - make.centerY.equalTo(titleLabel.snp.centerY) - } - - addSubview(rewardView) - rewardView.snp.makeConstraints { make in - make.leading.equalToSuperview().inset(UIConstants.horizontalInset) - make.top.equalTo(titleLabel.snp.bottom).offset(4.0) - } - } - - private func setupSkeleton(for options: StakingRewardSkeletonOptions) { - skeletonOptions = nil - - guard !options.isEmpty else { - skeletonView?.removeFromSuperview() - skeletonView = nil - return - } - - skeletonOptions = options - - let spaceSize = frame.size - - guard spaceSize.width > 0.0, spaceSize.height > 0.0 else { - return - } - - let skeletons = createSkeletons(for: spaceSize, options: options) - - let builder = Skrull( - size: spaceSize, - decorations: [], - skeletons: skeletons - ) - - let currentSkeletonView: SkrullableView? - - if let skeletonView = skeletonView { - currentSkeletonView = skeletonView - builder.updateSkeletons(in: skeletonView) - } else { - let view = builder - .fillSkeletonStart(R.color.colorSkeletonStart()!) - .fillSkeletonEnd(color: R.color.colorSkeletonEnd()!) - .build() - view.autoresizingMask = [] - insertSubview(view, aboveSubview: backgroundView) - - currentSkeletonView = view - skeletonView = view - - view.startSkrulling() - } - - currentSkeletonView?.frame = CGRect(origin: .zero, size: spaceSize) - } - - private func createSkeletons( - for spaceSize: CGSize, - options: StakingRewardSkeletonOptions - ) -> [Skeletonable] { - var skeletons: [Skeletonable] = [] - - if options.contains(StakingRewardSkeletonOptions.reward) { - let offset = CGPoint(x: 0.0, y: 12.0) - skeletons.append( - SingleSkeleton.createRow( - under: titleLabel, - containerView: backgroundView, - spaceSize: spaceSize, - offset: offset, - size: UIConstants.skeletonBigRowSize - ) - ) + addSubview(borderView) + borderView.snp.makeConstraints { make in + make.edges.equalToSuperview() } - if options.contains(StakingRewardSkeletonOptions.price) { - let offset = CGPoint(x: 0.0, y: 41.0) - skeletons.append( - SingleSkeleton.createRow( - under: titleLabel, - containerView: backgroundView, - spaceSize: spaceSize, - offset: offset, - size: UIConstants.skeletonSmallRowSize - ) - ) + addSubview(stackView) + stackView.snp.makeConstraints { make in + make.edges.equalToSuperview() } - return skeletons + stackView.addArrangedSubview(totalRewardView) } } extension StakingRewardView: SkeletonLoadable { func didDisappearSkeleton() { - skeletonView?.stopSkrulling() + totalRewardView.didDisappearSkeleton() + claimableRewardView?.didDisappearSkeleton() } func didAppearSkeleton() { - skeletonView?.stopSkrulling() - skeletonView?.startSkrulling() + totalRewardView.didAppearSkeleton() + claimableRewardView?.didAppearSkeleton() } func didUpdateSkeletonLayout() { - guard let skeletonOptions = skeletonOptions else { - return - } - - setupSkeleton(for: skeletonOptions) + totalRewardView.didUpdateSkeletonLayout() + claimableRewardView?.didUpdateSkeletonLayout() } } diff --git a/novawallet/Modules/Staking/StakingMain/View/StakingTotalRewardView.swift b/novawallet/Modules/Staking/StakingMain/View/StakingTotalRewardView.swift new file mode 100644 index 0000000000..f6ff94909e --- /dev/null +++ b/novawallet/Modules/Staking/StakingMain/View/StakingTotalRewardView.swift @@ -0,0 +1,162 @@ +import UIKit +import SoraUI + +final class StakingTotalRewardView: UIView { + let titleLabel: UILabel = .create { label in + label.apply(style: .regularSubhedlineSecondary) + } + + let filterView = BorderedActionControlView() + + let rewardView: MultiValueView = .create { view in + view.valueTop.textColor = R.color.colorTextPrimary() + view.valueTop.textAlignment = .left + view.valueTop.font = .boldTitle2 + view.valueBottom.textColor = R.color.colorTextSecondary() + view.valueBottom.textAlignment = .left + view.valueBottom.font = .regularSubheadline + view.spacing = 4.0 + } + + var skeletonView: SkrullableView? + + private var isLoading: Bool = false + + private var hasPrice: Bool = true + + override var intrinsicContentSize: CGSize { + let height: CGFloat = hasPrice ? 80 : 60 + + return CGSize(width: UIView.noIntrinsicMetric, height: height) + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .clear + + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + if isLoading { + updateLoadingState() + skeletonView?.restartSkrulling() + } + } + + func bind(totalRewards: LoadableViewModelState, filter: String?, hasPrice: Bool) { + stopLoadingIfNeeded() + + let title = totalRewards.value?.amount ?? "" + let price = totalRewards.value?.price + rewardView.bind(topValue: title, bottomValue: price) + + self.hasPrice = hasPrice + + if let filter = filter { + filterView.isHidden = false + filterView.bind(title: filter) + } else { + filterView.isHidden = true + } + + if totalRewards.isLoading { + startLoadingIfNeeded() + } + + invalidateIntrinsicContentSize() + setNeedsLayout() + } + + private func setupLayout() { + addSubview(titleLabel) + titleLabel.snp.makeConstraints { make in + make.leading.equalToSuperview() + make.top.equalToSuperview() + } + + addSubview(filterView) + filterView.snp.makeConstraints { make in + make.leading.equalTo(titleLabel.snp.trailing).offset(8.0) + make.trailing.lessThanOrEqualToSuperview() + make.centerY.equalTo(titleLabel.snp.centerY) + } + + addSubview(rewardView) + rewardView.snp.makeConstraints { make in + make.leading.equalToSuperview() + make.top.equalTo(titleLabel.snp.bottom).offset(8.0) + } + } +} + +extension StakingTotalRewardView: SkeletonableView { + var skeletonSuperview: UIView { + self + } + + var hidingViews: [UIView] { + [rewardView.valueTop, rewardView.valueBottom, filterView] + } + + func createSkeletons(for spaceSize: CGSize) -> [Skeletonable] { + let titleSkeleton = SingleSkeleton.createRow( + on: self, + containerView: self, + spaceSize: spaceSize, + offset: CGPoint(x: 0.0, y: 35.0), + size: UIConstants.skeletonBigRowSize + ) + + if hasPrice { + let priceSkeleton = SingleSkeleton.createRow( + on: self, + containerView: self, + spaceSize: spaceSize, + offset: CGPoint(x: 0.0, y: 65.0), + size: UIConstants.skeletonSmallRowSize + ) + + return [titleSkeleton, priceSkeleton] + } else { + return [titleSkeleton] + } + } + + func didStartSkeleton() { + isLoading = true + } + + func didStopSkeleton() { + isLoading = false + } +} + +extension StakingTotalRewardView: SkeletonLoadable { + func didDisappearSkeleton() { + if isLoading { + skeletonView?.stopSkrulling() + } + } + + func didAppearSkeleton() { + if isLoading { + skeletonView?.restartSkrulling() + } + } + + func didUpdateSkeletonLayout() { + if isLoading { + updateLoadingState() + skeletonView?.restartSkrulling() + } + } +} diff --git a/novawallet/Modules/Staking/StakingMain/View/StakingUnbondingsView.swift b/novawallet/Modules/Staking/StakingMain/View/StakingUnbondingsView.swift index dab359d80e..f6ec129d07 100644 --- a/novawallet/Modules/Staking/StakingMain/View/StakingUnbondingsView.swift +++ b/novawallet/Modules/Staking/StakingMain/View/StakingUnbondingsView.swift @@ -42,6 +42,16 @@ final class StakingUnbondingsView: UIView { return button }() + var canCancel: Bool { + get { + !cancelButton.isHidden + } + + set { + cancelButton.isHidden = !newValue + } + } + var locale = Locale.current { didSet { if oldValue != locale { @@ -137,21 +147,20 @@ final class StakingUnbondingsView: UIView { make.trailing.equalTo(backgroundView).offset(-16.0) } - addSubview(cancelButton) - cancelButton.snp.makeConstraints { make in - make.top.equalTo(stackView.snp.bottom).offset(16.0) - make.leading.equalTo(backgroundView).offset(16.0) - make.trailing.equalTo(backgroundView.snp.centerX).offset(-8.0) - make.height.equalTo(44.0) - make.bottom.equalToSuperview().inset(20.0) - } + let buttonsView = UIView.hStack( + alignment: .fill, + distribution: .fillEqually, + spacing: 16, + margins: nil, + [cancelButton, redeemButton] + ) - addSubview(redeemButton) - redeemButton.snp.makeConstraints { make in + addSubview(buttonsView) + buttonsView.snp.makeConstraints { make in make.top.equalTo(stackView.snp.bottom).offset(16.0) - make.leading.equalTo(backgroundView.snp.centerX).offset(8.0) - make.trailing.equalTo(backgroundView).offset(-16.0) + make.leading.trailing.equalTo(backgroundView).inset(16.0) make.height.equalTo(44.0) + make.bottom.equalToSuperview().inset(20.0) } } diff --git a/novawallet/Modules/Staking/StakingMain/ViewModel/NetworkStakingInfoViewModel.swift b/novawallet/Modules/Staking/StakingMain/ViewModel/NetworkStakingInfoViewModel.swift index 1f0fcca621..c34afc981d 100644 --- a/novawallet/Modules/Staking/StakingMain/ViewModel/NetworkStakingInfoViewModel.swift +++ b/novawallet/Modules/Staking/StakingMain/ViewModel/NetworkStakingInfoViewModel.swift @@ -2,9 +2,29 @@ import Foundation import SoraFoundation struct NetworkStakingInfoViewModel { - let totalStake: BalanceViewModelProtocol? - let minimalStake: BalanceViewModelProtocol? - let activeNominators: String? - let stakingPeriod: String? - let lockUpPeriod: String? + let totalStake: LoadableViewModelState? + let minimalStake: LoadableViewModelState? + let activeNominators: LoadableViewModelState? + let stakingPeriod: LoadableViewModelState? + let lockUpPeriod: LoadableViewModelState? + + var hasLoadingData: Bool { + totalStake?.isLoading == true || + minimalStake?.isLoading == true || + activeNominators?.isLoading == true || + stakingPeriod?.isLoading == true || + lockUpPeriod?.isLoading == true + } +} + +extension NetworkStakingInfoViewModel { + static var allLoading: NetworkStakingInfoViewModel { + .init( + totalStake: .loading, + minimalStake: .loading, + activeNominators: .loading, + stakingPeriod: .loading, + lockUpPeriod: .loading + ) + } } diff --git a/novawallet/Modules/Staking/StakingMain/ViewModel/StakingMainStaticViewModel.swift b/novawallet/Modules/Staking/StakingMain/ViewModel/StakingMainStaticViewModel.swift index 7abfb3e11e..8dd3a4ee5d 100644 --- a/novawallet/Modules/Staking/StakingMain/ViewModel/StakingMainStaticViewModel.swift +++ b/novawallet/Modules/Staking/StakingMain/ViewModel/StakingMainStaticViewModel.swift @@ -1,7 +1,37 @@ import Foundation protocol StakingMainStaticViewModelProtocol { + var canCancelUnbonding: Bool { get } + func networkInfoActiveNominators(for locale: Locale) -> String func actionsYourValidators(for locale: Locale) -> String func waitingNextEra(for timeString: String, locale: Locale) -> String + func networkInfoTitle(for locale: Locale) -> String +} + +extension StakingMainStaticViewModelProtocol { + func networkInfoActiveNominators(for locale: Locale) -> String { + R.string.localizable.stakingMainActiveNominatorsTitle( + preferredLanguages: locale.rLanguages + ) + } + + func actionsYourValidators(for locale: Locale) -> String { + R.string.localizable.stakingYourValidatorsTitle( + preferredLanguages: locale.rLanguages + ) + } + + func waitingNextEra(for timeString: String, locale: Locale) -> String { + R.string.localizable.stakingWaitingNextEraFormat( + timeString, + preferredLanguages: locale.rLanguages + ).uppercased() + } + + func networkInfoTitle(for locale: Locale) -> String { + R.string.localizable.stakingNetworkInfoTitle(preferredLanguages: locale.rLanguages) + } + + var canCancelUnbonding: Bool { true } } diff --git a/novawallet/Modules/Staking/StakingMain/ViewModel/StakingRewardViewModel.swift b/novawallet/Modules/Staking/StakingMain/ViewModel/StakingRewardViewModel.swift index 028d6e92b0..285ddbd931 100644 --- a/novawallet/Modules/Staking/StakingMain/ViewModel/StakingRewardViewModel.swift +++ b/novawallet/Modules/Staking/StakingMain/ViewModel/StakingRewardViewModel.swift @@ -1,21 +1,14 @@ -import Foundation +import UIKit struct StakingRewardViewModel { - enum ValueState { - case loading - case loaded(_ value: String) - - var value: String? { - switch self { - case .loading: - return nil - case let .loaded(value): - return value - } - } + struct ClaimableRewards { + let balance: BalanceViewModelProtocol + let canClaim: Bool } - let amount: ValueState - let price: ValueState? + let totalRewards: LoadableViewModelState + let claimableRewards: LoadableViewModelState? + let graphics: UIImage? let filter: String? + let hasPrice: Bool } diff --git a/novawallet/Modules/Staking/StakingMain/ViewModel/StakingSelectedEntityViewModel.swift b/novawallet/Modules/Staking/StakingMain/ViewModel/StakingSelectedEntityViewModel.swift new file mode 100644 index 0000000000..da7a6b816c --- /dev/null +++ b/novawallet/Modules/Staking/StakingMain/ViewModel/StakingSelectedEntityViewModel.swift @@ -0,0 +1,6 @@ +import Foundation + +struct StakingSelectedEntityViewModel { + let title: String + let loadingAddress: LoadableViewModelState +} diff --git a/novawallet/Modules/Staking/StakingMain/ViewModel/StakingViewState.swift b/novawallet/Modules/Staking/StakingMain/ViewModel/StakingViewState.swift index 066c1c7044..e151ec58bf 100644 --- a/novawallet/Modules/Staking/StakingMain/ViewModel/StakingViewState.swift +++ b/novawallet/Modules/Staking/StakingMain/ViewModel/StakingViewState.swift @@ -17,10 +17,6 @@ enum StakingViewState { unbondings: StakingUnbondingViewModel?, actions: [StakingManageOption] ) - case noStash( - viewModel: StakingEstimationViewModel, - alerts: [StakingAlert] - ) var rawType: Int { switch self { @@ -30,8 +26,6 @@ enum StakingViewState { return 1 case .validator: return 2 - case .noStash: - return 3 } } } diff --git a/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsPresenter.swift b/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsPresenter.swift index 0508b27815..db1ecdb9d9 100644 --- a/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsPresenter.swift +++ b/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsPresenter.swift @@ -45,11 +45,24 @@ extension StakingMoreOptionsPresenter: StakingMoreOptionsPresenterProtocol { } func selectOption(at index: Int) { - guard let dashboardItem = moreOptions[safe: index] else { + guard let item = moreOptions[safe: index] else { return } - wireframe.showStakingDetails(from: view, option: dashboardItem.stakingOption) + switch item { + case let .concrete(concrete): + wireframe.showStartStaking( + from: view, + chainAsset: concrete.chainAsset, + stakingType: concrete.stakingOption.type + ) + case let .combined(combined): + wireframe.showStartStaking( + from: view, + chainAsset: combined.chainAsset, + stakingType: nil + ) + } } func selectDApp(at index: Int) { diff --git a/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsProtocols.swift b/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsProtocols.swift index 5bad1f6b28..d9a490c470 100644 --- a/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsProtocols.swift +++ b/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsProtocols.swift @@ -22,8 +22,9 @@ protocol StakingMoreOptionsInteractorOutputProtocol: AnyObject { protocol StakingMoreOptionsWireframeProtocol: ErrorPresentable, AlertPresentable, CommonRetryable { func showBrowser(from view: ControllerBackedProtocol?, for dApp: DApp) - func showStakingDetails( + func showStartStaking( from view: StakingMoreOptionsViewProtocol?, - option: Multistaking.ChainAssetOption + chainAsset: ChainAsset, + stakingType: StakingType? ) } diff --git a/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsViewController.swift b/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsViewController.swift index 4e03d93819..6cf0eed0da 100644 --- a/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsViewController.swift +++ b/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsViewController.swift @@ -90,7 +90,10 @@ extension StakingMoreOptionsViewController: UICollectionViewDataSource { } } - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + func collectionView( + _ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath + ) -> UICollectionViewCell { switch StakingMoreOptionsSection(rawValue: indexPath.section) { case .dApps: let cell: DAppCollectionViewCell = collectionView.dequeueReusableCell(for: indexPath)! @@ -107,14 +110,20 @@ extension StakingMoreOptionsViewController: UICollectionViewDataSource { } } - func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + func collectionView( + _ collectionView: UICollectionView, + viewForSupplementaryElementOfKind kind: String, + at indexPath: IndexPath + ) -> UICollectionReusableView { switch StakingMoreOptionsSection(rawValue: indexPath.section) { case .dApps: let header: TitleCollectionHeaderView? = collectionView.dequeueReusableSupplementaryView( forSupplementaryViewOfKind: kind, for: indexPath ) - header?.bind(title: R.string.localizable.stakingMoreOptionsDAppsTitle(preferredLanguages: selectedLocale.rLanguages)) + header?.bind( + title: R.string.localizable.stakingMoreOptionsDAppsTitle(preferredLanguages: selectedLocale.rLanguages) + ) header?.titleLabel.apply(style: .title3Primary) header?.contentInsets = .zero return header ?? .init() diff --git a/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsViewModelFactory.swift b/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsViewModelFactory.swift index e92a582611..e246d316e5 100644 --- a/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsViewModelFactory.swift +++ b/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsViewModelFactory.swift @@ -20,12 +20,16 @@ extension StakingDashboardViewModelFactory: StakingMoreOptionsViewModelFactoryPr } func createDAppModel(for dApp: DApp) -> LoadableViewModelState { - .loaded(value: + let icon: ImageViewModelProtocol = dApp.icon.map { RemoteImageViewModel(url: $0) } ?? + StaticImageViewModel(image: R.image.iconDefaultDapp()!) + + return .loaded(value: DAppView.Model( - icon: dApp.icon.map { RemoteImageViewModel(url: $0) } ?? StaticImageViewModel(image: R.image.iconDefaultDapp()!), + icon: icon, title: dApp.name, subtitle: "" - )) + ) + ) } func createLoadingDAppModel() -> [LoadableViewModelState] { diff --git a/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsWireframe.swift b/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsWireframe.swift index 96655c117c..8cd51a7a44 100644 --- a/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsWireframe.swift +++ b/novawallet/Modules/Staking/StakingMoreOptions/StakingMoreOptionsWireframe.swift @@ -10,17 +10,20 @@ final class StakingMoreOptionsWireframe: StakingMoreOptionsWireframeProtocol { view?.controller.navigationController?.pushViewController(browserView.controller, animated: true) } - func showStakingDetails( + func showStartStaking( from view: StakingMoreOptionsViewProtocol?, - option: Multistaking.ChainAssetOption + chainAsset: ChainAsset, + stakingType: StakingType? ) { - guard let detailsView = StakingMainViewFactory.createView(for: option) else { + guard let startStakingView = StartStakingInfoViewFactory.createView( + chainAsset: chainAsset, + selectedStakingType: stakingType + ) else { return } - view?.controller.navigationController?.pushViewController( - detailsView.controller, - animated: true - ) + let navigationController = ImportantFlowViewFactory.createNavigation(from: startStakingView.controller) + + view?.controller.present(navigationController, animated: true, completion: nil) } } diff --git a/novawallet/Modules/Staking/StakingMoreOptions/View/StakingMoreOptionsViewLayout.swift b/novawallet/Modules/Staking/StakingMoreOptions/View/StakingMoreOptionsViewLayout.swift index 1c8c6bd16e..e1225ab0b1 100644 --- a/novawallet/Modules/Staking/StakingMoreOptions/View/StakingMoreOptionsViewLayout.swift +++ b/novawallet/Modules/Staking/StakingMoreOptions/View/StakingMoreOptionsViewLayout.swift @@ -119,7 +119,9 @@ final class StakingMoreOptionsViewLayout: UIView { sectionHeader.pinToVisibleBounds = false section.boundarySupplementaryItems = [sectionHeader] - let decorationItem = NSCollectionLayoutDecorationItem.background(elementKind: BlurBackgroundCollectionReusableView.reuseIdentifier) + let decorationItem = NSCollectionLayoutDecorationItem.background( + elementKind: BlurBackgroundCollectionReusableView.reuseIdentifier + ) decorationItem.contentInsets = .init(top: 44, leading: 16, bottom: 13, trailing: 16) section.decorationItems = [ diff --git a/novawallet/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationViewFactory.swift b/novawallet/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationViewFactory.swift index 41a9423ca8..7ea1c4ada9 100644 --- a/novawallet/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationViewFactory.swift +++ b/novawallet/Modules/Staking/StakingPayoutConfirmation/StakingPayoutConfirmationViewFactory.swift @@ -6,7 +6,7 @@ import RobinHood final class StakingPayoutConfirmationViewFactory { static func createView( - for state: StakingSharedState, + for state: RelaychainStakingSharedStateProtocol, payouts: [PayoutInfo] ) -> StakingPayoutConfirmationViewProtocol? { let chainAsset = state.stakingOption.chainAsset @@ -61,7 +61,7 @@ final class StakingPayoutConfirmationViewFactory { } private static func createInteractor( - state: StakingSharedState, + state: RelaychainStakingSharedStateProtocol, keystore: KeystoreProtocol, payouts: [PayoutInfo] ) -> StakingPayoutConfirmationInteractor? { diff --git a/novawallet/Modules/Staking/StakingRebagConfirm/StakingRebagConfirmInteractor.swift b/novawallet/Modules/Staking/StakingRebagConfirm/StakingRebagConfirmInteractor.swift index 18ccd2e068..f3b1e7c62d 100644 --- a/novawallet/Modules/Staking/StakingRebagConfirm/StakingRebagConfirmInteractor.swift +++ b/novawallet/Modules/Staking/StakingRebagConfirm/StakingRebagConfirmInteractor.swift @@ -131,7 +131,7 @@ final class StakingRebagConfirmInteractor: AnyProviderAutoCleaning, AnyCancellab return } - stashItemProvider = subscribeStashItemProvider(for: address) + stashItemProvider = subscribeStashItemProvider(for: address, chainId: chainAsset.chain.chainId) } private func subscribeBagListNode(stashItem: StashItem?) { diff --git a/novawallet/Modules/Staking/StakingRebagConfirm/StakingRebagConfirmViewFactory.swift b/novawallet/Modules/Staking/StakingRebagConfirm/StakingRebagConfirmViewFactory.swift index 2e87f44432..697d72136e 100644 --- a/novawallet/Modules/Staking/StakingRebagConfirm/StakingRebagConfirmViewFactory.swift +++ b/novawallet/Modules/Staking/StakingRebagConfirm/StakingRebagConfirmViewFactory.swift @@ -2,19 +2,23 @@ import Foundation import SoraFoundation struct StakingRebagConfirmViewFactory { - static func createView(with state: StakingSharedState) -> StakingRebagConfirmViewProtocol? { + static func createView(with state: RelaychainStakingSharedStateProtocol) -> StakingRebagConfirmViewProtocol? { let chainAsset = state.stakingOption.chainAsset guard let selectedMetaAccount = SelectedWalletSettings.shared.value, let currencyManager = CurrencyManager.shared, - let selectedAccount = selectedMetaAccount.fetchMetaChainAccount(for: chainAsset.chain.accountRequest()), - let networkInfoFactory = try? state.createNetworkInfoOperationFactory(for: chainAsset.chain), - let eraValidatorService = state.eraValidatorService - else { + let selectedAccount = selectedMetaAccount.fetchMetaChainAccount( + for: chainAsset.chain.accountRequest() + ) else { return nil } + let eraValidatorService = state.eraValidatorService + let networkInfoFactory = state.createNetworkInfoOperationFactory( + for: OperationManagerFacade.sharedDefaultQueue + ) + let chainRegistry = ChainRegistryFacade.sharedRegistry guard @@ -40,7 +44,7 @@ struct StakingRebagConfirmViewFactory { feeProxy: ExtrinsicFeeProxy(), walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, - stakingLocalSubscriptionFactory: state.stakingLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.localSubscriptionFactory, networkInfoFactory: networkInfoFactory, eraValidatorService: eraValidatorService, runtimeService: runtimeRegistry, diff --git a/novawallet/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationInteractor.swift b/novawallet/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationInteractor.swift index 492ffe2a5f..50bc8c5094 100644 --- a/novawallet/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationInteractor.swift +++ b/novawallet/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationInteractor.swift @@ -73,7 +73,7 @@ final class StakingRebondConfirmationInteractor: RuntimeConstantFetching, Accoun extension StakingRebondConfirmationInteractor: StakingRebondConfirmationInteractorInputProtocol { func setup() { if let address = selectedAccount.toAddress() { - stashItemProvider = subscribeStashItemProvider(for: address) + stashItemProvider = subscribeStashItemProvider(for: address, chainId: chainAsset.chain.chainId) } else { presenter.didReceiveStashItem(result: .failure(ChainAccountFetchingError.accountNotExists)) } diff --git a/novawallet/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationViewFactory.swift b/novawallet/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationViewFactory.swift index e3cc0ec9e7..61420471e4 100644 --- a/novawallet/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationViewFactory.swift +++ b/novawallet/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationViewFactory.swift @@ -7,7 +7,7 @@ import SubstrateSdk struct StakingRebondConfirmationViewFactory { static func createView( for variant: SelectedRebondVariant, - state: StakingSharedState + state: RelaychainStakingSharedStateProtocol ) -> StakingRebondConfirmationViewProtocol? { let chainAsset = state.stakingOption.chainAsset @@ -72,7 +72,7 @@ struct StakingRebondConfirmationViewFactory { } private static func createInteractor( - state: StakingSharedState + state: RelaychainStakingSharedStateProtocol ) -> StakingRebondConfirmationInteractor? { let chainRegistry = ChainRegistryFacade.sharedRegistry @@ -106,7 +106,7 @@ struct StakingRebondConfirmationViewFactory { accountRepositoryFactory: accountRepositoryFactory, extrinsicServiceFactory: extrinsicServiceFactory, signingWrapperFactory: SigningWrapperFactory(), - stakingLocalSubscriptionFactory: state.stakingLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.localSubscriptionFactory, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, feeProxy: feeProxy, diff --git a/novawallet/Modules/Staking/StakingRebondSetup/StakingRebondSetupInteractor.swift b/novawallet/Modules/Staking/StakingRebondSetup/StakingRebondSetupInteractor.swift index 23ae3ffb6a..e1f10d4020 100644 --- a/novawallet/Modules/Staking/StakingRebondSetup/StakingRebondSetupInteractor.swift +++ b/novawallet/Modules/Staking/StakingRebondSetup/StakingRebondSetupInteractor.swift @@ -61,7 +61,7 @@ final class StakingRebondSetupInteractor: RuntimeConstantFetching, AccountFetchi extension StakingRebondSetupInteractor: StakingRebondSetupInteractorInputProtocol { func setup() { if let address = selectedAccount.toAddress() { - stashItemProvider = subscribeStashItemProvider(for: address) + stashItemProvider = subscribeStashItemProvider(for: address, chainId: chainAsset.chain.chainId) } else { presenter.didReceiveStashItem(result: .failure(ChainAccountFetchingError.accountNotExists)) } diff --git a/novawallet/Modules/Staking/StakingRebondSetup/StakingRebondSetupViewFactory.swift b/novawallet/Modules/Staking/StakingRebondSetup/StakingRebondSetupViewFactory.swift index 7fd7ff86f7..dc9bcc6a10 100644 --- a/novawallet/Modules/Staking/StakingRebondSetup/StakingRebondSetupViewFactory.swift +++ b/novawallet/Modules/Staking/StakingRebondSetup/StakingRebondSetupViewFactory.swift @@ -4,7 +4,7 @@ import SoraKeystore import RobinHood final class StakingRebondSetupViewFactory { - static func createView(for state: StakingSharedState) -> StakingRebondSetupViewProtocol? { + static func createView(for state: RelaychainStakingSharedStateProtocol) -> StakingRebondSetupViewProtocol? { // MARK: Interactor guard let interactor = createInteractor(state: state), @@ -53,7 +53,7 @@ final class StakingRebondSetupViewFactory { } private static func createInteractor( - state: StakingSharedState + state: RelaychainStakingSharedStateProtocol ) -> StakingRebondSetupInteractor? { let chainRegistry = ChainRegistryFacade.sharedRegistry @@ -86,7 +86,7 @@ final class StakingRebondSetupViewFactory { chainAsset: chainAsset, accountRepositoryFactory: accountRepositoryFactory, extrinsicServiceFactory: extrinsicServiceFactory, - stakingLocalSubscriptionFactory: state.stakingLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.localSubscriptionFactory, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, feeProxy: feeProxy, diff --git a/novawallet/Modules/Staking/StakingRebondSetup/StakingRebondSetupWireframe.swift b/novawallet/Modules/Staking/StakingRebondSetup/StakingRebondSetupWireframe.swift index def35f378f..d0296a10bc 100644 --- a/novawallet/Modules/Staking/StakingRebondSetup/StakingRebondSetupWireframe.swift +++ b/novawallet/Modules/Staking/StakingRebondSetup/StakingRebondSetupWireframe.swift @@ -1,9 +1,9 @@ import Foundation final class StakingRebondSetupWireframe: StakingRebondSetupWireframeProtocol { - let state: StakingSharedState + let state: RelaychainStakingSharedStateProtocol - init(state: StakingSharedState) { + init(state: RelaychainStakingSharedStateProtocol) { self.state = state } diff --git a/novawallet/Modules/Staking/StakingRedeem/StakingRedeemInteractor.swift b/novawallet/Modules/Staking/StakingRedeem/StakingRedeemInteractor.swift index 576da3050b..b6fe78e13c 100644 --- a/novawallet/Modules/Staking/StakingRedeem/StakingRedeemInteractor.swift +++ b/novawallet/Modules/Staking/StakingRedeem/StakingRedeemInteractor.swift @@ -98,7 +98,7 @@ final class StakingRedeemInteractor: RuntimeConstantFetching, AccountFetching { } let wrapper = slashesOperationFactory.createSlashingSpansOperationForStash( - stash, + { try stash.toAccountId() }, engine: connection, runtimeService: registryService ) @@ -173,7 +173,7 @@ final class StakingRedeemInteractor: RuntimeConstantFetching, AccountFetching { extension StakingRedeemInteractor: StakingRedeemInteractorInputProtocol { func setup() { if let address = selectedAccount.toAddress() { - stashItemProvider = subscribeStashItemProvider(for: address) + stashItemProvider = subscribeStashItemProvider(for: address, chainId: chainAsset.chain.chainId) } else { presenter.didReceiveStashItem(result: .failure(ChainAccountFetchingError.accountNotExists)) } @@ -207,8 +207,8 @@ extension StakingRedeemInteractor: StakingRedeemInteractorInputProtocol { fetchSlashingSpansForStash(stashAddress) { [weak self] result in switch result { case let .success(slashingSpans): - let numberOfSlashes = slashingSpans.map { $0.prior.count + 1 } ?? 0 - self?.estimateFee(with: UInt32(numberOfSlashes)) + let numberOfSlashes = slashingSpans?.numOfSlashingSpans ?? 0 + self?.estimateFee(with: numberOfSlashes) case let .failure(error): self?.presenter.didSubmitRedeeming(result: .failure(error)) } @@ -219,8 +219,8 @@ extension StakingRedeemInteractor: StakingRedeemInteractorInputProtocol { fetchSlashingSpansForStash(stashAddress) { [weak self] result in switch result { case let .success(slashingSpans): - let numberOfSlashes = slashingSpans.map { $0.prior.count + 1 } ?? 0 - self?.submit(with: UInt32(numberOfSlashes)) + let numberOfSlashes = slashingSpans?.numOfSlashingSpans ?? 0 + self?.submit(with: numberOfSlashes) case let .failure(error): self?.presenter.didSubmitRedeeming(result: .failure(error)) } diff --git a/novawallet/Modules/Staking/StakingRedeem/StakingRedeemViewFactory.swift b/novawallet/Modules/Staking/StakingRedeem/StakingRedeemViewFactory.swift index 41d2baad9c..73bc5abf75 100644 --- a/novawallet/Modules/Staking/StakingRedeem/StakingRedeemViewFactory.swift +++ b/novawallet/Modules/Staking/StakingRedeem/StakingRedeemViewFactory.swift @@ -5,7 +5,7 @@ import RobinHood import SubstrateSdk final class StakingRedeemViewFactory { - static func createView(for state: StakingSharedState) -> StakingRedeemViewProtocol? { + static func createView(for state: RelaychainStakingSharedStateProtocol) -> StakingRedeemViewProtocol? { let chainAsset = state.stakingOption.chainAsset guard @@ -66,7 +66,7 @@ final class StakingRedeemViewFactory { } private static func createInteractor( - state: StakingSharedState + state: RelaychainStakingSharedStateProtocol ) -> StakingRedeemInteractor? { let chainRegistry = ChainRegistryFacade.sharedRegistry @@ -107,7 +107,7 @@ final class StakingRedeemViewFactory { accountRepositoryFactory: accountRepositoryFactory, extrinsicServiceFactory: extrinsicServiceFactory, signingWrapperFactory: SigningWrapperFactory(), - stakingLocalSubscriptionFactory: state.stakingLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.localSubscriptionFactory, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, slashesOperationFactory: SlashesOperationFactory(storageRequestFactory: storageRequestFactory), diff --git a/novawallet/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmInteractor.swift b/novawallet/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmInteractor.swift index cd85521450..03f1ff51fa 100644 --- a/novawallet/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmInteractor.swift +++ b/novawallet/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmInteractor.swift @@ -76,7 +76,7 @@ final class StakingRewardDestConfirmInteractor: AccountFetching { extension StakingRewardDestConfirmInteractor: StakingRewardDestConfirmInteractorInputProtocol { func setup() { if let address = try? selectedAccount.accountId.toAddress(using: chainAsset.chain.chainFormat) { - stashItemProvider = subscribeStashItemProvider(for: address) + stashItemProvider = subscribeStashItemProvider(for: address, chainId: chainAsset.chain.chainId) } else { presenter.didReceiveStashItem(result: .failure(CommonError.undefined)) } diff --git a/novawallet/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmViewFactory.swift b/novawallet/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmViewFactory.swift index 9d51cad1c4..586d6d1636 100644 --- a/novawallet/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmViewFactory.swift +++ b/novawallet/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmViewFactory.swift @@ -5,7 +5,7 @@ import SoraFoundation struct StakingRewardDestConfirmViewFactory { static func createView( - for state: StakingSharedState, + for state: RelaychainStakingSharedStateProtocol, rewardDestination: RewardDestination ) -> StakingRewardDestConfirmViewProtocol? { let chainAsset = state.stakingOption.chainAsset @@ -53,18 +53,19 @@ struct StakingRewardDestConfirmViewFactory { } private static func createInteractor( - state: StakingSharedState + state: RelaychainStakingSharedStateProtocol ) -> StakingRewardDestConfirmInteractor? { let chainAsset = state.stakingOption.chainAsset guard let metaAccount = SelectedWalletSettings.shared.value, let selectedAccount = metaAccount.fetch(for: chainAsset.chain.accountRequest()), - let rewardCalculationService = state.rewardCalculationService, let currencyManager = CurrencyManager.shared else { return nil } + let rewardCalculationService = state.rewardCalculatorService + let chainRegistry = ChainRegistryFacade.sharedRegistry guard @@ -87,7 +88,7 @@ struct StakingRewardDestConfirmViewFactory { return StakingRewardDestConfirmInteractor( selectedAccount: selectedAccount, chainAsset: chainAsset, - stakingLocalSubscriptionFactory: state.stakingLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.localSubscriptionFactory, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, extrinsicServiceFactory: extrinsicServiceFactory, diff --git a/novawallet/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupInteractor.swift b/novawallet/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupInteractor.swift index 4216e97a01..e738ea3857 100644 --- a/novawallet/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupInteractor.swift +++ b/novawallet/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupInteractor.swift @@ -135,7 +135,7 @@ final class StakingRewardDestSetupInteractor: AccountFetching { extension StakingRewardDestSetupInteractor: StakingRewardDestSetupInteractorInputProtocol { func setup() { if let address = try? selectedAccount.accountId.toAddress(using: chainAsset.chain.chainFormat) { - stashItemProvider = subscribeStashItemProvider(for: address) + stashItemProvider = subscribeStashItemProvider(for: address, chainId: chainAsset.chain.chainId) } else { presenter?.didReceiveStashItem(result: .failure(CommonError.undefined)) } diff --git a/novawallet/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupViewFactory.swift b/novawallet/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupViewFactory.swift index 498fab97c7..c1b1210146 100644 --- a/novawallet/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupViewFactory.swift +++ b/novawallet/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupViewFactory.swift @@ -3,7 +3,7 @@ import SoraKeystore import RobinHood struct StakingRewardDestSetupViewFactory { - static func createView(for state: StakingSharedState) -> StakingRewardDestSetupViewProtocol? { + static func createView(for state: RelaychainStakingSharedStateProtocol) -> StakingRewardDestSetupViewProtocol? { let chainAsset = state.stakingOption.chainAsset guard @@ -56,18 +56,19 @@ struct StakingRewardDestSetupViewFactory { } private static func createInteractor( - state: StakingSharedState + state: RelaychainStakingSharedStateProtocol ) -> StakingRewardDestSetupInteractor? { let chainAsset = state.stakingOption.chainAsset guard let metaAccount = SelectedWalletSettings.shared.value, let selectedAccount = metaAccount.fetch(for: chainAsset.chain.accountRequest()), - let rewardCalculationService = state.rewardCalculationService, let currencyManager = CurrencyManager.shared else { return nil } + let rewardCalculationService = state.rewardCalculatorService + let chainRegistry = ChainRegistryFacade.sharedRegistry guard @@ -90,7 +91,7 @@ struct StakingRewardDestSetupViewFactory { return StakingRewardDestSetupInteractor( selectedAccount: selectedAccount, chainAsset: chainAsset, - stakingLocalSubscriptionFactory: state.stakingLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.localSubscriptionFactory, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, extrinsicServiceFactory: extrinsicServiceFactory, diff --git a/novawallet/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupWireframe.swift b/novawallet/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupWireframe.swift index e2510d5d7c..0f3ba4df76 100644 --- a/novawallet/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupWireframe.swift +++ b/novawallet/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestSetupWireframe.swift @@ -2,9 +2,9 @@ import Foundation import SoraFoundation final class StakingRewardDestSetupWireframe: StakingRewardDestSetupWireframeProtocol { - let state: StakingSharedState + let state: RelaychainStakingSharedStateProtocol - init(state: StakingSharedState) { + init(state: RelaychainStakingSharedStateProtocol) { self.state = state } diff --git a/novawallet/Modules/Staking/StakingRewardDetails/StakingRewardDetailsViewFactory.swift b/novawallet/Modules/Staking/StakingRewardDetails/StakingRewardDetailsViewFactory.swift index 1cfb4e2eb4..83ef018260 100644 --- a/novawallet/Modules/Staking/StakingRewardDetails/StakingRewardDetailsViewFactory.swift +++ b/novawallet/Modules/Staking/StakingRewardDetails/StakingRewardDetailsViewFactory.swift @@ -5,7 +5,7 @@ import SubstrateSdk final class StakingRewardDetailsViewFactory { static func createView( - for state: StakingSharedState, + for state: RelaychainStakingSharedStateProtocol, input: StakingRewardDetailsInput ) -> StakingRewardDetailsViewProtocol? { guard let currencyManager = CurrencyManager.shared else { diff --git a/novawallet/Modules/Staking/StakingRewardDetails/StakingRewardDetailsWireframe.swift b/novawallet/Modules/Staking/StakingRewardDetails/StakingRewardDetailsWireframe.swift index fbf9030127..281c32f0f5 100644 --- a/novawallet/Modules/Staking/StakingRewardDetails/StakingRewardDetailsWireframe.swift +++ b/novawallet/Modules/Staking/StakingRewardDetails/StakingRewardDetailsWireframe.swift @@ -1,9 +1,9 @@ import Foundation final class StakingRewardDetailsWireframe: StakingRewardDetailsWireframeProtocol { - let state: StakingSharedState + let state: RelaychainStakingSharedStateProtocol - init(state: StakingSharedState) { + init(state: RelaychainStakingSharedStateProtocol) { self.state = state } diff --git a/novawallet/Modules/Staking/StakingRewardPayouts/StakingRewardPayoutsViewFactory.swift b/novawallet/Modules/Staking/StakingRewardPayouts/StakingRewardPayoutsViewFactory.swift index 08fa1b537a..c066c90e7c 100644 --- a/novawallet/Modules/Staking/StakingRewardPayouts/StakingRewardPayoutsViewFactory.swift +++ b/novawallet/Modules/Staking/StakingRewardPayouts/StakingRewardPayoutsViewFactory.swift @@ -6,7 +6,7 @@ import IrohaCrypto final class StakingRewardPayoutsViewFactory { static func createViewForNominator( - for state: StakingSharedState, + for state: RelaychainStakingSharedStateProtocol, stashAddress: AccountAddress ) -> StakingRewardPayoutsViewProtocol? { let chainAsset = state.stakingOption.chainAsset @@ -30,7 +30,7 @@ final class StakingRewardPayoutsViewFactory { } static func createViewForValidator( - for state: StakingSharedState, + for state: RelaychainStakingSharedStateProtocol, stashAddress: AccountAddress ) -> StakingRewardPayoutsViewProtocol? { let chainAsset = state.stakingOption.chainAsset @@ -48,7 +48,7 @@ final class StakingRewardPayoutsViewFactory { } private static func createView( - for state: StakingSharedState, + for state: RelaychainStakingSharedStateProtocol, stashAddress: AccountAddress, validatorsResolutionFactory: PayoutValidatorsFactoryProtocol, payoutInfoFactory: PayoutInfoFactoryProtocol @@ -90,7 +90,7 @@ final class StakingRewardPayoutsViewFactory { private static func createView( for payoutService: PayoutRewardsServiceProtocol, - state: StakingSharedState + state: RelaychainStakingSharedStateProtocol ) -> StakingRewardPayoutsViewProtocol? { let chainAsset = state.stakingOption.chainAsset @@ -100,18 +100,10 @@ final class StakingRewardPayoutsViewFactory { let operationManager = OperationManagerFacade.sharedManager - let storageRequestFactory = StorageRequestFactory( - remoteFactory: StorageKeyFactory(), - operationManager: operationManager + let eraCountdownOperationFactory = state.createEraCountdownOperationFactory( + for: OperationManagerFacade.sharedDefaultQueue ) - guard let eraCountdownOperationFactory = try? state.createEraCountdownOperationFactory( - for: chainAsset.chain, - storageRequestFactory: storageRequestFactory - ) else { - return nil - } - let assetInfo = chainAsset.assetDisplayInfo let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) let balanceViewModelFactory = BalanceViewModelFactory( @@ -136,7 +128,7 @@ final class StakingRewardPayoutsViewFactory { let interactor = StakingRewardPayoutsInteractor( chainAsset: chainAsset, chainRegistry: ChainRegistryFacade.sharedRegistry, - stakingLocalSubscriptionFactory: state.stakingLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.localSubscriptionFactory, priceLocalSubscriptionFactory: PriceProviderFactory.shared, payoutService: payoutService, eraCountdownOperationFactory: eraCountdownOperationFactory, diff --git a/novawallet/Modules/Staking/StakingRewardPayouts/StakingRewardPayoutsWireframe.swift b/novawallet/Modules/Staking/StakingRewardPayouts/StakingRewardPayoutsWireframe.swift index bf409341b5..00fed9aaf6 100644 --- a/novawallet/Modules/Staking/StakingRewardPayouts/StakingRewardPayoutsWireframe.swift +++ b/novawallet/Modules/Staking/StakingRewardPayouts/StakingRewardPayoutsWireframe.swift @@ -1,9 +1,9 @@ import Foundation final class StakingRewardPayoutsWireframe: StakingRewardPayoutsWireframeProtocol { - let state: StakingSharedState + let state: RelaychainStakingSharedStateProtocol - init(state: StakingSharedState) { + init(state: RelaychainStakingSharedStateProtocol) { self.state = state } diff --git a/novawallet/Modules/Staking/StakingSelectPool/Model/ButtonViewModel.swift b/novawallet/Modules/Staking/StakingSelectPool/Model/ButtonViewModel.swift new file mode 100644 index 0000000000..99bdfee25c --- /dev/null +++ b/novawallet/Modules/Staking/StakingSelectPool/Model/ButtonViewModel.swift @@ -0,0 +1,5 @@ +enum ButtonViewModel { + case hidden + case active + case inactive +} diff --git a/novawallet/Modules/Staking/StakingSelectPool/Model/StakingSelectPoolViewModelFactory.swift b/novawallet/Modules/Staking/StakingSelectPool/Model/StakingSelectPoolViewModelFactory.swift new file mode 100644 index 0000000000..facc93c50f --- /dev/null +++ b/novawallet/Modules/Staking/StakingSelectPool/Model/StakingSelectPoolViewModelFactory.swift @@ -0,0 +1,72 @@ +import SoraFoundation + +protocol StakingSelectPoolViewModelFactoryProtocol { + func createStakingSelectPoolViewModel( + from poolStats: NominationPools.PoolStats, + selectedPoolId: NominationPools.PoolId?, + chainAsset: ChainAsset, + locale: Locale + ) -> StakingSelectPoolViewModel +} + +final class StakingSelectPoolViewModelFactory: StakingSelectPoolViewModelFactoryProtocol { + let apyFormatter: LocalizableResource + let membersFormatter: LocalizableResource + let poolIconFactory: NominationPoolsIconFactoryProtocol + + init( + apyFormatter: LocalizableResource, + membersFormatter: LocalizableResource, + poolIconFactory: NominationPoolsIconFactoryProtocol + ) { + self.apyFormatter = apyFormatter + self.membersFormatter = membersFormatter + self.poolIconFactory = poolIconFactory + } + + func createStakingSelectPoolViewModel( + from poolStats: NominationPools.PoolStats, + selectedPoolId: NominationPools.PoolId?, + chainAsset: ChainAsset, + locale: Locale + ) -> StakingSelectPoolViewModel { + let selectedPool = NominationPools.SelectedPool(poolStats: poolStats) + let title = selectedPool.title(for: chainAsset.chain.chainFormat) ?? "" + let apy = selectedPool.maxApy.map { + apyFormatter.value(for: locale).stringFromDecimal($0) + } ?? nil + let period = R.string.localizable.commonPerYear(preferredLanguages: locale.rLanguages) + let members = membersFormatter.value(for: locale).string(from: .init(value: poolStats.membersCount)) ?? "" + let imageViewModel = selectedPoolId != poolStats.poolId ? poolIconFactory.createIconViewModel( + for: chainAsset, + poolId: poolStats.poolId, + bondedAccountId: poolStats.bondedAccountId + ) : StaticImageViewModel(image: R.image.iconCheckbox()!) + + return StakingSelectPoolViewModel( + imageViewModel: imageViewModel, + name: title, + apy: apy.map { .init(value: $0, period: period) }, + members: members, + id: poolStats.poolId + ) + } +} + +extension StakingSelectPoolViewModelFactoryProtocol { + func createStakingSelectPoolViewModels( + from poolStats: [NominationPools.PoolStats], + selectedPoolId: NominationPools.PoolId?, + chainAsset: ChainAsset, + locale: Locale + ) -> [StakingSelectPoolViewModel] { + poolStats.map { + createStakingSelectPoolViewModel( + from: $0, + selectedPoolId: selectedPoolId, + chainAsset: chainAsset, + locale: locale + ) + } + } +} diff --git a/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolInteractor.swift b/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolInteractor.swift new file mode 100644 index 0000000000..118f70d461 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolInteractor.swift @@ -0,0 +1,159 @@ +import UIKit +import SubstrateSdk +import RobinHood +import BigInt + +final class StakingSelectPoolInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning { + weak var presenter: StakingSelectPoolInteractorOutputProtocol? + let poolsOperationFactory: NominationPoolsOperationFactoryProtocol + let eraNominationPoolsService: EraNominationPoolsServiceProtocol + let validatorRewardService: RewardCalculatorServiceProtocol + let connection: JSONRPCEngine + let runtimeService: RuntimeCodingServiceProtocol + let npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol + let chainAsset: ChainAsset + let rewardEngineOperationFactory: NPoolsRewardEngineFactoryProtocol + let recommendationMediator: RelaychainStakingRecommendationMediating + let amount: BigUInt + + private var maxMembersPerPoolProvider: AnyDataProvider? + private var maxPoolMembersPerPool: UncertainStorage = .undefined + private var poolsCancellable: CancellableCall? + + private let operationQueue: OperationQueue + + init( + poolsOperationFactory: NominationPoolsOperationFactoryProtocol, + npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, + rewardEngineOperationFactory: NPoolsRewardEngineFactoryProtocol, + recommendationMediator: RelaychainStakingRecommendationMediating, + eraNominationPoolsService: EraNominationPoolsServiceProtocol, + validatorRewardService: RewardCalculatorServiceProtocol, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol, + chainAsset: ChainAsset, + amount: BigUInt, + operationQueue: OperationQueue + ) { + self.poolsOperationFactory = poolsOperationFactory + self.npoolsLocalSubscriptionFactory = npoolsLocalSubscriptionFactory + self.rewardEngineOperationFactory = rewardEngineOperationFactory + self.recommendationMediator = recommendationMediator + self.eraNominationPoolsService = eraNominationPoolsService + self.validatorRewardService = validatorRewardService + self.connection = connection + self.runtimeService = runtimeService + self.chainAsset = chainAsset + self.amount = amount + self.operationQueue = operationQueue + } + + private func performMaxMembersPerPoolSubscription() { + clear(dataProvider: &maxMembersPerPoolProvider) + maxMembersPerPoolProvider = subscribeMaxPoolMembersPerPool(for: chainAsset.chain.chainId) + } + + private func fetchSparePoolsInfo() { + clear(cancellable: &poolsCancellable) + + let maxApyWrapper = rewardEngineOperationFactory.createEngineWrapper( + for: eraNominationPoolsService, + validatorRewardService: validatorRewardService, + connection: connection, + runtimeService: runtimeService + ) + + let maxPoolMembers = maxPoolMembersPerPool.value ?? nil + let preferrablePool = StakingConstants.recommendedPoolIds[chainAsset.chain.chainId] + let params = RecommendedNominationPoolsParams( + maxMembersPerPool: { maxPoolMembers }, + preferrablePool: { preferrablePool } + ) + + let poolStatsWrapper = poolsOperationFactory.createPoolRecommendationsInfoWrapper( + for: eraNominationPoolsService, + rewardEngine: { + try maxApyWrapper.targetOperation.extractNoCancellableResultData() + }, + params: params, + connection: connection, + runtimeService: runtimeService + ) + poolStatsWrapper.addDependency(wrapper: maxApyWrapper) + + poolStatsWrapper.targetOperation.completionBlock = { [weak self] in + guard poolStatsWrapper === self?.poolsCancellable else { + return + } + self?.poolsCancellable = nil + + DispatchQueue.main.async { + do { + let stats = try poolStatsWrapper.targetOperation.extractNoCancellableResultData() + self?.presenter?.didReceive(poolStats: stats) + } catch { + self?.presenter?.didReceive(error: .poolStats(error)) + } + } + } + + poolsCancellable = poolStatsWrapper + operationQueue.addOperations( + maxApyWrapper.allOperations + poolStatsWrapper.allOperations, + waitUntilFinished: false + ) + } + + private func provideRecommendation() { + recommendationMediator.delegate = self + recommendationMediator.startRecommending() + recommendationMediator.update(amount: amount) + } +} + +extension StakingSelectPoolInteractor: StakingSelectPoolInteractorInputProtocol { + func setup() { + performMaxMembersPerPoolSubscription() + provideRecommendation() + } + + func refreshPools() { + guard maxPoolMembersPerPool.isDefined else { + performMaxMembersPerPoolSubscription() + return + } + fetchSparePoolsInfo() + } + + func refreshRecommendation() { + provideRecommendation() + } +} + +extension StakingSelectPoolInteractor: NPoolsLocalStorageSubscriber, NPoolsLocalSubscriptionHandler { + func handleMaxPoolMembersPerPool(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(value): + maxPoolMembersPerPool = .defined(value) + fetchSparePoolsInfo() + case let .failure(error): + presenter?.didReceive(error: .poolStats(error)) + } + } +} + +extension StakingSelectPoolInteractor: RelaychainStakingRecommendationDelegate { + func didReceive( + recommendation: RelaychainStakingRecommendation, + amount _: BigUInt + ) { + guard case let .pool(recommendedPool) = recommendation.staking else { + return + } + presenter?.didReceive(recommendedPool: recommendedPool) + } + + func didReceiveRecommendation(error: Error) { + presenter?.didReceive(error: .recommendation(error)) + } +} diff --git a/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolPresenter.swift b/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolPresenter.swift new file mode 100644 index 0000000000..f5ddb2c53c --- /dev/null +++ b/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolPresenter.swift @@ -0,0 +1,201 @@ +import Foundation +import SoraFoundation + +final class StakingSelectPoolPresenter { + weak var view: StakingSelectPoolViewProtocol? + weak var delegate: StakingSelectPoolDelegate? + + let wireframe: StakingSelectPoolWireframeProtocol + let interactor: StakingSelectPoolInteractorInputProtocol + let viewModelFactory: StakingSelectPoolViewModelFactoryProtocol + let chainAsset: ChainAsset + + private var poolStats: [NominationPools.PoolStats]? + private var poolStatsMap: [NominationPools.PoolId: NominationPools.PoolStats] = [:] + private var selectedPoolId: NominationPools.PoolId? + private var recommendedPoolId: NominationPools.PoolId? + + init( + interactor: StakingSelectPoolInteractorInputProtocol, + wireframe: StakingSelectPoolWireframeProtocol, + viewModelFactory: StakingSelectPoolViewModelFactoryProtocol, + chainAsset: ChainAsset, + delegate: StakingSelectPoolDelegate?, + selectedPool: NominationPools.SelectedPool?, + localizationManager: LocalizationManagerProtocol + ) { + self.interactor = interactor + self.wireframe = wireframe + self.viewModelFactory = viewModelFactory + self.chainAsset = chainAsset + self.delegate = delegate + selectedPoolId = selectedPool?.poolId + self.localizationManager = localizationManager + } + + private func provideViewModel() { + guard let stats = poolStats else { + return + } + let viewModels = viewModelFactory.createStakingSelectPoolViewModels( + from: stats, + selectedPoolId: selectedPoolId, + chainAsset: chainAsset, + locale: selectedLocale + ) + + view?.didReceivePools(state: .loaded(value: viewModels)) + } + + private func provideSingleViewModel(poolId: NominationPools.PoolId?) { + guard let poolId = poolId, let poolStats = poolStatsMap[poolId] else { + return + } + let viewModel = viewModelFactory.createStakingSelectPoolViewModel( + from: poolStats, + selectedPoolId: selectedPoolId, + chainAsset: chainAsset, + locale: selectedLocale + ) + + view?.didReceivePoolUpdate(viewModel: viewModel) + } + + private func sortPools() { + guard let recommendedPoolId = recommendedPoolId, + let poolStats = poolStats, + !poolStats.isEmpty else { + return + } + + if poolStats[0].poolId != recommendedPoolId { + if let currentPosition = poolStats.firstIndex(where: { $0.poolId == recommendedPoolId }) { + self.poolStats?.remove(at: currentPosition) + self.poolStats?.insert(poolStats[currentPosition], at: 0) + } + } + } + + private func notifySelectedPoolChangedIfNeeded(oldSelection: NominationPools.PoolId?) { + guard let selectedPoolId = selectedPoolId, oldSelection != selectedPoolId else { + return + } + guard let poolStats = poolStatsMap[selectedPoolId] else { + return + } + let isRecommended = recommendedPoolId == selectedPoolId + + delegate?.changePoolSelection(selectedPool: .init(poolStats: poolStats), isRecommended: isRecommended) + } + + private func provideRecommendedButtonViewModel() { + guard let recommendedPoolId = recommendedPoolId, !poolStatsMap.isEmpty else { + view?.didReceiveRecommendedButton(viewModel: .hidden) + return + } + + if recommendedPoolId == selectedPoolId { + view?.didReceiveRecommendedButton(viewModel: .inactive) + } else { + view?.didReceiveRecommendedButton(viewModel: .active) + } + } +} + +extension StakingSelectPoolPresenter: StakingSelectPoolPresenterProtocol { + func setup() { + interactor.setup() + provideRecommendedButtonViewModel() + view?.didReceivePools(state: .loading) + } + + func selectPool(poolId: NominationPools.PoolId) { + guard let poolStats = poolStatsMap[poolId] else { + return + } + let previousSelectedPoolId = selectedPoolId + selectedPoolId = poolId + provideSingleViewModel(poolId: previousSelectedPoolId) + provideSingleViewModel(poolId: poolId) + provideRecommendedButtonViewModel() + delegate?.changePoolSelection( + selectedPool: .init(poolStats: poolStats), + isRecommended: poolId == recommendedPoolId + ) + wireframe.complete(from: view) + } + + func showPoolInfo(poolId: NominationPools.PoolId) { + guard let view = view, + let pool = poolStatsMap[poolId], + let address = try? pool.bondedAccountId.toAddress(using: chainAsset.chain.chainFormat) else { + return + } + wireframe.presentAccountOptions( + from: view, + address: address, + chain: chainAsset.chain, + locale: selectedLocale + ) + } + + func selectRecommended() { + guard let recommendedPoolId = recommendedPoolId else { + return + } + selectPool(poolId: recommendedPoolId) + } + + func search() { + guard let delegate = delegate else { + return + } + + wireframe.showSearch(from: view, delegate: delegate, selectedPoolId: selectedPoolId) + } +} + +extension StakingSelectPoolPresenter: StakingSelectPoolInteractorOutputProtocol { + func didReceive(poolStats: [NominationPools.PoolStats]) { + self.poolStats = poolStats + poolStatsMap = poolStats.reduce(into: [NominationPools.PoolId: NominationPools.PoolStats]()) { result, pool in + result[pool.poolId] = pool + } + notifySelectedPoolChangedIfNeeded(oldSelection: selectedPoolId) + sortPools() + provideRecommendedButtonViewModel() + provideViewModel() + } + + func didReceive(recommendedPool: NominationPools.SelectedPool) { + recommendedPoolId = recommendedPool.poolId + if selectedPoolId == nil { + selectedPoolId = recommendedPoolId + notifySelectedPoolChangedIfNeeded(oldSelection: nil) + } + sortPools() + provideRecommendedButtonViewModel() + provideViewModel() + } + + func didReceive(error: StakingSelectPoolError) { + switch error { + case .poolStats: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.refreshPools() + } + case .recommendation: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.refreshRecommendation() + } + } + } +} + +extension StakingSelectPoolPresenter: Localizable { + func applyLocalization() { + if view?.isSetup == true { + provideViewModel() + } + } +} diff --git a/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolProtocols.swift b/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolProtocols.swift new file mode 100644 index 0000000000..2481a2175b --- /dev/null +++ b/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolProtocols.swift @@ -0,0 +1,40 @@ +protocol StakingSelectPoolViewProtocol: ControllerBackedProtocol { + func didReceivePools(state: LoadableViewModelState<[StakingSelectPoolViewModel]>) + func didReceivePoolUpdate(viewModel: StakingSelectPoolViewModel) + func didReceiveRecommendedButton(viewModel: ButtonViewModel) +} + +protocol StakingSelectPoolPresenterProtocol: AnyObject { + func setup() + func selectPool(poolId: NominationPools.PoolId) + func showPoolInfo(poolId: NominationPools.PoolId) + func selectRecommended() + func search() +} + +protocol StakingSelectPoolInteractorInputProtocol: AnyObject { + func setup() + func refreshPools() + func refreshRecommendation() +} + +protocol StakingSelectPoolInteractorOutputProtocol: AnyObject { + func didReceive(poolStats: [NominationPools.PoolStats]) + func didReceive(recommendedPool: NominationPools.SelectedPool) + func didReceive(error: StakingSelectPoolError) +} + +protocol StakingSelectPoolWireframeProtocol: AnyObject, AddressOptionsPresentable, CommonRetryable, AlertPresentable { + func complete(from view: ControllerBackedProtocol?) + + func showSearch( + from view: ControllerBackedProtocol?, + delegate: StakingSelectPoolDelegate, + selectedPoolId: NominationPools.PoolId? + ) +} + +enum StakingSelectPoolError: Error { + case poolStats(Error) + case recommendation(Error) +} diff --git a/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolViewController.swift b/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolViewController.swift new file mode 100644 index 0000000000..581d09e9ef --- /dev/null +++ b/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolViewController.swift @@ -0,0 +1,176 @@ +import UIKit +import SoraFoundation + +typealias StakingSelectPoolViewModel = StakingPoolTableViewCell.Model + +final class StakingSelectPoolViewController: UIViewController, ViewHolder { + typealias RootViewType = StakingSelectPoolViewLayout + + let presenter: StakingSelectPoolPresenterProtocol + let numberFormatter: LocalizableResource + + private var state: LoadableViewModelState<[StakingSelectPoolViewModel]> = .loading + + init( + presenter: StakingSelectPoolPresenterProtocol, + numberFormatter: LocalizableResource, + localizationManager: LocalizationManagerProtocol + ) { + self.presenter = presenter + self.numberFormatter = numberFormatter + super.init(nibName: nil, bundle: nil) + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = StakingSelectPoolViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + rootView.tableView.dataSource = self + rootView.tableView.delegate = self + setupLocalization() + setupHandlers() + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: rootView.searchButton) + + presenter.setup() + } + + private func setupLocalization() { + title = R.string.localizable.stakingSelectPoolTitle(preferredLanguages: selectedLocale.rLanguages) + + let buttonTitle = R.string.localizable.stakingSelectValidatorsRecommendedButtonTitle( + preferredLanguages: selectedLocale.rLanguages + ) + + rootView.recommendedButton.imageWithTitleView?.title = buttonTitle + } + + private func setupHandlers() { + rootView.recommendedButton.addTarget(self, action: #selector(selectRecommendedAction), for: .touchUpInside) + rootView.searchButton.addTarget(self, action: #selector(searchAction), for: .touchUpInside) + } + + @objc private func selectRecommendedAction() { + presenter.selectRecommended() + } + + @objc private func searchAction() { + presenter.search() + } +} + +extension StakingSelectPoolViewController: StakingSelectPoolViewProtocol { + func didReceivePools(state: LoadableViewModelState<[StakingSelectPoolViewModel]>) { + self.state = state + switch state { + case .loading: + rootView.loadingView.isHidden = false + rootView.loadingView.start() + case .cached, .loaded: + rootView.loadingView.stop() + rootView.loadingView.isHidden = true + rootView.tableView.reloadData() + } + } + + func didReceivePoolUpdate(viewModel: StakingSelectPoolViewModel) { + guard let viewModels = state.value, + let index = viewModels.firstIndex(where: { $0.id == viewModel.id }) else { + return + } + + state.insert(newElement: viewModel, at: index) + + if let cell = rootView.tableView.visibleCells[safe: index] as? StakingPoolTableViewCell { + cell.bind(viewModel: viewModel) + } + } + + func didReceiveRecommendedButton(viewModel: ButtonViewModel) { + switch viewModel { + case .hidden: + rootView.recommendedButton.isHidden = true + case .active: + rootView.recommendedButton.isHidden = false + rootView.recommendedButton.isUserInteractionEnabled = true + rootView.recommendedButton.apply(style: .accentButton) + case .inactive: + rootView.recommendedButton.isHidden = false + rootView.recommendedButton.isUserInteractionEnabled = false + rootView.recommendedButton.apply(style: .inactiveButton) + } + } +} + +extension StakingSelectPoolViewController: UITableViewDataSource { + func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { + state.value?.count ?? 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: StakingPoolTableViewCell = tableView.dequeueReusableCell(for: indexPath) + if let model = state.value?[safe: indexPath.row] { + cell.bind(viewModel: model) + cell.infoAction = { [weak self] viewModel in + self?.presenter.showPoolInfo(poolId: viewModel.id) + } + } + return cell + } +} + +extension StakingSelectPoolViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + guard let model = state.value?[safe: indexPath.row] else { + return + } + presenter.selectPool(poolId: model.id) + } + + func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat { + 44 + } + + func tableView(_: UITableView, heightForHeaderInSection _: Int) -> CGFloat { + guard let viewModels = state.value, !viewModels.isEmpty else { + return 0 + } + return 26 + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection _: Int) -> UIView? { + guard let viewModels = state.value, !viewModels.isEmpty else { + return nil + } + let header: StakingSelectPoolListHeaderView = tableView.dequeueReusableHeaderFooterView() + let count = numberFormatter.value(for: selectedLocale).string(from: NSNumber(value: viewModels.count)) + let title = R.string.localizable.stakingSelectPoolCount( + count ?? "", + preferredLanguages: selectedLocale.rLanguages + ) + let details = R.string.localizable.stakingSelectPoolMembers(preferredLanguages: selectedLocale.rLanguages) + header.bind( + title: title, + details: details + ) + return header + } +} + +extension StakingSelectPoolViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + rootView.tableView.reloadData() + setupLocalization() + } + } +} diff --git a/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolViewFactory.swift b/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolViewFactory.swift new file mode 100644 index 0000000000..6b03231b52 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolViewFactory.swift @@ -0,0 +1,80 @@ +import Foundation +import SoraFoundation +import BigInt + +protocol StakingSelectPoolDelegate: AnyObject { + func changePoolSelection(selectedPool: NominationPools.SelectedPool, isRecommended: Bool) +} + +enum StakingSelectPoolViewFactory { + static func createView( + state: RelaychainStartStakingStateProtocol, + amount: BigUInt, + selectedPool: NominationPools.SelectedPool?, + delegate: StakingSelectPoolDelegate? + ) -> StakingSelectPoolViewProtocol? { + let chainId = state.chainAsset.chain.chainId + let chainRegistry = ChainRegistryFacade.sharedRegistry + + guard + let runtimeService = chainRegistry.getRuntimeProvider(for: chainId), + let connection = chainRegistry.getConnection(for: chainId), + let activePoolService = state.activePoolsService else { + return nil + } + let queue = OperationQueue() + + let recommendationFactory = StakingRecommendationMediatorFactory( + chainRegistry: ChainRegistryFacade.sharedRegistry, + operationQueue: queue + ) + guard let recommendationMediator = recommendationFactory.createPoolStakingMediator(for: state) else { + return nil + } + + let poolsOperationFactory = NominationPoolsOperationFactory(operationQueue: queue) + let rewardCalculationFactory = NPoolsRewardEngineFactory(operationFactory: poolsOperationFactory) + + let interactor = StakingSelectPoolInteractor( + poolsOperationFactory: poolsOperationFactory, + npoolsLocalSubscriptionFactory: state.npLocalSubscriptionFactory, + rewardEngineOperationFactory: rewardCalculationFactory, + recommendationMediator: recommendationMediator, + eraNominationPoolsService: activePoolService, + validatorRewardService: state.relaychainRewardCalculatorService, + connection: connection, + runtimeService: runtimeService, + chainAsset: state.chainAsset, + amount: amount, + operationQueue: queue + ) + + let wireframe = StakingSelectPoolWireframe(state: state) + let viewModelFactory = StakingSelectPoolViewModelFactory( + apyFormatter: NumberFormatter.percentAPY.localizableResource(), + membersFormatter: NumberFormatter.quantity.localizableResource(), + poolIconFactory: NominationPoolsIconFactory() + ) + + let presenter = StakingSelectPoolPresenter( + interactor: interactor, + wireframe: wireframe, + viewModelFactory: viewModelFactory, + chainAsset: state.chainAsset, + delegate: delegate, + selectedPool: selectedPool, + localizationManager: LocalizationManager.shared + ) + + let view = StakingSelectPoolViewController( + presenter: presenter, + numberFormatter: NumberFormatter.quantity.localizableResource(), + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + interactor.presenter = presenter + + return view + } +} diff --git a/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolWireframe.swift b/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolWireframe.swift new file mode 100644 index 0000000000..bab1faaf08 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSelectPool/StakingSelectPoolWireframe.swift @@ -0,0 +1,35 @@ +import Foundation + +final class StakingSelectPoolWireframe: StakingSelectPoolWireframeProtocol { + let state: RelaychainStartStakingStateProtocol + + init(state: RelaychainStartStakingStateProtocol) { + self.state = state + } + + func complete(from view: ControllerBackedProtocol?) { + if let amountView: StakingSetupAmountViewProtocol = view?.controller.navigationController?.findTopView() { + view?.controller.navigationController?.popToViewController( + amountView.controller, + animated: true + ) + } + } + + func showSearch( + from view: ControllerBackedProtocol?, + delegate: StakingSelectPoolDelegate, + selectedPoolId: NominationPools.PoolId? + ) { + guard let view = view, + let searchView = NominationPoolSearchViewFactory.createView( + state: state, + delegate: delegate, + selectedPoolId: selectedPoolId + ) else { + return + } + + view.controller.navigationController?.pushViewController(searchView.controller, animated: true) + } +} diff --git a/novawallet/Modules/Staking/StakingSelectPool/View/StakingPoolTableViewCell.swift b/novawallet/Modules/Staking/StakingSelectPool/View/StakingPoolTableViewCell.swift new file mode 100644 index 0000000000..f8f2b3f843 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSelectPool/View/StakingPoolTableViewCell.swift @@ -0,0 +1,94 @@ +import UIKit +import SnapKit + +final class StakingPoolTableViewCell: UITableViewCell { + let view = StakingPoolView(frame: .zero) + + var infoAction: ((StakingPoolTableViewCell.Model) -> Void)? + private var currentModel: StakingPoolTableViewCell.Model? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + setupLayout() + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var contentInsets = UIEdgeInsets(top: 5, left: 16, bottom: 5, right: 16) { + didSet { + updateLayout() + } + } + + override func prepareForReuse() { + super.prepareForReuse() + infoAction = nil + } + + private func configure() { + backgroundColor = .clear + selectedBackgroundView = UIView() + selectedBackgroundView?.backgroundColor = .clear + + view.infoButton.addTarget(self, action: #selector(tapInfoButton), for: .touchUpInside) + } + + @objc func tapInfoButton() { + guard let viewModel = currentModel else { + return + } + infoAction?(viewModel) + } + + private func setupLayout() { + contentView.addSubview(view) + view.snp.makeConstraints { + $0.edges.equalToSuperview().inset(contentInsets) + } + } + + private func updateLayout() { + view.snp.updateConstraints { + $0.edges.equalToSuperview().inset(contentInsets) + } + } +} + +extension StakingPoolTableViewCell { + struct Model { + let imageViewModel: ImageViewModelProtocol? + let name: String + let apy: RewardApyModel? + let members: String + let id: NominationPools.PoolId + } + + struct RewardApyModel { + let value: String + let period: String + } + + func bind(viewModel: Model) { + currentModel?.imageViewModel?.cancel(on: view.iconView) + currentModel = viewModel + + view.iconView.image = nil + + let imageSize = StakingPoolView.Constants.iconSize + viewModel.imageViewModel?.loadImage( + on: view.iconView, + targetSize: imageSize, + animated: true + ) + + view.poolName.text = viewModel.name + view.rewardView.fView.text = viewModel.apy?.value + view.rewardView.sView.text = viewModel.apy?.period + view.membersCountLabel.text = viewModel.members + } +} diff --git a/novawallet/Modules/Staking/StakingSelectPool/View/StakingPoolView.swift b/novawallet/Modules/Staking/StakingSelectPool/View/StakingPoolView.swift new file mode 100644 index 0000000000..2f3c68fd76 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSelectPool/View/StakingPoolView.swift @@ -0,0 +1,58 @@ +import UIKit +import SnapKit + +typealias SelectPoolRewardView = GenericPairValueView +typealias SelectPoolAccountView = GenericPairValueView> +typealias SelectPoolMembersView = GenericPairValueView + +final class StakingPoolView: GenericTitleValueView { + var iconView: UIImageView { titleView.fView } + var poolView: GenericMultiValueView { titleView.sView } + var poolName: UILabel { poolView.valueTop } + var rewardView: SelectPoolRewardView { poolView.valueBottom } + var membersCountLabel: UILabel { valueView.fView } + var infoButton: UIButton { valueView.sView } + + override init(frame: CGRect) { + super.init(frame: frame) + + configure() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + titleView.spacing = 12 + titleView.makeHorizontal() + titleView.stackView.alignment = .center + rewardView.spacing = 2 + rewardView.makeHorizontal() + rewardView.fView.apply(style: .caption1Positive) + rewardView.sView.apply(style: .caption1Tertiary) + rewardView.fView.setContentHuggingPriority(.high, for: .horizontal) + rewardView.sView.setContentHuggingPriority(.defaultLow, for: .horizontal) + valueView.spacing = 8 + valueView.makeHorizontal() + poolView.spacing = 2 + poolView.valueTop.apply(style: .footnotePrimary) + poolName.textAlignment = .left + poolName.setContentHuggingPriority(.defaultLow, for: .horizontal) + iconView.setContentHuggingPriority(.defaultLow, for: .vertical) + + let icon = R.image.iconInfoFilled()?.tinted(with: R.color.colorIconSecondary()!) + infoButton.setImage(icon, for: .normal) + iconView.snp.makeConstraints { + $0.size.equalTo(Constants.iconSize) + } + membersCountLabel.apply(style: .footnotePrimary) + } +} + +extension StakingPoolView { + enum Constants { + static let iconSize = CGSize(width: 24, height: 24) + } +} diff --git a/novawallet/Modules/Staking/StakingSelectPool/View/StakingSelectPoolListHeaderView.swift b/novawallet/Modules/Staking/StakingSelectPool/View/StakingSelectPoolListHeaderView.swift new file mode 100644 index 0000000000..2caf79a661 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSelectPool/View/StakingSelectPoolListHeaderView.swift @@ -0,0 +1,3 @@ +import UIKit + +typealias StakingSelectPoolListHeaderView = CustomValidatorListHeaderView diff --git a/novawallet/Modules/Staking/StakingSelectPool/View/StakingSelectPoolViewLayout.swift b/novawallet/Modules/Staking/StakingSelectPool/View/StakingSelectPoolViewLayout.swift new file mode 100644 index 0000000000..632ded994d --- /dev/null +++ b/novawallet/Modules/Staking/StakingSelectPool/View/StakingSelectPoolViewLayout.swift @@ -0,0 +1,59 @@ +import UIKit +import SoraUI + +final class StakingSelectPoolViewLayout: UIView { + let recommendedButton: RoundedButton = .create { + $0.apply(style: .inactiveButton) + $0.contentInsets = .init(top: 7, left: 12, bottom: 8, right: 12) + } + + let tableView: UITableView = .create { + $0.tableFooterView = UIView() + $0.backgroundColor = .clear + $0.separatorStyle = .none + $0.registerClassForCell(StakingPoolTableViewCell.self) + $0.registerHeaderFooterView(withClass: StakingSelectPoolListHeaderView.self) + } + + let loadingView: ListLoadingView = .create { + $0.isHidden = true + } + + let searchButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage(R.image.iconSearchWhite(), for: .normal) + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = R.color.colorSecondaryScreenBackground() + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + addSubview(recommendedButton) + recommendedButton.snp.makeConstraints { + $0.leading.equalToSuperview().offset(16) + $0.top.equalTo(safeAreaLayoutGuide.snp.top).offset(12) + $0.trailing.lessThanOrEqualToSuperview() + $0.height.equalTo(32) + } + + addSubview(tableView) + tableView.snp.makeConstraints { + $0.top.equalTo(recommendedButton.snp.bottom).offset(16) + $0.leading.bottom.trailing.equalToSuperview() + } + + addSubview(loadingView) + loadingView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } +} diff --git a/novawallet/Modules/Staking/StakingSelectPool/View/StakingSelectPoolViewStyles.swift b/novawallet/Modules/Staking/StakingSelectPool/View/StakingSelectPoolViewStyles.swift new file mode 100644 index 0000000000..c1848272d9 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSelectPool/View/StakingSelectPoolViewStyles.swift @@ -0,0 +1,18 @@ +import SoraUI + +extension RoundedButton.Style { + static let accentButton = RoundedButton.Style( + background: .init( + fillColor: R.color.colorButtonBackgroundPrimary()!, + highlightedFillColor: R.color.colorButtonBackgroundPrimary()! + ), + title: .semiboldFootnoteButtonText + ) + static let inactiveButton = RoundedButton.Style( + background: .init( + fillColor: R.color.colorButtonBackgroundInactive()!, + highlightedFillColor: R.color.colorButtonBackgroundInactive()! + ), + title: .semiboldFootnoteButtonInactive + ) +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Model/ButtonState.swift b/novawallet/Modules/Staking/StakingSetupAmount/Model/ButtonState.swift new file mode 100644 index 0000000000..388e1716d9 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Model/ButtonState.swift @@ -0,0 +1,23 @@ +import Foundation +import SoraFoundation + +struct ButtonState { + let title: LocalizableResource + let enabled: Bool + + static let startState = ButtonState( + title: LocalizableResource { + R.string.localizable.transferSetupEnterAmount(preferredLanguages: $0.rLanguages) + }, + enabled: false + ) + + static func continueState(enabled: Bool) -> ButtonState { + .init( + title: LocalizableResource { + R.string.localizable.commonContinue(preferredLanguages: $0.rLanguages) + }, + enabled: enabled + ) + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Model/DirectStakingTypeAccountViewModel.swift b/novawallet/Modules/Staking/StakingSetupAmount/Model/DirectStakingTypeAccountViewModel.swift new file mode 100644 index 0000000000..5009d9a3c9 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Model/DirectStakingTypeAccountViewModel.swift @@ -0,0 +1,6 @@ +struct DirectStakingTypeAccountViewModel { + let count: String? + let title: String + let subtitle: String? + let isRecommended: Bool +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Model/SelectedStakingViewModelFactory.swift b/novawallet/Modules/Staking/StakingSetupAmount/Model/SelectedStakingViewModelFactory.swift new file mode 100644 index 0000000000..dd30a4ec6e --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Model/SelectedStakingViewModelFactory.swift @@ -0,0 +1,95 @@ +import Foundation +import SoraFoundation +import BigInt + +protocol SelectedStakingViewModelFactoryProtocol { + func createRecommended( + for stakingType: SelectedStakingOption, + locale: Locale + ) -> RecommendedStakingTypeViewModel + + func createValidator( + for validators: PreparedValidators, + displaysRecommended: Bool, + locale: Locale + ) -> DirectStakingTypeViewModel.ValidatorModel + + func createPool( + for pool: NominationPools.SelectedPool, + chainAsset: ChainAsset, + displaysRecommended: Bool, + locale: Locale + ) -> PoolStakingTypeViewModel.PoolAccountModel +} + +final class SelectedStakingViewModelFactory {} + +extension SelectedStakingViewModelFactory: SelectedStakingViewModelFactoryProtocol { + func createRecommended( + for stakingType: SelectedStakingOption, + locale: Locale + ) -> RecommendedStakingTypeViewModel { + switch stakingType { + case .direct: + return RecommendedStakingTypeViewModel( + title: R.string.localizable.stakingDirectStaking(preferredLanguages: locale.rLanguages), + subtitle: R.string.localizable.commonRecommended(preferredLanguages: locale.rLanguages) + ) + case .pool: + return RecommendedStakingTypeViewModel( + title: R.string.localizable.stakingPoolStaking(preferredLanguages: locale.rLanguages), + subtitle: R.string.localizable.commonRecommended(preferredLanguages: locale.rLanguages) + ) + } + } + + func createValidator( + for validators: PreparedValidators, + displaysRecommended: Bool, + locale: Locale + ) -> DirectStakingTypeViewModel.ValidatorModel { + let strings = R.string.localizable.self + + if displaysRecommended { + return .init( + title: strings.stakingTypeDirect(preferredLanguages: locale.rLanguages), + subtitle: strings.stakingTypeRecommendedValidatorsSubtitle(preferredLanguages: locale.rLanguages), + isRecommended: true, + count: nil + ) + } else { + let validatorsString = strings.stakingCustomHeaderValidatorsTitle( + validators.targets.count, + validators.maxTargets, + preferredLanguages: locale.rLanguages + ) + + return .init( + title: strings.stakingTypeDirect(preferredLanguages: locale.rLanguages), + subtitle: validatorsString, + isRecommended: false, + count: nil + ) + } + } + + func createPool( + for pool: NominationPools.SelectedPool, + chainAsset: ChainAsset, + displaysRecommended: Bool, + locale: Locale + ) -> PoolStakingTypeViewModel.PoolAccountModel { + let poolName = pool.title(for: chainAsset.chain.chainFormat) ?? "" + let title = R.string.localizable.stakingPoolStaking(preferredLanguages: locale.rLanguages) + let subtitle = displaysRecommended ? R.string.localizable.commonRecommended( + preferredLanguages: locale.rLanguages + ) : poolName + + return PoolStakingTypeViewModel.PoolAccountModel( + icon: nil, + title: title, + subtitle: subtitle, + isRecommended: displaysRecommended + ) + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingAmountViewModelFactory.swift b/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingAmountViewModelFactory.swift new file mode 100644 index 0000000000..138a892b83 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingAmountViewModelFactory.swift @@ -0,0 +1,47 @@ +import Foundation +import SoraFoundation +import BigInt + +protocol StakingAmountViewModelFactoryProtocol { + func balance(amount: BigUInt?, chainAsset: ChainAsset, locale: Locale) -> TitleHorizontalMultiValueView.Model + + func maxApy(for stakingType: SelectedStakingOption, locale: Locale) -> String +} + +struct StakingAmountViewModelFactory: StakingAmountViewModelFactoryProtocol { + let balanceViewModelFactory: BalanceViewModelFactoryProtocol + let estimatedEarningsFormatter: LocalizableResource + + init( + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + estimatedEarningsFormatter: LocalizableResource + ) { + self.balanceViewModelFactory = balanceViewModelFactory + self.estimatedEarningsFormatter = estimatedEarningsFormatter + } + + func balance( + amount: BigUInt?, + chainAsset: ChainAsset, + locale: Locale + ) -> TitleHorizontalMultiValueView.Model { + let balance = balanceViewModelFactory.balanceWithPriceIfPossible( + amount: amount, + priceData: nil, + chainAsset: chainAsset + ).value(for: locale) + + let title = R.string.localizable.walletSendAmountTitle(preferredLanguages: locale.rLanguages) + let available = R.string.localizable.commonAvailablePrefix(preferredLanguages: locale.rLanguages) + return .init( + title: title, + subtitle: available, + value: balance.amount + ) + } + + func maxApy(for stakingType: SelectedStakingOption, locale: Locale) -> String { + let maxApy = stakingType.maxApy ?? 0 + return estimatedEarningsFormatter.value(for: locale).stringFromDecimal(maxApy) ?? "" + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingSelectionMethod.swift b/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingSelectionMethod.swift new file mode 100644 index 0000000000..3df19890d8 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingSelectionMethod.swift @@ -0,0 +1,42 @@ +import Foundation + +enum StakingSelectionMethod { + case recommendation(RelaychainStakingRecommendation?) + case manual(SelectedStakingOption, RelaychainStakingRestrictions) + + var isRecommendation: Bool { + switch self { + case .recommendation: + return true + case .manual: + return false + } + } + + var selectedStakingOption: SelectedStakingOption? { + switch self { + case let .recommendation(recommendation): + return recommendation?.staking + case let .manual(staking, _): + return staking + } + } + + var restrictions: RelaychainStakingRestrictions? { + switch self { + case let .recommendation(recommendation): + return recommendation?.restrictions + case let .manual(_, restrictions): + return restrictions + } + } + + var recommendation: RelaychainStakingRecommendation? { + switch self { + case let .recommendation(recommendation): + return recommendation + case let .manual: + return nil + } + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingSetupTypeEntityFacade.swift b/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingSetupTypeEntityFacade.swift new file mode 100644 index 0000000000..8546345893 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingSetupTypeEntityFacade.swift @@ -0,0 +1,69 @@ +import Foundation + +final class StakingSetupTypeEntityFacade { + static var associationKey: String = "com.nova.wallet.setup.entity.flow" + + weak var delegate: StakingTypeDelegate? + let selectedMethod: StakingSelectionMethod + + init(selectedMethod: StakingSelectionMethod, delegate: StakingTypeDelegate?) { + self.selectedMethod = selectedMethod + self.delegate = delegate + } + + func bindToFlow(controller: AnyObject) { + objc_setAssociatedObject( + controller, + &Self.associationKey, + self, + .OBJC_ASSOCIATION_RETAIN + ) + } + + private func convert(validatorList: [SelectedValidatorInfo], maxTargets: Int) -> StakingSelectionMethod? { + guard case let .direct(validators) = selectedMethod.selectedStakingOption, + let restrictions = selectedMethod.restrictions else { + return nil + } + + let selectedAddresses = validatorList.map(\.address) + + let usedRecommendation = Set(selectedAddresses) == Set(validators.recommendedValidators.map(\.address)) + return .manual(.init( + staking: .direct(.init( + targets: validatorList, + maxTargets: maxTargets, + electedAndPrefValidators: validators.electedAndPrefValidators, + recommendedValidators: validators.recommendedValidators + )), + restrictions: restrictions, + usedRecommendation: usedRecommendation + )) + } +} + +extension StakingSetupTypeEntityFacade: StakingSelectValidatorsDelegateProtocol { + func changeValidatorsSelection(validatorList: [SelectedValidatorInfo], maxTargets: Int) { + guard let method = convert(validatorList: validatorList, maxTargets: maxTargets) else { + return + } + + delegate?.changeStakingType(method: method) + } +} + +extension StakingSetupTypeEntityFacade: StakingSelectPoolDelegate { + func changePoolSelection(selectedPool: NominationPools.SelectedPool, isRecommended: Bool) { + guard let restrictions = selectedMethod.restrictions else { + return + } + + let method = StakingSelectionMethod.manual(.init( + staking: .pool(selectedPool), + restrictions: restrictions, + usedRecommendation: isRecommended + )) + + delegate?.changeStakingType(method: method) + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingTypeAccountViewModel.swift b/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingTypeAccountViewModel.swift new file mode 100644 index 0000000000..e894f38781 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingTypeAccountViewModel.swift @@ -0,0 +1,6 @@ +struct StakingTypeAccountViewModel { + let imageViewModel: ImageViewModelProtocol? + let title: String + let subtitle: String? + let isRecommended: Bool +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingTypeBalanceFactory.swift b/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingTypeBalanceFactory.swift new file mode 100644 index 0000000000..b0f284fa32 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingTypeBalanceFactory.swift @@ -0,0 +1,87 @@ +import Foundation +import BigInt + +protocol StakingTypeBalanceFactoryProtocol: AnyObject { + func getAvailableBalance( + from assetBalance: AssetBalance?, + stakingMethod: StakingSelectionMethod + ) -> BigUInt? + + func getStakeableBalance( + from assetBalance: AssetBalance?, + existentialDeposit: BigUInt?, + stakingMethod: StakingSelectionMethod + ) -> BigUInt? +} + +extension StakingTypeBalanceFactoryProtocol { + func getAvailableBalance(from assetBalance: AssetBalance?) -> BigUInt? { + getAvailableBalance(from: assetBalance, stakingMethod: .recommendation(nil)) + } +} + +final class StakingTypeBalanceFactory: StakingTypeBalanceFactoryProtocol { + let stakingType: StakingType? + + init(stakingType: StakingType?) { + self.stakingType = stakingType + } + + var stakingTypeAllowsLocks: Bool { + switch stakingType { + case .relaychain, .auraRelaychain, .azero, .none, .parachain, .turing: + return true + case .nominationPools, .unsupported: + return false + } + } + + private func getManualAvailableBalance( + for assetBalance: AssetBalance?, + stakingOption: SelectedStakingOption + ) -> BigUInt? { + switch stakingOption { + case .direct: + return assetBalance?.freeInPlank + case .pool: + return assetBalance?.transferable + } + } + + func getAvailableBalance( + from assetBalance: AssetBalance?, + stakingMethod: StakingSelectionMethod + ) -> BigUInt? { + switch stakingMethod { + case .recommendation: + return stakingTypeAllowsLocks ? assetBalance?.freeInPlank : assetBalance?.transferable + case let .manual(stakingManual): + return getManualAvailableBalance(for: assetBalance, stakingOption: stakingManual.staking) + } + } + + func getStakeableBalance( + from assetBalance: AssetBalance?, + existentialDeposit: BigUInt?, + stakingMethod: StakingSelectionMethod + ) -> BigUInt? { + let optAvailableBalance = getAvailableBalance(from: assetBalance, stakingMethod: stakingMethod) + + switch stakingMethod.selectedStakingOption { + case .pool: + guard + let existentialDeposit = existentialDeposit, + let assetBalance = assetBalance, + let availableBalance = optAvailableBalance else { + return optAvailableBalance + } + + let totalMinusEd = assetBalance.totalInPlank >= existentialDeposit ? + assetBalance.totalInPlank - existentialDeposit : 0 + + return min(availableBalance, totalMinusEd) + case .none, .direct: + return optAvailableBalance + } + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingTypeViewModel.swift b/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingTypeViewModel.swift new file mode 100644 index 0000000000..0278d0b0af --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Model/StakingTypeViewModel.swift @@ -0,0 +1,16 @@ +struct StakingTypeViewModel { + enum TypeModel { + case recommended(RecommendedStakingTypeViewModel) + case direct(DirectStakingTypeViewModel.ValidatorModel) + case pools(PoolStakingTypeViewModel.PoolAccountModel) + } + + let type: TypeModel + let maxApy: String + let shouldEnableSelection: Bool +} + +struct RecommendedStakingTypeViewModel { + let title: String + let subtitle: String +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/DirectStakingRecommendationFactory.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/DirectStakingRecommendationFactory.swift new file mode 100644 index 0000000000..b8234c6f49 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/DirectStakingRecommendationFactory.swift @@ -0,0 +1,90 @@ +import Foundation +import RobinHood + +protocol DirectStakingRecommendationFactoryProtocol: AnyObject { + func createValidatorsRecommendationWrapper() -> CompoundOperationWrapper +} + +final class DirectStakingRecommendationFactory { + let runtimeProvider: RuntimeCodingServiceProtocol + let operationFactory: ValidatorOperationFactoryProtocol + let defaultMaxNominations: Int + let clusterLimit: Int + let preferredValidators: [AccountId] + + init( + runtimeProvider: RuntimeCodingServiceProtocol, + operationFactory: ValidatorOperationFactoryProtocol, + defaultMaxNominations: Int = SubstrateConstants.maxNominations, + clusterLimit: Int = StakingConstants.targetsClusterLimit, + preferredValidators: [AccountId] + ) { + self.runtimeProvider = runtimeProvider + self.operationFactory = operationFactory + self.defaultMaxNominations = defaultMaxNominations + self.clusterLimit = clusterLimit + self.preferredValidators = preferredValidators + } + + private func createRecommendationOperation( + dependingOn validatorsWrapper: CompoundOperationWrapper, + maxNominationsOperation: BaseOperation, + clusterLimit: Int + ) -> BaseOperation { + ClosureOperation { + let validators = try validatorsWrapper.targetOperation.extractNoCancellableResultData() + let maxNominations = try maxNominationsOperation.extractNoCancellableResultData() + + let resultLimit = min(validators.electedValidators.count, maxNominations) + let recommendedValidators = RecommendationsComposer( + resultSize: resultLimit, + clusterSizeLimit: clusterLimit + ).compose( + from: validators.electedToSelectedValidators(), + preferrences: validators.preferredValidators + ) + + return PreparedValidators( + targets: recommendedValidators, + maxTargets: resultLimit, + electedAndPrefValidators: validators, + recommendedValidators: recommendedValidators + ) + } + } +} + +extension DirectStakingRecommendationFactory: DirectStakingRecommendationFactoryProtocol { + func createValidatorsRecommendationWrapper() -> CompoundOperationWrapper { + let validatorsWrapper = operationFactory.allPreferred(for: preferredValidators) + + let codingFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() + let maxNominationsOperation = PrimitiveConstantOperation( + path: .maxNominations, + fallbackValue: defaultMaxNominations + ) + + maxNominationsOperation.configurationBlock = { + do { + maxNominationsOperation.codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + } catch { + maxNominationsOperation.result = .failure(error) + } + } + + maxNominationsOperation.addDependency(codingFactoryOperation) + + let recommendationOperation = createRecommendationOperation( + dependingOn: validatorsWrapper, + maxNominationsOperation: maxNominationsOperation, + clusterLimit: clusterLimit + ) + + recommendationOperation.addDependency(maxNominationsOperation) + recommendationOperation.addDependency(validatorsWrapper.targetOperation) + + let dependecies = validatorsWrapper.allOperations + [codingFactoryOperation, maxNominationsOperation] + + return CompoundOperationWrapper(targetOperation: recommendationOperation, dependencies: dependecies) + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/DirectStakingRecommendationMediator.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/DirectStakingRecommendationMediator.swift new file mode 100644 index 0000000000..e68716d5ec --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/DirectStakingRecommendationMediator.swift @@ -0,0 +1,93 @@ +import Foundation +import RobinHood +import SubstrateSdk +import BigInt + +class DirectStakingRecommendationMediator: BaseStakingRecommendationMediator { + let recommendationFactory: DirectStakingRecommendationFactoryProtocol + let restrictionsBuilder: RelaychainStakingRestrictionsBuilding + let operationQueue: OperationQueue + + var restrictions: RelaychainStakingRestrictions? + + init( + recommendationFactory: DirectStakingRecommendationFactoryProtocol, + restrictionsBuilder: RelaychainStakingRestrictionsBuilding, + operationQueue: OperationQueue + ) { + self.recommendationFactory = recommendationFactory + self.restrictionsBuilder = restrictionsBuilder + self.operationQueue = operationQueue + } + + private func handle(validators: PreparedValidators, amount: BigUInt) { + guard let restrictions = restrictions else { + return + } + + let recommendation = RelaychainStakingRecommendation( + staking: .direct(validators), + restrictions: restrictions, + validationFactory: nil + ) + + didReceive(recommendation: recommendation, for: amount) + } + + override func updateRecommendation(for amount: BigUInt) { + if let recommendation = recommendation { + didReceive(recommendation: recommendation, for: amount) + return + } + + let wrapper = recommendationFactory.createValidatorsRecommendationWrapper() + + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard self?.pendingOperation === wrapper else { + return + } + + self?.pendingOperation = nil + + do { + let validators = try wrapper.targetOperation.extractNoCancellableResultData() + self?.handle(validators: validators, amount: amount) + } catch { + self?.delegate?.didReceiveRecommendation(error: error) + } + } + } + + pendingOperation = wrapper + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } + + override func performSetup() { + restrictionsBuilder.delegate = self + restrictionsBuilder.start() + } + + override func clearState() { + restrictionsBuilder.delegate = nil + restrictionsBuilder.stop() + } +} + +extension DirectStakingRecommendationMediator: RelaychainStakingRestrictionsBuilderDelegate { + func restrictionsBuilder( + _: RelaychainStakingRestrictionsBuilding, + didPrepare restrictions: RelaychainStakingRestrictions + ) { + self.restrictions = restrictions + + isReady = true + + updateRecommendationIfReady() + } + + func restrictionsBuilder(_: RelaychainStakingRestrictionsBuilding, didReceive error: Error) { + delegate?.didReceiveRecommendation(error: error) + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/DirectStakingRestrictionsBuilder.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/DirectStakingRestrictionsBuilder.swift new file mode 100644 index 0000000000..6ed25c36e5 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/DirectStakingRestrictionsBuilder.swift @@ -0,0 +1,176 @@ +import Foundation +import RobinHood +import BigInt + +final class DirectStakingRestrictionsBuilder: AnyCancellableCleaning { + let chainAsset: ChainAsset + let stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol + let networkInfoFactory: NetworkStakingInfoOperationFactoryProtocol + let eraValidatorService: EraValidatorServiceProtocol + let runtimeService: RuntimeCodingServiceProtocol + let operationQueue: OperationQueue + + private var minJoinStakeStorage: UncertainStorage = .undefined + private var minRewardableStakeStorage: UncertainStorage = .undefined + private var counterForNominatorsStorage: UncertainStorage = .undefined + private var maxNominatorCountStorage: UncertainStorage = .undefined + + private var bagListProvider: AnyDataProvider? + private var minBondProvider: AnyDataProvider? + private var counterForNominatorsProvider: AnyDataProvider? + private var maxNominatorsCountProvider: AnyDataProvider? + + private var networkInfoCancellable: CancellableCall? + + private var minRewardableStakeBuilder: DirectStakingMinStakeBuilder? + + weak var delegate: RelaychainStakingRestrictionsBuilderDelegate? + + init( + chainAsset: ChainAsset, + stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol, + networkInfoFactory: NetworkStakingInfoOperationFactoryProtocol, + eraValidatorService: EraValidatorServiceProtocol, + runtimeService: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue + ) { + self.chainAsset = chainAsset + self.stakingLocalSubscriptionFactory = stakingLocalSubscriptionFactory + self.networkInfoFactory = networkInfoFactory + self.eraValidatorService = eraValidatorService + self.runtimeService = runtimeService + self.operationQueue = operationQueue + } + + deinit { + clear(cancellable: &networkInfoCancellable) + } + + private func provideNetworkInfo() { + let wrapper = networkInfoFactory.networkStakingOperation( + for: eraValidatorService, + runtimeService: runtimeService + ) + + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard let self = self, self.networkInfoCancellable === wrapper else { + return + } + + self.networkInfoCancellable = nil + + do { + let networkInfo = try wrapper.targetOperation.extractNoCancellableResultData() + self.minRewardableStakeBuilder?.apply(param1: networkInfo) + } catch { + self.delegate?.restrictionsBuilder(self, didReceive: error) + } + } + } + + networkInfoCancellable = wrapper + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } + + private func sendUpdateIfReady() { + guard + minJoinStakeStorage.isDefined, + minRewardableStakeStorage.isDefined, + counterForNominatorsStorage.isDefined, + maxNominatorCountStorage.isDefined else { + return + } + + let allowsNewStakers: Bool + + if + let counterForNominators = counterForNominatorsStorage.valueWhenDefined(else: nil), + let maxNominatorsCount = maxNominatorCountStorage.valueWhenDefined(else: nil) { + allowsNewStakers = counterForNominators < maxNominatorsCount + } else { + allowsNewStakers = true + } + + let restrictions = RelaychainStakingRestrictions( + minJoinStake: minJoinStakeStorage.valueWhenDefined(else: nil), + minRewardableStake: minRewardableStakeStorage.valueWhenDefined(else: nil), + allowsNewStakers: allowsNewStakers + ) + + delegate?.restrictionsBuilder(self, didPrepare: restrictions) + } +} + +extension DirectStakingRestrictionsBuilder: RelaychainStakingRestrictionsBuilding { + func start() { + minRewardableStakeBuilder = DirectStakingMinStakeBuilder { [weak self] value in + self?.minRewardableStakeStorage = .defined(value) + self?.sendUpdateIfReady() + } + + minBondProvider = subscribeToMinNominatorBond(for: chainAsset.chain.chainId) + bagListProvider = subscribeBagsListSize(for: chainAsset.chain.chainId) + counterForNominatorsProvider = subscribeToCounterForNominators(for: chainAsset.chain.chainId) + maxNominatorsCountProvider = subscribeMaxNominatorsCount(for: chainAsset.chain.chainId) + + provideNetworkInfo() + } + + func stop() { + clear(cancellable: &networkInfoCancellable) + + minBondProvider = nil + bagListProvider = nil + maxNominatorsCountProvider = nil + counterForNominatorsProvider = nil + minRewardableStakeBuilder = nil + + minJoinStakeStorage = .undefined + minRewardableStakeStorage = .undefined + counterForNominatorsStorage = .undefined + maxNominatorCountStorage = .undefined + } +} + +extension DirectStakingRestrictionsBuilder: StakingLocalStorageSubscriber, StakingLocalSubscriptionHandler { + func handleMinNominatorBond(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(minNominatorBond): + minJoinStakeStorage = .defined(minNominatorBond) + minRewardableStakeBuilder?.apply(param3: minNominatorBond) + case let .failure(error): + delegate?.restrictionsBuilder(self, didReceive: error) + } + } + + func handleBagListSize(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(bagListSize): + minRewardableStakeBuilder?.apply(param2: bagListSize) + case let .failure(error): + delegate?.restrictionsBuilder(self, didReceive: error) + } + } + + func handleCounterForNominators(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(counterForNominators): + counterForNominatorsStorage = .defined(counterForNominators) + sendUpdateIfReady() + case let .failure(error): + delegate?.restrictionsBuilder(self, didReceive: error) + } + } + + func handleMaxNominatorsCount(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(maxNominatorCount): + maxNominatorCountStorage = .defined(maxNominatorCount) + sendUpdateIfReady() + case let .failure(error): + delegate?.restrictionsBuilder(self, didReceive: error) + } + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/HybridStakingRecommendationMediator.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/HybridStakingRecommendationMediator.swift new file mode 100644 index 0000000000..6a9d3217b8 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/HybridStakingRecommendationMediator.swift @@ -0,0 +1,114 @@ +import Foundation +import BigInt + +final class HybridStakingRecommendationMediator: BaseStakingRecommendationMediator { + let directStakingMediator: RelaychainStakingRecommendationMediating + let nominationPoolsMediator: RelaychainStakingRecommendationMediating + let directStakingRestrictionsBuilder: RelaychainStakingRestrictionsBuilding + let chainAsset: ChainAsset + + private var restrictions: RelaychainStakingRestrictions? + + private var validationFactory: StakingRecommendationValidationFactoryProtocol? + + init( + chainAsset: ChainAsset, + directStakingMediator: RelaychainStakingRecommendationMediating, + nominationPoolsMediator: RelaychainStakingRecommendationMediating, + directStakingRestrictionsBuilder: RelaychainStakingRestrictionsBuilding + ) { + self.chainAsset = chainAsset + self.directStakingMediator = directStakingMediator + self.nominationPoolsMediator = nominationPoolsMediator + self.directStakingRestrictionsBuilder = directStakingRestrictionsBuilder + } + + private func updateValidationFactory() { + if let minRewardableStake = restrictions?.minRewardableStake { + validationFactory = HybridStakingValidationFactory( + directRewardableStake: minRewardableStake, + chainAsset: chainAsset + ) + } else { + validationFactory = nil + } + } + + override func updateRecommendation(for amount: BigUInt) { + guard let restrictions = restrictions else { + return + } + + if let minStake = restrictions.minRewardableStake, amount < minStake { + directStakingMediator.delegate = nil + nominationPoolsMediator.delegate = self + + nominationPoolsMediator.update(amount: amount) + } else { + directStakingMediator.delegate = self + nominationPoolsMediator.delegate = nil + + directStakingMediator.update(amount: amount) + } + } + + override func performSetup() { + directStakingMediator.startRecommending() + nominationPoolsMediator.startRecommending() + + directStakingRestrictionsBuilder.delegate = self + directStakingRestrictionsBuilder.start() + } + + override func clearState() { + super.clearState() + + directStakingMediator.delegate = nil + directStakingMediator.stopRecommending() + + nominationPoolsMediator.delegate = nil + nominationPoolsMediator.stopRecommending() + + directStakingRestrictionsBuilder.delegate = nil + directStakingRestrictionsBuilder.stop() + } +} + +extension HybridStakingRecommendationMediator: RelaychainStakingRestrictionsBuilderDelegate { + func restrictionsBuilder( + _: RelaychainStakingRestrictionsBuilding, + didPrepare restrictions: RelaychainStakingRestrictions + ) { + self.restrictions = restrictions + + updateValidationFactory() + + isReady = true + updateRecommendationIfReady() + } + + func restrictionsBuilder(_: RelaychainStakingRestrictionsBuilding, didReceive error: Error) { + delegate?.didReceiveRecommendation(error: error) + } +} + +extension HybridStakingRecommendationMediator: RelaychainStakingRecommendationDelegate { + func didReceive(recommendation: RelaychainStakingRecommendation, amount: BigUInt) { + let factories = [ + recommendation.validationFactory, + validationFactory + ].compactMap { $0 } + + let hybridRecommendation = RelaychainStakingRecommendation( + staking: recommendation.staking, + restrictions: recommendation.restrictions, + validationFactory: CombinedStakingValidationFactory(factories: factories) + ) + + delegate?.didReceive(recommendation: hybridRecommendation, amount: amount) + } + + func didReceiveRecommendation(error: Error) { + delegate?.didReceiveRecommendation(error: error) + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/NominationPoolRecommendationFactory.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/NominationPoolRecommendationFactory.swift new file mode 100644 index 0000000000..8fc95e0531 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/NominationPoolRecommendationFactory.swift @@ -0,0 +1,91 @@ +import Foundation +import RobinHood +import SubstrateSdk + +protocol NominationPoolRecommendationFactoryProtocol: AnyObject { + func createPoolRecommendationWrapper( + for maxMembersPerPool: UInt32?, + preferrablePool: NominationPools.PoolId? + ) -> CompoundOperationWrapper +} + +enum NominationPoolRecommendationFactoryError: Error { + case noPoolToJoin +} + +final class NominationPoolRecommendationFactory { + let eraPoolsService: EraNominationPoolsServiceProtocol + let validatorRewardService: RewardCalculatorServiceProtocol + let rewardEngineOperationFactory: NPoolsRewardEngineFactoryProtocol + let connection: JSONRPCEngine + let runtimeService: RuntimeCodingServiceProtocol + let storageOperationFactory: NominationPoolsOperationFactoryProtocol + + init( + eraPoolsService: EraNominationPoolsServiceProtocol, + validatorRewardService: RewardCalculatorServiceProtocol, + rewardEngineOperationFactory: NPoolsRewardEngineFactoryProtocol, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol, + storageOperationFactory: NominationPoolsOperationFactoryProtocol + ) { + self.eraPoolsService = eraPoolsService + self.validatorRewardService = validatorRewardService + self.rewardEngineOperationFactory = rewardEngineOperationFactory + self.connection = connection + self.runtimeService = runtimeService + self.storageOperationFactory = storageOperationFactory + } +} + +extension NominationPoolRecommendationFactory: NominationPoolRecommendationFactoryProtocol { + func createPoolRecommendationWrapper( + for maxMembersPerPool: UInt32?, + preferrablePool: NominationPools.PoolId? + ) -> CompoundOperationWrapper { + let maxApyWrapper = rewardEngineOperationFactory.createEngineWrapper( + for: eraPoolsService, + validatorRewardService: validatorRewardService, + connection: connection, + runtimeService: runtimeService + ) + + let params = RecommendedNominationPoolsParams( + maxMembersPerPool: { maxMembersPerPool }, + preferrablePool: { preferrablePool } + ) + + let poolStatsWrapper = storageOperationFactory.createPoolRecommendationsInfoWrapper( + for: eraPoolsService, + rewardEngine: { + try maxApyWrapper.targetOperation.extractNoCancellableResultData() + }, + params: params, + connection: connection, + runtimeService: runtimeService + ) + + poolStatsWrapper.addDependency(wrapper: maxApyWrapper) + + let mergeOperation = ClosureOperation { + let poolStatsList = try poolStatsWrapper.targetOperation.extractNoCancellableResultData() + + guard let maxPoolStats = poolStatsList.first else { + throw NominationPoolRecommendationFactoryError.noPoolToJoin + } + + return .init( + poolId: maxPoolStats.poolId, + bondedAccountId: maxPoolStats.bondedAccountId, + metadata: maxPoolStats.metadata, + maxApy: maxPoolStats.maxApy + ) + } + + mergeOperation.addDependency(poolStatsWrapper.targetOperation) + + let dependencies = maxApyWrapper.allOperations + poolStatsWrapper.allOperations + + return CompoundOperationWrapper(targetOperation: mergeOperation, dependencies: dependencies) + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/PoolStakingRecommendationMediator.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/PoolStakingRecommendationMediator.swift new file mode 100644 index 0000000000..10ad5282fc --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/PoolStakingRecommendationMediator.swift @@ -0,0 +1,138 @@ +import Foundation +import BigInt +import RobinHood + +final class PoolStakingRecommendationMediator: BaseStakingRecommendationMediator { + let restrictionsBuilder: RelaychainStakingRestrictionsBuilding + let operationFactory: NominationPoolRecommendationFactoryProtocol + let operationQueue: OperationQueue + + let chainAsset: ChainAsset + let npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol + + var restrictions: RelaychainStakingRestrictions? + + private var maxMembersPerPoolStorage: UncertainStorage = .undefined + private var maxMembersPerPoolProvider: AnyDataProvider? + + init( + chainAsset: ChainAsset, + npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, + restrictionsBuilder: RelaychainStakingRestrictionsBuilding, + operationFactory: NominationPoolRecommendationFactoryProtocol, + operationQueue: OperationQueue + ) { + self.chainAsset = chainAsset + self.restrictionsBuilder = restrictionsBuilder + self.npoolsLocalSubscriptionFactory = npoolsLocalSubscriptionFactory + self.operationFactory = operationFactory + self.operationQueue = operationQueue + } + + private func updateReadyState() { + isReady = restrictions != nil && maxMembersPerPoolStorage.isDefined + } + + private func handle(pool: NominationPools.SelectedPool, amount: BigUInt) { + guard let restrictions = restrictions else { + return + } + + let recommendation = RelaychainStakingRecommendation( + staking: .pool(pool), + restrictions: restrictions, + validationFactory: nil + ) + + didReceive(recommendation: recommendation, for: amount) + } + + override func updateRecommendation(for amount: BigUInt) { + guard case let .defined(maxMembersPerPool) = maxMembersPerPoolStorage else { + return + } + + if let recommendation = recommendation { + didReceive(recommendation: recommendation, for: amount) + return + } + + let preferrablePool = StakingConstants.recommendedPoolIds[chainAsset.chain.chainId] + let wrapper = operationFactory.createPoolRecommendationWrapper( + for: maxMembersPerPool, + preferrablePool: preferrablePool + ) + + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard self?.pendingOperation === wrapper else { + return + } + + self?.pendingOperation = nil + + do { + let pool = try wrapper.targetOperation.extractNoCancellableResultData() + self?.handle(pool: pool, amount: amount) + } catch { + self?.delegate?.didReceiveRecommendation(error: error) + } + } + } + + pendingOperation = wrapper + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } + + override func performSetup() { + maxMembersPerPoolProvider = subscribeMaxPoolMembersPerPool(for: chainAsset.chain.chainId) + + restrictionsBuilder.delegate = self + restrictionsBuilder.start() + } + + override func clearState() { + maxMembersPerPoolProvider = nil + + restrictionsBuilder.delegate = nil + restrictionsBuilder.stop() + } +} + +extension PoolStakingRecommendationMediator: NPoolsLocalStorageSubscriber, NPoolsLocalSubscriptionHandler { + func handleMaxPoolMembersPerPool(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(value): + let shouldUpdate = maxMembersPerPoolStorage + .map { $0 != value } + .value ?? true + + maxMembersPerPoolStorage = .defined(value) + + updateReadyState() + + if shouldUpdate { + updateRecommendationIfReady() + } + case let .failure(error): + delegate?.didReceiveRecommendation(error: error) + } + } +} + +extension PoolStakingRecommendationMediator: RelaychainStakingRestrictionsBuilderDelegate { + func restrictionsBuilder( + _: RelaychainStakingRestrictionsBuilding, + didPrepare restrictions: RelaychainStakingRestrictions + ) { + self.restrictions = restrictions + + updateReadyState() + updateRecommendationIfReady() + } + + func restrictionsBuilder(_: RelaychainStakingRestrictionsBuilding, didReceive error: Error) { + delegate?.didReceiveRecommendation(error: error) + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/PoolStakingRestrictionsBuilder.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/PoolStakingRestrictionsBuilder.swift new file mode 100644 index 0000000000..fe242f3fb0 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/PoolStakingRestrictionsBuilder.swift @@ -0,0 +1,90 @@ +import Foundation +import RobinHood +import BigInt + +final class PoolStakingRestrictionsBuilder { + let chainAsset: ChainAsset + let npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol + + weak var delegate: RelaychainStakingRestrictionsBuilderDelegate? + + private var minJoinBondProvider: AnyDataProvider? + private var maxPoolMembersProvider: AnyDataProvider? + private var counterForPoolMembersProvider: AnyDataProvider? + + private var minJoinBond: BigUInt? + private var maxPoolMembers: UInt32? + private var counterForPoolMembers: UInt32? + + init( + chainAsset: ChainAsset, + npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol + ) { + self.chainAsset = chainAsset + self.npoolsLocalSubscriptionFactory = npoolsLocalSubscriptionFactory + } + + func sendRestrictions() { + let allowsNewStakers: Bool + + if let maxPoolMembers = maxPoolMembers, let counterForPoolMembers = counterForPoolMembers { + allowsNewStakers = counterForPoolMembers < maxPoolMembers + } else { + allowsNewStakers = true + } + + let restrictions = RelaychainStakingRestrictions( + minJoinStake: minJoinBond, + minRewardableStake: minJoinBond, + allowsNewStakers: allowsNewStakers + ) + + delegate?.restrictionsBuilder(self, didPrepare: restrictions) + } +} + +extension PoolStakingRestrictionsBuilder: RelaychainStakingRestrictionsBuilding { + func start() { + minJoinBondProvider = subscribeMinJoinBond(for: chainAsset.chain.chainId) + maxPoolMembersProvider = subscribeMaxPoolMembers(for: chainAsset.chain.chainId) + counterForPoolMembersProvider = subscribeCounterForPoolMembers(for: chainAsset.chain.chainId) + } + + func stop() { + minJoinBondProvider = nil + maxPoolMembersProvider = nil + counterForPoolMembersProvider = nil + } +} + +extension PoolStakingRestrictionsBuilder: NPoolsLocalStorageSubscriber, NPoolsLocalSubscriptionHandler { + func handleMinJoinBond(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(minJoinBond): + self.minJoinBond = minJoinBond + sendRestrictions() + case let .failure(error): + delegate?.restrictionsBuilder(self, didReceive: error) + } + } + + func handleMaxPoolMembers(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(maxPoolMembers): + self.maxPoolMembers = maxPoolMembers + sendRestrictions() + case let .failure(error): + delegate?.restrictionsBuilder(self, didReceive: error) + } + } + + func handleCounterForPoolMembers(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(counterForPoolMembers): + self.counterForPoolMembers = counterForPoolMembers + sendRestrictions() + case let .failure(error): + delegate?.restrictionsBuilder(self, didReceive: error) + } + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/RelaychainStakingRecommendation.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/RelaychainStakingRecommendation.swift new file mode 100644 index 0000000000..90b186ceb0 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/RelaychainStakingRecommendation.swift @@ -0,0 +1,14 @@ +import Foundation +import BigInt + +struct RelaychainStakingRecommendation { + let staking: SelectedStakingOption + let restrictions: RelaychainStakingRestrictions + let validationFactory: StakingRecommendationValidationFactoryProtocol? +} + +struct RelaychainStakingManual { + let staking: SelectedStakingOption + let restrictions: RelaychainStakingRestrictions + let usedRecommendation: Bool +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/RelaychainStakingRecommendationMediator.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/RelaychainStakingRecommendationMediator.swift new file mode 100644 index 0000000000..9050af8d95 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/RelaychainStakingRecommendationMediator.swift @@ -0,0 +1,94 @@ +import Foundation +import BigInt +import RobinHood + +protocol RelaychainStakingRecommendationMediating: AnyObject { + var delegate: RelaychainStakingRecommendationDelegate? { get set } + + func startRecommending() + func update(amount: BigUInt) + func stopRecommending() +} + +protocol RelaychainStakingRecommendationDelegate: AnyObject { + func didReceive(recommendation: RelaychainStakingRecommendation, amount: BigUInt) + func didReceiveRecommendation(error: Error) +} + +class BaseStakingRecommendationMediator: AnyCancellableCleaning { + weak var delegate: RelaychainStakingRecommendationDelegate? + + var recommendation: RelaychainStakingRecommendation? + var amount: BigUInt? + var pendingOperation: CancellableCall? + + var isReady: Bool = false + + deinit { + cancelAllOperations() + } + + func updateRecommendationIfReady() { + clear(cancellable: &pendingOperation) + + if isReady, let amount = amount { + updateRecommendation(for: amount) + } + } + + func updateRecommendation(for _: BigUInt) { + fatalError("Must be overriden by subclass") + } + + func performSetup() { + fatalError("Must be overriden by subclass") + } + + func cancelAllOperations() { + clear(cancellable: &pendingOperation) + } + + func clearState() { + cancelAllOperations() + + recommendation = nil + amount = nil + isReady = false + } + + func didReceive(recommendation: RelaychainStakingRecommendation, for amount: BigUInt) { + self.recommendation = recommendation + self.amount = amount + + delegate?.didReceive(recommendation: recommendation, amount: amount) + } +} + +extension BaseStakingRecommendationMediator: RelaychainStakingRecommendationMediating { + func startRecommending() { + guard !isReady else { + return + } + + clearState() + + performSetup() + } + + func update(amount: BigUInt) { + guard isReady else { + self.amount = amount + return + } + + clear(cancellable: &pendingOperation) + + self.amount = amount + + updateRecommendation(for: amount) + } + + func stopRecommending() { + clearState() + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/RelaychainStakingRestrictions.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/RelaychainStakingRestrictions.swift new file mode 100644 index 0000000000..4b3e624b3f --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/RelaychainStakingRestrictions.swift @@ -0,0 +1,8 @@ +import Foundation +import BigInt + +struct RelaychainStakingRestrictions { + let minJoinStake: BigUInt? + let minRewardableStake: BigUInt? + let allowsNewStakers: Bool +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/RelaychainStakingRestrictionsBuilder.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/RelaychainStakingRestrictionsBuilder.swift new file mode 100644 index 0000000000..0441d4b0db --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/RelaychainStakingRestrictionsBuilder.swift @@ -0,0 +1,20 @@ +import Foundation + +protocol RelaychainStakingRestrictionsBuilding: AnyObject { + var delegate: RelaychainStakingRestrictionsBuilderDelegate? { get set } + + func start() + func stop() +} + +protocol RelaychainStakingRestrictionsBuilderDelegate: AnyObject { + func restrictionsBuilder( + _ builder: RelaychainStakingRestrictionsBuilding, + didPrepare restrictions: RelaychainStakingRestrictions + ) + + func restrictionsBuilder( + _ builder: RelaychainStakingRestrictionsBuilding, + didReceive error: Error + ) +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/StakingRecommendationMediatorFactory.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/StakingRecommendationMediatorFactory.swift new file mode 100644 index 0000000000..58fc32e148 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/StakingRecommendationMediatorFactory.swift @@ -0,0 +1,182 @@ +import Foundation +import SubstrateSdk +import RobinHood + +protocol StakingRecommendationMediatorFactoryProtocol { + func createDirectStakingMediator( + for state: RelaychainStartStakingStateProtocol + ) -> RelaychainStakingRecommendationMediating? + + func createPoolStakingMediator( + for state: RelaychainStartStakingStateProtocol + ) -> RelaychainStakingRecommendationMediating? + + func createHybridStakingMediator( + for state: RelaychainStartStakingStateProtocol + ) -> RelaychainStakingRecommendationMediating? + + func createDirectStakingRestrictionsBuilder( + for state: RelaychainStartStakingStateProtocol + ) -> RelaychainStakingRestrictionsBuilding? + + func createPoolStakingRestrictionsBuilder( + for state: RelaychainStartStakingStateProtocol + ) -> RelaychainStakingRestrictionsBuilding? +} + +final class StakingRecommendationMediatorFactory { + let chainRegistry: ChainRegistryProtocol + let operationQueue: OperationQueue + + init( + chainRegistry: ChainRegistryProtocol, + operationQueue: OperationQueue + ) { + self.chainRegistry = chainRegistry + self.operationQueue = operationQueue + } + + private func createDirectStakingRecommendationFactory( + for state: RelaychainStartStakingStateProtocol + ) -> DirectStakingRecommendationFactoryProtocol? { + let chain = state.chainAsset.chain + let chainId = chain.chainId + + guard + let runtimeService = chainRegistry.getRuntimeProvider(for: chainId), + let connection = chainRegistry.getConnection(for: chainId) else { + return nil + } + + let storageRequestFactory = StorageRequestFactory( + remoteFactory: StorageKeyFactory(), + operationManager: OperationManager(operationQueue: operationQueue) + ) + + let identityOperationFactory = IdentityOperationFactory( + requestFactory: storageRequestFactory + ) + + let validatorOperationFactory = ValidatorOperationFactory( + chainInfo: state.chainAsset.chainAssetInfo, + eraValidatorService: state.eraValidatorService, + rewardService: state.relaychainRewardCalculatorService, + storageRequestFactory: storageRequestFactory, + runtimeService: runtimeService, + engine: connection, + identityOperationFactory: identityOperationFactory + ) + + return DirectStakingRecommendationFactory( + runtimeProvider: runtimeService, + operationFactory: validatorOperationFactory, + defaultMaxNominations: SubstrateConstants.maxNominations, + clusterLimit: StakingConstants.targetsClusterLimit, + preferredValidators: StakingConstants.preferredValidatorIds(for: chain) + ) + } +} + +extension StakingRecommendationMediatorFactory: StakingRecommendationMediatorFactoryProtocol { + func createDirectStakingMediator( + for state: RelaychainStartStakingStateProtocol + ) -> RelaychainStakingRecommendationMediating? { + guard + let recommendationFactory = createDirectStakingRecommendationFactory(for: state), + let restrictionsBuilder = createDirectStakingRestrictionsBuilder(for: state) else { + return nil + } + + return DirectStakingRecommendationMediator( + recommendationFactory: recommendationFactory, + restrictionsBuilder: restrictionsBuilder, + operationQueue: operationQueue + ) + } + + func createPoolStakingMediator( + for state: RelaychainStartStakingStateProtocol + ) -> RelaychainStakingRecommendationMediating? { + let chainId = state.chainAsset.chain.chainId + + guard + let restrictionsBuilder = createPoolStakingRestrictionsBuilder(for: state), + let connection = chainRegistry.getConnection(for: chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + return nil + } + + guard let activePoolService = state.activePoolsService else { + return nil + } + + let poolsOperationFactory = NominationPoolsOperationFactory(operationQueue: operationQueue) + + let rewardCalculationFactory = NPoolsRewardEngineFactory(operationFactory: poolsOperationFactory) + + let operationFactory = NominationPoolRecommendationFactory( + eraPoolsService: activePoolService, + validatorRewardService: state.relaychainRewardCalculatorService, + rewardEngineOperationFactory: rewardCalculationFactory, + connection: connection, + runtimeService: runtimeService, + storageOperationFactory: poolsOperationFactory + ) + + return PoolStakingRecommendationMediator( + chainAsset: state.chainAsset, + npoolsLocalSubscriptionFactory: state.npLocalSubscriptionFactory, + restrictionsBuilder: restrictionsBuilder, + operationFactory: operationFactory, + operationQueue: operationQueue + ) + } + + func createHybridStakingMediator( + for state: RelaychainStartStakingStateProtocol + ) -> RelaychainStakingRecommendationMediating? { + guard + let directStakingRestrictionsBuilder = createDirectStakingRestrictionsBuilder(for: state), + let directStakingMediator = createDirectStakingMediator(for: state), + let poolMediator = createPoolStakingMediator(for: state) else { + return nil + } + + return HybridStakingRecommendationMediator( + chainAsset: state.chainAsset, + directStakingMediator: directStakingMediator, + nominationPoolsMediator: poolMediator, + directStakingRestrictionsBuilder: directStakingRestrictionsBuilder + ) + } + + func createDirectStakingRestrictionsBuilder( + for state: RelaychainStartStakingStateProtocol + ) -> RelaychainStakingRestrictionsBuilding? { + let networkInfoFactory = state.createNetworkInfoOperationFactory(for: operationQueue) + + let chainId = state.chainAsset.chain.chainId + + guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + return nil + } + + return DirectStakingRestrictionsBuilder( + chainAsset: state.chainAsset, + stakingLocalSubscriptionFactory: state.relaychainLocalSubscriptionFactory, + networkInfoFactory: networkInfoFactory, + eraValidatorService: state.eraValidatorService, + runtimeService: runtimeService, + operationQueue: operationQueue + ) + } + + func createPoolStakingRestrictionsBuilder( + for state: RelaychainStartStakingStateProtocol + ) -> RelaychainStakingRestrictionsBuilding? { + PoolStakingRestrictionsBuilder( + chainAsset: state.chainAsset, + npoolsLocalSubscriptionFactory: state.npLocalSubscriptionFactory + ) + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/StakingRecommendationValidationFactory.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/StakingRecommendationValidationFactory.swift new file mode 100644 index 0000000000..76cbb7f82a --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/StakingRecommendationValidationFactory.swift @@ -0,0 +1,138 @@ +import Foundation +import BigInt + +struct StakingRecommendationValidationParams { + let stakingAmount: Decimal? + let assetBalance: AssetBalance? + let assetLocks: AssetLocks? + let fee: BigUInt? + let existentialDeposit: BigUInt? + let stakeUpdateClosure: (Decimal) -> Void +} + +protocol StakingRecommendationValidationFactoryProtocol: AnyObject { + func createValidations( + for params: StakingRecommendationValidationParams, + controller: ControllerBackedProtocol?, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + presentable: StakingErrorPresentable, + locale: Locale + ) -> [DataValidating] +} + +final class HybridStakingValidationFactory { + let directRewardableStake: BigUInt + let chainAsset: ChainAsset + + init(directRewardableStake: BigUInt, chainAsset: ChainAsset) { + self.directRewardableStake = directRewardableStake + self.chainAsset = chainAsset + } + + private func notStakingLockedInPool( + params: StakingRecommendationValidationParams, + controller: ControllerBackedProtocol?, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + presentable: StakingErrorPresentable, + locale: Locale + ) -> DataValidating { + let precision = chainAsset.assetDisplayInfo.assetPrecision + let rewardableStake = directRewardableStake + + return ErrorConditionViolation(onError: { + guard let assetBalance = params.assetBalance else { + return + } + + let optMaxLock = params.assetLocks?.max { lock1, lock2 in + lock1.amount < lock2.amount + } + + let lockReason = optMaxLock?.lockType?.displayType.value(for: locale) ?? "" + + let fee = params.fee ?? 0 + + let availableToStake = assetBalance.transferable > fee ? assetBalance.transferable - fee : 0 + let availableToStakeDecimal = availableToStake.decimal(precision: UInt16(bitPattern: precision)) + + let availableToStakeString = balanceViewModelFactory.amountFromValue( + availableToStakeDecimal + ).value(for: locale) + + let rewardableStakeDecimal = rewardableStake.decimal(precision: UInt16(bitPattern: precision)) + let rewardableStakeString = balanceViewModelFactory.amountFromValue( + rewardableStakeDecimal + ).value(for: locale) + + presentable.presentLockedTokensInPoolStaking( + from: controller, + lockReason: lockReason, + availableToStake: availableToStakeString, + directRewardableToStake: rewardableStakeString, + locale: locale + ) + }, preservesCondition: { + guard + let assetBalance = params.assetBalance, + assetBalance.locked > 0, + let stakingAmountInPlank = params.stakingAmount?.toSubstrateAmount( + precision: precision + ) else { + return true + } + + let fee = params.fee ?? 0 + + return stakingAmountInPlank + fee <= assetBalance.transferable || + stakingAmountInPlank >= rewardableStake + }) + } +} + +extension HybridStakingValidationFactory: StakingRecommendationValidationFactoryProtocol { + func createValidations( + for params: StakingRecommendationValidationParams, + controller: ControllerBackedProtocol?, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + presentable: StakingErrorPresentable, + locale: Locale + ) -> [DataValidating] { + let validation = notStakingLockedInPool( + params: params, + controller: controller, + balanceViewModelFactory: balanceViewModelFactory, + presentable: presentable, + locale: locale + ) + + return [validation] + } +} + +final class CombinedStakingValidationFactory: StakingRecommendationValidationFactoryProtocol { + let factories: [StakingRecommendationValidationFactoryProtocol] + + init(factories: [StakingRecommendationValidationFactoryProtocol]) { + self.factories = factories + } + + func createValidations( + for params: StakingRecommendationValidationParams, + controller: ControllerBackedProtocol?, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + presentable: StakingErrorPresentable, + locale: Locale + ) -> [DataValidating] { + let validations = factories + .map { $0.createValidations( + for: params, + controller: controller, + balanceViewModelFactory: balanceViewModelFactory, + presentable: presentable, + locale: locale + ) } + .flatMap { $0 } + + return validations + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountInteractor.swift b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountInteractor.swift new file mode 100644 index 0000000000..c892e131b4 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountInteractor.swift @@ -0,0 +1,265 @@ +import UIKit +import RobinHood +import BigInt + +final class StakingSetupAmountInteractor: AnyProviderAutoCleaning, AnyCancellableCleaning { + weak var presenter: StakingSetupAmountInteractorOutputProtocol? + + var chainAsset: ChainAsset { state.chainAsset } + + let state: RelaychainStartStakingStateProtocol + let selectedAccount: ChainAccountResponse + let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol + let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol + let extrinsicService: ExtrinsicServiceProtocol + let extrinsicFeeProxy: ExtrinsicFeeProxyProtocol + let extrinsicSubmissionProxy: StartStakingExtrinsicProxyProtocol + let recommendationMediatorFactory: StakingRecommendationMediatorFactoryProtocol + let runtimeService: RuntimeCodingServiceProtocol + let operationQueue: OperationQueue + + private var priceProvider: StreamableProvider? + private var balanceProvider: StreamableProvider? + private var locksProvider: StreamableProvider? + private var recommendationMediator: RelaychainStakingRecommendationMediating? + + init( + state: RelaychainStartStakingStateProtocol, + selectedAccount: ChainAccountResponse, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + extrinsicService: ExtrinsicServiceProtocol, + extrinsicFeeProxy: ExtrinsicFeeProxyProtocol, + extrinsicSubmissionProxy: StartStakingExtrinsicProxyProtocol, + recommendationMediatorFactory: StakingRecommendationMediatorFactoryProtocol, + runtimeService: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue, + currencyManager: CurrencyManagerProtocol + ) { + self.state = state + self.selectedAccount = selectedAccount + self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory + self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory + self.extrinsicService = extrinsicService + self.extrinsicFeeProxy = extrinsicFeeProxy + self.extrinsicSubmissionProxy = extrinsicSubmissionProxy + self.recommendationMediatorFactory = recommendationMediatorFactory + self.runtimeService = runtimeService + self.operationQueue = operationQueue + self.currencyManager = currencyManager + } + + private func setupRecommendationMediator(for type: StakingType?) { + if type == nil { + if state.supportsPoolStaking() { + recommendationMediator = recommendationMediatorFactory.createHybridStakingMediator(for: state) + } else { + recommendationMediator = recommendationMediatorFactory.createDirectStakingMediator(for: state) + } + } else if type == .nominationPools { + recommendationMediator = recommendationMediatorFactory.createPoolStakingMediator(for: state) + } else { + recommendationMediator = recommendationMediatorFactory.createDirectStakingMediator(for: state) + } + + configureCurrentMediator() + } + + private func configureCurrentMediator() { + if recommendationMediator == nil { + presenter?.didReceive(error: .recommendation(CommonError.dataCorruption)) + return + } + + recommendationMediator?.delegate = self + recommendationMediator?.startRecommending() + recommendationMediator?.update(amount: 0) + } + + private func performPriceSubscription() { + clear(streamableProvider: &priceProvider) + + guard let priceId = chainAsset.asset.priceId else { + presenter?.didReceive(price: nil) + return + } + + priceProvider = subscribeToPrice(for: priceId, currency: selectedCurrency) + } + + private func performAssetBalanceSubscription() { + clear(streamableProvider: &balanceProvider) + + let chainAssetId = chainAsset.chainAssetId + + balanceProvider = subscribeToAssetBalanceProvider( + for: selectedAccount.accountId, + chainId: chainAssetId.chainId, + assetId: chainAssetId.assetId + ) + } + + private func performAssetLocksSubscription() { + clear(streamableProvider: &locksProvider) + + let chainAssetId = chainAsset.chainAssetId + + locksProvider = subscribeToLocksProvider( + for: selectedAccount.accountId, + chainId: chainAssetId.chainId, + assetId: chainAssetId.assetId + ) + } + + private func provideExistentialDeposit() { + fetchConstant( + for: .existentialDeposit, + runtimeCodingService: runtimeService, + operationManager: OperationManager(operationQueue: operationQueue) + ) { [weak self] (result: Result) in + switch result { + case let .success(existentialDeposit): + self?.presenter?.didReceive(existentialDeposit: existentialDeposit) + case let .failure(error): + self?.presenter?.didReceive(error: .existentialDeposit(error)) + } + } + } +} + +extension StakingSetupAmountInteractor: StakingSetupAmountInteractorInputProtocol, RuntimeConstantFetching { + func setup() { + extrinsicFeeProxy.delegate = self + + performAssetBalanceSubscription() + performPriceSubscription() + performAssetLocksSubscription() + provideExistentialDeposit() + + setupRecommendationMediator(for: state.stakingType) + } + + func remakeSubscriptions() { + performAssetBalanceSubscription() + performPriceSubscription() + performAssetLocksSubscription() + } + + func remakeRecommendationSetup() { + recommendationMediator?.stopRecommending() + + setupRecommendationMediator(for: state.stakingType) + } + + func retryExistentialDeposit() { + provideExistentialDeposit() + } + + func estimateFee(for staking: SelectedStakingOption, amount: BigUInt, feeId: TransactionFeeId) { + extrinsicSubmissionProxy.estimateFee( + using: extrinsicService, + feeProxy: extrinsicFeeProxy, + stakingOption: staking, + amount: amount, + feeId: feeId + ) + } + + func updateRecommendation(for amount: BigUInt) { + recommendationMediator?.update(amount: amount) + } +} + +extension StakingSetupAmountInteractor: ExtrinsicFeeProxyDelegate { + func didReceiveFee(result: Result, for identifier: TransactionFeeId) { + switch result { + case let .success(info): + presenter?.didReceive(fee: BigUInt(info.fee), feeId: identifier) + case let .failure(error): + presenter?.didReceive(error: .fee(error, identifier)) + } + } +} + +extension StakingSetupAmountInteractor: RelaychainStakingRecommendationDelegate { + func didReceive(recommendation: RelaychainStakingRecommendation, amount: BigUInt) { + presenter?.didReceive(recommendation: recommendation, amount: amount) + } + + func didReceiveRecommendation(error: Error) { + presenter?.didReceive(error: .recommendation(error)) + } +} + +extension StakingSetupAmountInteractor: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { + func handleAssetBalance( + result: Result, + accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) { + guard + chainId == chainAsset.chain.chainId, + assetId == chainAsset.asset.assetId, + accountId == selectedAccount.accountId else { + return + } + + switch result { + case let .success(balance): + let balance = balance ?? .createZero( + for: .init(chainId: chainId, assetId: assetId), + accountId: accountId + ) + presenter?.didReceive(assetBalance: balance) + case let .failure(error): + presenter?.didReceive(error: .assetBalance(error)) + } + } + + func handleAccountLocks( + result: Result<[DataProviderChange], Error>, + accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) { + guard + chainId == chainAsset.chain.chainId, + assetId == chainAsset.asset.assetId, + accountId == selectedAccount.accountId else { + return + } + + switch result { + case let .success(changes): + let locks = changes.mergeToDict([:]).values + presenter?.didReceive(locks: Array(locks)) + case let .failure(error): + presenter?.didReceive(error: .locks(error)) + } + } +} + +extension StakingSetupAmountInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { + func handlePrice(result: Result, priceId: AssetModel.PriceId) { + if chainAsset.asset.priceId == priceId { + switch result { + case let .success(priceData): + presenter?.didReceive(price: priceData) + case let .failure(error): + presenter?.didReceive(error: .price(error)) + } + } + } +} + +extension StakingSetupAmountInteractor: SelectedCurrencyDepending { + func applyCurrency() { + guard presenter != nil, + let priceId = chainAsset.asset.priceId else { + return + } + + priceProvider = subscribeToPrice(for: priceId, currency: selectedCurrency) + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountPresenter.swift b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountPresenter.swift new file mode 100644 index 0000000000..d8762ca5b7 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountPresenter.swift @@ -0,0 +1,509 @@ +import Foundation +import SoraFoundation +import BigInt + +final class StakingSetupAmountPresenter { + weak var view: StakingSetupAmountViewProtocol? + let wireframe: StakingSetupAmountWireframeProtocol + let interactor: StakingSetupAmountInteractorInputProtocol + let viewModelFactory: StakingAmountViewModelFactoryProtocol + let stakingTypeViewModelFactory: SelectedStakingViewModelFactoryProtocol + let chainAssetViewModelFactory: ChainAssetViewModelFactoryProtocol + let balanceViewModelFactory: BalanceViewModelFactoryProtocol + let dataValidatingFactory: RelaychainStakingValidatorFacadeProtocol + let balanceDerivationFactory: StakingTypeBalanceFactoryProtocol + let recommendsMultipleStakings: Bool + let chainAsset: ChainAsset + let logger: LoggerProtocol + + private var setupMethod: StakingSelectionMethod = .recommendation(nil) + + private var assetBalance: AssetBalance? + private var existentialDeposit: BigUInt? + private var buttonState: ButtonState = .startState + private var inputResult: AmountInputResult? { + didSet { + if inputResult != nil { + buttonState = .continueState(enabled: true) + } else { + buttonState = .continueState(enabled: false) + } + + provideButtonState() + } + } + + private var pendingRecommendationAmount: BigUInt? + private var priceData: PriceData? + private var fee: BigUInt? + private var pendingFeeId: TransactionFeeId? + private var assetLocks: AssetLocks? + + init( + interactor: StakingSetupAmountInteractorInputProtocol, + wireframe: StakingSetupAmountWireframeProtocol, + viewModelFactory: StakingAmountViewModelFactoryProtocol, + stakingTypeViewModelFactory: SelectedStakingViewModelFactoryProtocol, + chainAssetViewModelFactory: ChainAssetViewModelFactoryProtocol, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + balanceDerivationFactory: StakingTypeBalanceFactoryProtocol, + dataValidatingFactory: RelaychainStakingValidatorFacadeProtocol, + chainAsset: ChainAsset, + recommendsMultipleStakings: Bool, + localizationManager: LocalizationManagerProtocol, + logger: LoggerProtocol + ) { + self.interactor = interactor + self.wireframe = wireframe + self.viewModelFactory = viewModelFactory + self.stakingTypeViewModelFactory = stakingTypeViewModelFactory + self.chainAssetViewModelFactory = chainAssetViewModelFactory + self.balanceViewModelFactory = balanceViewModelFactory + self.balanceDerivationFactory = balanceDerivationFactory + self.dataValidatingFactory = dataValidatingFactory + self.chainAsset = chainAsset + self.recommendsMultipleStakings = recommendsMultipleStakings + self.logger = logger + self.localizationManager = localizationManager + } + + private func provideBalanceModel() { + let viewModel = viewModelFactory.balance( + amount: availableBalanceInPlank(), + chainAsset: chainAsset, + locale: selectedLocale + ) + + view?.didReceive(balance: viewModel) + } + + private func provideTitle() { + let title = R.string.localizable.stakingSetupAmountTitle( + chainAsset.assetDisplayInfo.symbol, + preferredLanguages: selectedLocale.rLanguages + ) + view?.didReceive(title: title) + } + + private func provideButtonState() { + view?.didReceiveButtonState( + title: buttonState.title.value(for: selectedLocale), + enabled: buttonState.enabled + ) + } + + private func provideChainAssetViewModel() { + guard let asset = chainAsset.chain.utilityAsset() else { + return + } + + let chain = chainAsset.chain + let chainAsset = ChainAsset(chain: chain, asset: asset) + let viewModel = chainAssetViewModelFactory.createViewModel(from: chainAsset) + view?.didReceiveInputChainAsset(viewModel: viewModel) + } + + private func provideAmountPriceViewModel() { + if chainAsset.chain.utilityAsset()?.priceId != nil { + let priceData = priceData ?? PriceData.zero() + + let price = balanceViewModelFactory.priceFromAmount( + inputAmount(), + priceData: priceData + ).value(for: selectedLocale) + + view?.didReceiveAmountInputPrice(viewModel: price) + } else { + view?.didReceiveAmountInputPrice(viewModel: nil) + } + } + + private func provideAmountInputViewModel() { + let amount = inputResult != nil ? inputAmount() : nil + let viewModel = balanceViewModelFactory.createBalanceInputViewModel( + amount + ).value(for: selectedLocale) + + view?.didReceiveAmount(inputViewModel: viewModel) + } + + private func stakeableBalanceMinusFee() -> Decimal { + let feeValue = fee ?? 0 + guard + let precision = chainAsset.chain.utilityAsset()?.displayInfo.assetPrecision, + let balance = stakeableBalance(), + let feeDecimal = Decimal.fromSubstrateAmount(feeValue, precision: precision) else { + return 0 + } + + return balance >= feeDecimal ? balance - feeDecimal : 0 + } + + private func inputAmount() -> Decimal { + inputResult?.absoluteValue(from: stakeableBalanceMinusFee()) ?? 0 + } + + private func inputAmountInPlank() -> BigUInt { + inputAmount().toSubstrateAmount(precision: chainAsset.assetDisplayInfo.assetPrecision) ?? 0 + } + + private func availableBalance() -> Decimal? { + availableBalanceInPlank().flatMap { $0.decimal(precision: chainAsset.asset.precision) } + } + + private func stakeableBalance() -> Decimal? { + stakeableBalanceInPlank().flatMap { $0.decimal(precision: chainAsset.asset.precision) } + } + + private func availableBalanceInPlank() -> BigUInt? { + balanceDerivationFactory.getAvailableBalance( + from: assetBalance, + stakingMethod: setupMethod + ) + } + + private func stakeableBalanceInPlank() -> BigUInt? { + balanceDerivationFactory.getStakeableBalance( + from: assetBalance, + existentialDeposit: existentialDeposit, + stakingMethod: setupMethod + ) + } + + private func manualAvailableBalanceInPlank(for stakingOption: SelectedStakingOption) -> BigUInt? { + switch stakingOption { + case .direct: + return assetBalance?.freeInPlank + case .pool: + return assetBalance?.transferable + } + } + + private func updateAfterAmountChanged() { + refreshFee() + provideAmountPriceViewModel() + updateRecommendationIfNeeded() + } + + private func refreshFee() { + guard let stakingOption = setupMethod.selectedStakingOption else { + return + } + + let amount = inputAmountInPlank() + + let feeId = StartStakingFeeIdFactory.generateFeeId(for: stakingOption, amount: amount) + + fee = nil + pendingFeeId = feeId + + interactor.estimateFee(for: stakingOption, amount: amount, feeId: feeId) + } + + private func provideStakingTypeViewModel() { + switch setupMethod { + case let .recommendation(stakingRecommendation): + if inputResult == nil, recommendsMultipleStakings { + view?.didReceive(stakingType: nil) + } else if let stakingType = stakingRecommendation?.staking { + provideRecommendedStakingTypeViewModel(for: stakingType) + } else { + view?.didReceive(stakingType: .loading) + } + case let .manual(stakingManual): + provideManualStakingTypeViewModel(for: stakingManual) + } + } + + private func provideManualStakingTypeViewModel(for model: RelaychainStakingManual) { + let innerViewModel: StakingTypeViewModel.TypeModel + + switch model.staking { + case let .direct(validators): + let validatorViewModel = stakingTypeViewModelFactory.createValidator( + for: validators, + displaysRecommended: model.usedRecommendation, + locale: selectedLocale + ) + + innerViewModel = .direct(validatorViewModel) + case let .pool(selectedPool): + let poolViewModel = stakingTypeViewModelFactory.createPool( + for: selectedPool, + chainAsset: chainAsset, + displaysRecommended: model.usedRecommendation, + locale: selectedLocale + ) + + innerViewModel = .pools(poolViewModel) + } + + let maxApy = viewModelFactory.maxApy(for: model.staking, locale: selectedLocale) + + let stakingType = StakingTypeViewModel( + type: innerViewModel, + maxApy: maxApy, + shouldEnableSelection: true + ) + + view?.didReceive(stakingType: .loaded(value: stakingType)) + } + + private func provideRecommendedStakingTypeViewModel(for model: SelectedStakingOption) { + let viewModel = stakingTypeViewModelFactory.createRecommended( + for: model, + locale: selectedLocale + ) + + let maxApy = viewModelFactory.maxApy(for: model, locale: selectedLocale) + + let stakingType = StakingTypeViewModel( + type: .recommended(viewModel), + maxApy: maxApy, + shouldEnableSelection: true + ) + + view?.didReceive(stakingType: .loaded(value: stakingType)) + } + + private func updateRecommendationIfNeeded() { + let amount = inputAmountInPlank() + + guard amount != pendingRecommendationAmount, setupMethod.isRecommendation else { + return + } + + pendingRecommendationAmount = amount + setupMethod = .recommendation(nil) + + provideStakingTypeViewModel() + + interactor.updateRecommendation(for: amount) + } +} + +extension StakingSetupAmountPresenter: StakingSetupAmountPresenterProtocol { + func setup() { + interactor.setup() + + provideTitle() + provideBalanceModel() + provideChainAssetViewModel() + provideAmountPriceViewModel() + provideAmountInputViewModel() + provideButtonState() + provideStakingTypeViewModel() + refreshFee() + + if !recommendsMultipleStakings { + updateRecommendationIfNeeded() + } + } + + func updateAmount(_ newValue: Decimal?) { + inputResult = newValue.map { .absolute($0) } + updateAfterAmountChanged() + } + + func selectAmountPercentage(_ percentage: Float) { + inputResult = .rate(Decimal(Double(percentage))) + + provideAmountInputViewModel() + updateAfterAmountChanged() + } + + func selectStakingType() { + if chainAsset.asset.hasMultipleStakingOptions { + wireframe.showStakingTypeSelection( + from: view, + method: setupMethod, + amount: inputAmountInPlank(), + delegate: self + ) + } else if case let .direct(validators) = setupMethod.selectedStakingOption { + let delegateFacade = StakingSetupTypeEntityFacade( + selectedMethod: setupMethod, + delegate: self + ) + + wireframe.showSelectValidators( + from: view, + selectedValidators: validators, + delegate: delegateFacade + ) + } + } + + func proceed() { + var currentInputAmount = inputAmount() + + let defaultValidations: [DataValidating] = dataValidatingFactory.createValidations( + from: setupMethod, + params: .init( + chainAsset: chainAsset, + stakingAmount: currentInputAmount, + availableBalance: availableBalanceInPlank(), + assetBalance: assetBalance, + fee: fee, + existentialDeposit: existentialDeposit, + feeRefreshClosure: { [weak self] in + self?.refreshFee() + }, stakeUpdateClosure: { newAmount in + currentInputAmount = newAmount + } + ), + locale: selectedLocale + ) + + let recommendedValidations = setupMethod.recommendation?.validationFactory?.createValidations( + for: .init( + stakingAmount: currentInputAmount, + assetBalance: assetBalance, + assetLocks: assetLocks, + fee: fee, + existentialDeposit: existentialDeposit, + stakeUpdateClosure: { newStake in + currentInputAmount = newStake + } + ), + controller: view, + balanceViewModelFactory: balanceViewModelFactory, + presentable: wireframe, + locale: selectedLocale + ) ?? [] + + let validators = defaultValidations + recommendedValidations + DataValidationRunner(validators: validators).runValidation { [weak self] in + guard let stakingOption = self?.setupMethod.selectedStakingOption else { + return + } + + self?.wireframe.showConfirmation( + from: self?.view, + stakingOption: stakingOption, + amount: currentInputAmount + ) + } + } +} + +extension StakingSetupAmountPresenter: StakingSetupAmountInteractorOutputProtocol { + func didReceive(price: PriceData?) { + priceData = price + provideAmountPriceViewModel() + } + + func didReceive(assetBalance: AssetBalance) { + self.assetBalance = assetBalance + provideBalanceModel() + + if case .rate = inputResult { + // fee and recommendation might change because staking amount depends on balance + provideAmountInputViewModel() + updateRecommendationIfNeeded() + refreshFee() + } + } + + func didReceive(existentialDeposit: BigUInt) { + self.existentialDeposit = existentialDeposit + } + + func didReceive(fee: BigUInt?, feeId: TransactionFeeId) { + logger.debug("Did receive fee: \(String(describing: fee))") + + guard pendingFeeId == feeId else { + return + } + + self.fee = fee + pendingFeeId = nil + + if case .rate = inputResult { + provideAmountInputViewModel() + updateRecommendationIfNeeded() + } + } + + func didReceive(recommendation: RelaychainStakingRecommendation, amount: BigUInt) { + logger.debug("Did receive recommendation for amount: \(amount)") + + // check that we are waiting recommendation for particular amount + guard pendingRecommendationAmount == amount, setupMethod.isRecommendation else { + return + } + + setupMethod = .recommendation(recommendation) + + // display balance respects staking type + provideBalanceModel() + + provideStakingTypeViewModel() + refreshFee() + } + + func didReceive(locks: AssetLocks) { + assetLocks = locks + } + + func didReceive(error: StakingSetupAmountError) { + logger.error("Did receive error: \(error)") + + switch error { + case .assetBalance, .price, .locks: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.remakeSubscriptions() + } + case let .fee(_, feeId): + guard feeId == pendingFeeId else { + return + } + + pendingFeeId = nil + + wireframe.presentFeeStatus(on: view, locale: selectedLocale) { [weak self] in + self?.refreshFee() + } + case .recommendation: + guard setupMethod.isRecommendation else { + return + } + + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.remakeRecommendationSetup() + } + case .existentialDeposit: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryExistentialDeposit() + } + } + } +} + +extension StakingSetupAmountPresenter: Localizable { + func applyLocalization() { + guard view?.isSetup == true else { + return + } + + provideBalanceModel() + provideButtonState() + provideTitle() + provideAmountInputViewModel() + provideAmountPriceViewModel() + provideStakingTypeViewModel() + } +} + +extension StakingSetupAmountPresenter: StakingTypeDelegate { + func changeStakingType(method: StakingSelectionMethod) { + pendingRecommendationAmount = nil + + setupMethod = method + + provideBalanceModel() + provideStakingTypeViewModel() + updateRecommendationIfNeeded() + refreshFee() + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountProtocols.swift b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountProtocols.swift new file mode 100644 index 0000000000..d962ca3a1c --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountProtocols.swift @@ -0,0 +1,71 @@ +import Foundation +import BigInt + +protocol StakingSetupAmountViewProtocol: ControllerBackedProtocol { + func didReceive(balance: TitleHorizontalMultiValueView.Model) + func didReceive(title: String) + func didReceiveButtonState(title: String, enabled: Bool) + func didReceiveInputChainAsset(viewModel: ChainAssetViewModel) + func didReceiveAmount(inputViewModel: AmountInputViewModelProtocol) + func didReceiveAmountInputPrice(viewModel: String?) + func didReceive(stakingType: LoadableViewModelState?) +} + +protocol StakingSetupAmountPresenterProtocol: AnyObject { + func setup() + func proceed() + func updateAmount(_ newValue: Decimal?) + func selectAmountPercentage(_ percentage: Float) + func selectStakingType() +} + +protocol StakingSetupAmountInteractorInputProtocol: AnyObject { + func setup() + func remakeSubscriptions() + func remakeRecommendationSetup() + func retryExistentialDeposit() + + func estimateFee(for staking: SelectedStakingOption, amount: BigUInt, feeId: TransactionFeeId) + func updateRecommendation(for amount: BigUInt) +} + +protocol StakingSetupAmountInteractorOutputProtocol: AnyObject { + func didReceive(price: PriceData?) + func didReceive(assetBalance: AssetBalance) + func didReceive(fee: BigUInt?, feeId: TransactionFeeId) + func didReceive(recommendation: RelaychainStakingRecommendation, amount: BigUInt) + func didReceive(existentialDeposit: BigUInt) + func didReceive(locks: AssetLocks) + func didReceive(error: StakingSetupAmountError) +} + +protocol StakingSetupAmountWireframeProtocol: AlertPresentable, ErrorPresentable, FeeRetryable, + CommonRetryable, RelaychainStakingErrorPresentable { + func showStakingTypeSelection( + from view: ControllerBackedProtocol?, + method: StakingSelectionMethod, + amount: BigUInt, + delegate: StakingTypeDelegate? + ) + + func showConfirmation( + from view: ControllerBackedProtocol?, + stakingOption: SelectedStakingOption, + amount: Decimal + ) + + func showSelectValidators( + from view: ControllerBackedProtocol?, + selectedValidators: PreparedValidators, + delegate: StakingSetupTypeEntityFacade + ) +} + +enum StakingSetupAmountError: Error { + case assetBalance(Error) + case price(Error) + case fee(Error, TransactionFeeId) + case recommendation(Error) + case locks(Error) + case existentialDeposit(Error) +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewController.swift b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewController.swift new file mode 100644 index 0000000000..f0f1081d86 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewController.swift @@ -0,0 +1,157 @@ +import UIKit +import SoraFoundation + +final class StakingSetupAmountViewController: UIViewController, ViewHolder, ImportantViewProtocol { + typealias RootViewType = StakingSetupAmountViewLayout + + let presenter: StakingSetupAmountPresenterProtocol + + let keyboardAppearanceStrategy: KeyboardAppearanceStrategyProtocol + + init( + presenter: StakingSetupAmountPresenterProtocol, + keyboardAppearanceStrategy: KeyboardAppearanceStrategyProtocol, + localizationManager: LocalizationManagerProtocol + ) { + self.presenter = presenter + self.keyboardAppearanceStrategy = keyboardAppearanceStrategy + + super.init(nibName: nil, bundle: nil) + + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = StakingSetupAmountViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupLocalization() + setupHandlers() + + presenter.setup() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + keyboardAppearanceStrategy.onViewWillAppear(for: rootView.amountInputView.textField) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + keyboardAppearanceStrategy.onViewDidAppear(for: rootView.amountInputView.textField) + } + + private func setupLocalization() { + setupAmountInputAccessoryView(for: selectedLocale) + + rootView.estimatedRewardsView.titleView.text = R.string.localizable.stakingEstimatedEarnings( + preferredLanguages: selectedLocale.rLanguages + ) + + rootView.estimatedRewardsView.detailsValueLabel.text = R.string.localizable.commonPerYear( + preferredLanguages: selectedLocale.rLanguages + ) + } + + private func setupHandlers() { + rootView.amountInputView.addTarget( + self, + action: #selector(actionAmountChange), + for: .editingChanged + ) + + rootView.actionButton.addTarget( + self, + action: #selector(actionContinue), + for: .touchUpInside + ) + } + + @objc private func actionAmountChange() { + let amount = rootView.amountInputView.inputViewModel?.decimalAmount + presenter.updateAmount(amount) + } + + @objc private func actionContinue() { + presenter.proceed() + } + + @objc private func selectStakingTypeAction() { + presenter.selectStakingType() + } + + private func setupAmountInputAccessoryView(for locale: Locale) { + let accessoryView = UIFactory.default.createAmountAccessoryView( + for: self, + locale: locale + ) + + rootView.amountInputView.textField.inputAccessoryView = accessoryView + } +} + +extension StakingSetupAmountViewController: StakingSetupAmountViewProtocol { + func didReceive(balance: TitleHorizontalMultiValueView.Model) { + rootView.amountView.bind(model: balance) + } + + func didReceive(title: String) { + self.title = title + } + + func didReceiveButtonState(title: String, enabled: Bool) { + rootView.actionButton.applyState(title: title, enabled: enabled) + } + + func didReceiveInputChainAsset(viewModel: ChainAssetViewModel) { + rootView.amountInputView.bind(assetViewModel: viewModel.assetViewModel) + } + + func didReceiveAmount(inputViewModel: AmountInputViewModelProtocol) { + rootView.amountInputView.bind(inputViewModel: inputViewModel) + } + + func didReceiveAmountInputPrice(viewModel: String?) { + rootView.amountInputView.bind(priceViewModel: viewModel) + } + + func didReceive(stakingType: LoadableViewModelState?) { + rootView.setStakingType(viewModel: stakingType) + + rootView.stakingTypeView.addTarget( + self, + action: #selector(selectStakingTypeAction), + for: .touchUpInside + ) + } +} + +extension StakingSetupAmountViewController: AmountInputAccessoryViewDelegate { + func didSelect(on _: AmountInputAccessoryView, percentage: Float) { + rootView.amountInputView.textField.resignFirstResponder() + + presenter.selectAmountPercentage(percentage) + } + + func didSelectDone(on _: AmountInputAccessoryView) { + rootView.amountInputView.textField.resignFirstResponder() + } +} + +extension StakingSetupAmountViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewFactory.swift b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewFactory.swift new file mode 100644 index 0000000000..58de80808f --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewFactory.swift @@ -0,0 +1,118 @@ +import Foundation +import SoraFoundation +import RobinHood + +struct StakingSetupAmountViewFactory { + static func createView( + for state: RelaychainStartStakingStateProtocol + ) -> StakingSetupAmountViewProtocol? { + guard + let currencyManager = CurrencyManager.shared, + let interactor = createInteractor(for: state) else { + return nil + } + + let wireframe = StakingSetupAmountWireframe(state: state) + + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: state.chainAsset.assetDisplayInfo, + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager) + ) + + let viewModelFactory = StakingAmountViewModelFactory( + balanceViewModelFactory: balanceViewModelFactory, + estimatedEarningsFormatter: NumberFormatter.percentBase.localizableResource() + ) + let chainAssetViewModelFactory = ChainAssetViewModelFactory(networkViewModelFactory: NetworkViewModelFactory()) + + let dataValidatingFactory = RelaychainStakingValidatorFacade( + presentable: wireframe, + balanceFactory: balanceViewModelFactory + ) + + let balanceDerivationFactory = StakingTypeBalanceFactory(stakingType: state.stakingType) + + let presenter = StakingSetupAmountPresenter( + interactor: interactor, + wireframe: wireframe, + viewModelFactory: viewModelFactory, + stakingTypeViewModelFactory: SelectedStakingViewModelFactory(), + chainAssetViewModelFactory: chainAssetViewModelFactory, + balanceViewModelFactory: balanceViewModelFactory, + balanceDerivationFactory: balanceDerivationFactory, + dataValidatingFactory: dataValidatingFactory, + chainAsset: state.chainAsset, + recommendsMultipleStakings: state.recommendsMultipleStakings, + localizationManager: LocalizationManager.shared, + logger: Logger.shared + ) + + let view = StakingSetupAmountViewController( + presenter: presenter, + keyboardAppearanceStrategy: EventDrivenKeyboardStrategy( + events: [.viewDidAppear], + triggersOnes: true + ), + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + interactor.presenter = presenter + dataValidatingFactory.view = view + + return view + } + + private static func createInteractor( + for state: RelaychainStartStakingStateProtocol + ) -> StakingSetupAmountInteractor? { + let request = state.chainAsset.chain.accountRequest() + let chainId = state.chainAsset.chain.chainId + + guard let selectedAccount = SelectedWalletSettings.shared.value?.fetch(for: request) else { + return nil + } + + let chainRegistry = ChainRegistryFacade.sharedRegistry + + guard + let runtimeProvider = chainRegistry.getRuntimeProvider(for: chainId), + let connection = chainRegistry.getConnection(for: chainId), + let currencyManager = CurrencyManager.shared else { + return nil + } + + let extrinsicService = ExtrinsicServiceFactory( + runtimeRegistry: runtimeProvider, + engine: connection, + operationManager: OperationManagerFacade.sharedManager + ).createService(account: selectedAccount, chain: state.chainAsset.chain) + + let recommendationFactory = StakingRecommendationMediatorFactory( + chainRegistry: chainRegistry, + operationQueue: OperationManagerFacade.sharedDefaultQueue + ) + + let feeProxy = ExtrinsicFeeProxy() + + let extrinsicProxy = StartStakingExtrinsicProxy( + selectedAccount: selectedAccount, + runtimeService: runtimeProvider, + operationQueue: OperationManagerFacade.sharedDefaultQueue + ) + + return .init( + state: state, + selectedAccount: selectedAccount, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + priceLocalSubscriptionFactory: PriceProviderFactory.shared, + extrinsicService: extrinsicService, + extrinsicFeeProxy: feeProxy, + extrinsicSubmissionProxy: extrinsicProxy, + recommendationMediatorFactory: recommendationFactory, + runtimeService: runtimeProvider, + operationQueue: OperationManagerFacade.sharedDefaultQueue, + currencyManager: currencyManager + ) + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewLayout.swift b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewLayout.swift new file mode 100644 index 0000000000..68ff29f56b --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewLayout.swift @@ -0,0 +1,222 @@ +import UIKit +import SoraUI + +final class StakingSetupAmountViewLayout: ScrollableContainerLayoutView { + let amountView: TitleHorizontalMultiValueView = .create { + $0.titleView.apply(style: .footnoteSecondary) + $0.detailsTitleLabel.apply(style: .footnoteSecondary) + $0.detailsValueLabel.apply(style: .footnotePrimary) + } + + let amountInputView = NewAmountInputView() + let estimatedRewardsView: LoadableTitleHorizontalMultiValueView = .create { view in + view.titleView.apply(style: .footnoteSecondary) + view.detailsTitleLabel.apply(style: .semiboldFootnotePositive) + view.detailsValueLabel.apply(style: .caption1Secondary) + } + + var stakingTypeView: BackgroundedContentControl = StakingTypeAccountView(frame: .zero) + + let actionButton: TriangularedButton = .create { + $0.applyDefaultStyle() + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = R.color.colorSecondaryScreenBackground() + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func setupLayout() { + super.setupLayout() + + stackView.layoutMargins = Constants.contentInsets + + addSubview(actionButton) + actionButton.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) + make.bottom.equalTo(safeAreaLayoutGuide).inset(UIConstants.actionBottomInset) + make.height.equalTo(UIConstants.actionHeight) + } + + addArrangedSubview(amountView, spacingAfter: 8) + amountView.snp.makeConstraints { + $0.height.equalTo(Constants.amountHeight) + } + + addArrangedSubview(amountInputView, spacingAfter: Constants.verticalSpacing) + amountInputView.snp.makeConstraints { + $0.height.equalTo(Constants.amountInputHeight) + } + + addArrangedSubview(stakingTypeView, spacingAfter: Constants.verticalSpacing) + + addArrangedSubview(estimatedRewardsView) + + estimatedRewardsView.snp.makeConstraints { + $0.height.equalTo(Constants.estimatedRewardsHeight) + } + + stakingTypeView.isHidden = true + estimatedRewardsView.isHidden = true + } + + func setStakingType(viewModel: LoadableViewModelState?) { + if let viewModel = viewModel { + stakingTypeView.isHidden = false + estimatedRewardsView.isHidden = false + + estimatedRewardsView.stopLoadingIfNeeded() + + switch viewModel { + case let .cached(value), let .loaded(value): + setSelectedStaking( + viewModel: viewModel.map(with: { $0.type }), + canProceed: value.shouldEnableSelection + ) + + estimatedRewardsView.detailsTitleLabel.text = value.maxApy + case .loading: + setSelectedStaking(viewModel: .loading, canProceed: false) + estimatedRewardsView.startLoadingIfNeeded() + } + + } else { + stakingTypeView.isHidden = true + estimatedRewardsView.isHidden = true + } + } + + private func setSelectedStaking( + viewModel: LoadableViewModelState, + canProceed: Bool + ) { + switch viewModel { + case let .cached(value), let .loaded(value): + if let accountTypeViewModel = mapAccountViewModel(from: value) { + setStakingAccountType( + viewModel: viewModel.map { _ in accountTypeViewModel }, + canProceed: canProceed + ) + } else if let validatorViewModel = mapValidatorViewModel(from: value) { + setStakingValidatorType( + viewModel: viewModel.map(with: { _ in validatorViewModel }), + canProceed: canProceed + ) + } + case .loading: + setStakingAccountType(viewModel: .loading, canProceed: canProceed) + } + } + + private func mapAccountViewModel(from viewModel: StakingTypeViewModel.TypeModel) -> StakingTypeAccountViewModel? { + switch viewModel { + case let .recommended(viewModel): + return StakingTypeAccountViewModel( + imageViewModel: nil, + title: viewModel.title, + subtitle: viewModel.subtitle, + isRecommended: true + ) + case let .pools(viewModel): + return StakingTypeAccountViewModel( + imageViewModel: viewModel.icon, + title: viewModel.title, + subtitle: viewModel.subtitle, + isRecommended: viewModel.isRecommended + ) + case .direct: + return nil + } + } + + private func mapValidatorViewModel( + from viewModel: StakingTypeViewModel.TypeModel + ) -> DirectStakingTypeAccountViewModel? { + switch viewModel { + case .recommended, .pools: + return nil + case let .direct(validatorViewModel): + return DirectStakingTypeAccountViewModel( + count: validatorViewModel.count, + title: validatorViewModel.title, + subtitle: validatorViewModel.subtitle, + isRecommended: validatorViewModel.isRecommended + ) + } + } + + private func setStakingAccountType( + viewModel: LoadableViewModelState, + canProceed: Bool + ) { + let typeView: StakingTypeAccountView + + if let currentTypeView = stakingTypeView as? StakingTypeAccountView { + typeView = currentTypeView + } else { + stakingTypeView.removeFromSuperview() + + typeView = StakingTypeAccountView(frame: .zero) + insertArrangedSubview(typeView, after: amountInputView, spacingAfter: Constants.verticalSpacing) + + stakingTypeView = typeView + } + + typeView.canProceed = canProceed + + typeView.stopLoadingIfNeeded() + + switch viewModel { + case let .cached(value), let .loaded(value): + typeView.bind(viewModel: value) + case .loading: + typeView.startLoadingIfNeeded() + } + } + + private func setStakingValidatorType( + viewModel: LoadableViewModelState, + canProceed: Bool + ) { + let typeView: StakingTypeValidatorView + + if let currentTypeView = stakingTypeView as? StakingTypeValidatorView { + typeView = currentTypeView + } else { + stakingTypeView.removeFromSuperview() + + typeView = StakingTypeValidatorView(frame: .zero) + insertArrangedSubview(typeView, after: amountInputView, spacingAfter: Constants.verticalSpacing) + + stakingTypeView = typeView + } + + typeView.canProceed = canProceed + + typeView.stopLoadingIfNeeded() + + switch viewModel { + case let .cached(value), let .loaded(value): + typeView.bind(viewModel: value) + case .loading: + typeView.startLoadingIfNeeded() + } + } +} + +extension StakingSetupAmountViewLayout { + enum Constants { + static let contentInsets = UIEdgeInsets(top: 12, left: 16, bottom: 0, right: 16) + static let amountHeight: CGFloat = 18 + static let amountInputHeight: CGFloat = 64 + static let estimatedRewardsHeight: CGFloat = 44 + static let verticalSpacing: CGFloat = 16 + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountWireframe.swift b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountWireframe.swift new file mode 100644 index 0000000000..b543cbfe88 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountWireframe.swift @@ -0,0 +1,90 @@ +import Foundation +import BigInt + +final class StakingSetupAmountWireframe: StakingSetupAmountWireframeProtocol { + let state: RelaychainStartStakingStateProtocol + + init(state: RelaychainStartStakingStateProtocol) { + self.state = state + } + + func showStakingTypeSelection( + from view: ControllerBackedProtocol?, + method: StakingSelectionMethod, + amount: BigUInt, + delegate: StakingTypeDelegate? + ) { + guard let stakingTypeView = StakingTypeViewFactory.createView( + state: state, + method: method, + amount: amount, + delegate: delegate + ) else { + return + } + + view?.controller.navigationController?.pushViewController( + stakingTypeView.controller, + animated: true + ) + } + + func showConfirmation( + from view: ControllerBackedProtocol?, + stakingOption: SelectedStakingOption, + amount: Decimal + ) { + guard + let confirmationView = StartStakingConfirmViewFactory.createView( + for: stakingOption, + amount: amount, + state: state + ) else { + return + } + + view?.controller.navigationController?.pushViewController( + confirmationView.controller, + animated: true + ) + } + + func showSelectValidators( + from view: ControllerBackedProtocol?, + selectedValidators: PreparedValidators, + delegate: StakingSetupTypeEntityFacade + ) { + let fullValidatorList = CustomValidatorsFullList( + allValidators: selectedValidators.electedAndPrefValidators.electedToSelectedValidators(), + preferredValidators: selectedValidators.electedAndPrefValidators.preferredValidators + ) + + let selectionValidatorGroups = SelectionValidatorGroups( + fullValidatorList: fullValidatorList, + recommendedValidatorList: selectedValidators.recommendedValidators + ) + + let hasIdentity = fullValidatorList.allValidators.contains { $0.hasIdentity } + let validatorsSelectionParams = ValidatorsSelectionParams( + maxNominations: selectedValidators.maxTargets, + hasIdentity: hasIdentity + ) + + guard let validatorsView = CustomValidatorListViewFactory.createValidatorListView( + for: state, + selectionValidatorGroups: selectionValidatorGroups, + selectedValidatorList: SharedList(items: selectedValidators.targets), + validatorsSelectionParams: validatorsSelectionParams, + delegate: delegate + ) else { + return + } + + delegate.bindToFlow(controller: validatorsView.controller) + + view?.controller.navigationController?.pushViewController( + validatorsView.controller, + animated: true + ) + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/StakingTypeAccountView.swift b/novawallet/Modules/Staking/StakingSetupAmount/StakingTypeAccountView.swift new file mode 100644 index 0000000000..4b69825bd6 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/StakingTypeAccountView.swift @@ -0,0 +1,139 @@ +import UIKit +import SoraUI + +final class StakingTypeAccountView: RowView< + GenericTitleValueView, UIImageView> +> { + var iconImageView: UIImageView { rowContentView.titleView.imageView } + var titleLabel: UILabel { rowContentView.titleView.detailsView.valueTop } + var subtitleLabel: UILabel { rowContentView.titleView.detailsView.valueBottom } + var disclosureImageView: UIImageView { rowContentView.valueView } + + private var imageViewModel: ImageViewModelProtocol? + + var skeletonView: SkrullableView? + + private var isLoading: Bool = false + + var canProceed: Bool = true { + didSet { + updateActivityState() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + configure() + updateActivityState() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + if isLoading { + updateLoadingState() + skeletonView?.restartSkrulling() + } + } + + private func configure() { + roundedBackgroundView.apply(style: .roundedLightCell) + preferredHeight = 52 + contentInsets = .init(top: 9, left: 16, bottom: 9, right: 12) + borderView.borderType = .none + + titleLabel.textAlignment = .left + subtitleLabel.textAlignment = .left + titleLabel.apply(style: .footnotePrimary) + subtitleLabel.apply(style: .init( + textColor: R.color.colorTextPositive(), + font: .caption1 + )) + } + + private func updateActivityState() { + if canProceed { + disclosureImageView.image = R.image.iconSmallArrow()?.tinted(with: R.color.colorTextSecondary()!) + } else { + disclosureImageView.image = nil + } + + isUserInteractionEnabled = canProceed + } + + func bind(viewModel: StakingTypeAccountViewModel) { + imageViewModel?.cancel(on: iconImageView) + imageViewModel = viewModel.imageViewModel + iconImageView.image = nil + + let imageSize = rowContentView.titleView.iconWidth + viewModel.imageViewModel?.loadImage( + on: iconImageView, + targetSize: CGSize(width: imageSize, height: imageSize), + animated: true + ) + titleLabel.text = viewModel.title + subtitleLabel.text = viewModel.subtitle + + if viewModel.isRecommended { + subtitleLabel.apply(style: .init( + textColor: R.color.colorTextPositive(), + font: .caption1 + )) + } else { + subtitleLabel.apply(style: .caption1Secondary) + } + + iconImageView.isHidden = viewModel.imageViewModel == nil + } +} + +extension StakingTypeAccountView: SkeletonableView { + func createSkeletons(for spaceSize: CGSize) -> [Skeletonable] { + let titleSize = CGSize(width: 80, height: 10) + let titleOffset = CGPoint(x: contentInsets.left, y: contentInsets.top + 4) + + let titleRow = SingleSkeleton.createRow( + on: self, + containerView: self, + spaceSize: spaceSize, + offset: titleOffset, + size: titleSize + ) + + let detailsSize = CGSize(width: 101, height: 8) + let detailsOffset = CGPoint(x: contentInsets.left, y: titleOffset.y + titleSize.height + 8) + + let detailsRow = SingleSkeleton.createRow( + on: self, + containerView: self, + spaceSize: spaceSize, + offset: detailsOffset, + size: detailsSize + ) + + return [titleRow, detailsRow] + } + + var skeletonSuperview: UIView { + self + } + + var hidingViews: [UIView] { + [rowContentView] + } + + func didStartSkeleton() { + isLoading = true + } + + func didStopSkeleton() { + isLoading = false + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/View/GenericStakingTypeAccountView.swift b/novawallet/Modules/Staking/StakingSetupAmount/View/GenericStakingTypeAccountView.swift new file mode 100644 index 0000000000..0dd530fd18 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/View/GenericStakingTypeAccountView.swift @@ -0,0 +1,145 @@ +import UIKit +import SoraUI + +class GenericStakingTypeAccountView: RowView< + GenericTitleValueView, UIImageView> +> where T: UIView { + var titleLabel: UILabel { rowContentView.titleView.sView.valueTop } + var subtitleLabel: UILabel { rowContentView.titleView.sView.valueBottom } + var disclosureImageView: UIImageView { rowContentView.valueView } + + var genericViewSkeletonSize: CGSize = .zero + + var skeletonView: SkrullableView? + var isLoading: Bool = false + + var canProceed: Bool = true { + didSet {} + } + + override init(frame: CGRect) { + super.init(frame: frame) + + configure() + updateActivityState() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + if isLoading { + updateLoadingState() + skeletonView?.restartSkrulling() + } + } + + func configure() { + roundedBackgroundView.apply(style: .roundedLightCell) + preferredHeight = 52 + contentInsets = .init(top: 9, left: 16, bottom: 9, right: 14) + borderView.borderType = .none + + titleLabel.textAlignment = .left + subtitleLabel.textAlignment = .left + titleLabel.apply(style: .footnotePrimary) + subtitleLabel.apply(style: .init( + textColor: R.color.colorTextPositive(), + font: .caption1 + )) + + rowContentView.titleView.makeHorizontal() + rowContentView.titleView.stackView.alignment = .center + rowContentView.titleView.spacing = 12 + rowContentView.titleView.sView.spacing = 2 + } + + private func updateActivityState() { + if canProceed { + disclosureImageView.image = R.image.iconSmallArrow()?.tinted(with: R.color.colorTextSecondary()!) + } else { + disclosureImageView.image = nil + } + + isUserInteractionEnabled = canProceed + } +} + +extension GenericStakingTypeAccountView: SkeletonableView { + func createSkeletons(for spaceSize: CGSize) -> [Skeletonable] { + var skeletons: [Skeletonable] = [] + + var leadingOffset: CGFloat = contentInsets.left + + if genericViewSkeletonSize != .zero { + let offset = CGPoint( + x: contentInsets.left, + y: spaceSize.height / 2.0 - genericViewSkeletonSize.height / 2.0 + ) + + let cornerRadius = 6 / genericViewSkeletonSize.height + let cornerRadii = CGSize(width: cornerRadius, height: cornerRadius) + + let genericViewSkeleton = SingleSkeleton.createRow( + on: self, + containerView: self, + spaceSize: spaceSize, + offset: offset, + size: genericViewSkeletonSize, + cornerRadii: cornerRadii + ) + + skeletons.append(genericViewSkeleton) + + leadingOffset = offset.x + genericViewSkeletonSize.width + rowContentView.titleView.spacing + } + + let titleSize = CGSize(width: 80, height: 10) + let titleOffset = CGPoint(x: leadingOffset, y: contentInsets.top + 4) + + let titleRow = SingleSkeleton.createRow( + on: self, + containerView: self, + spaceSize: spaceSize, + offset: titleOffset, + size: titleSize + ) + + skeletons.append(titleRow) + + let detailsSize = CGSize(width: 101, height: 8) + let detailsOffset = CGPoint(x: leadingOffset, y: titleOffset.y + titleSize.height + 8) + + let detailsRow = SingleSkeleton.createRow( + on: self, + containerView: self, + spaceSize: spaceSize, + offset: detailsOffset, + size: detailsSize + ) + + skeletons.append(detailsRow) + + return skeletons + } + + var skeletonSuperview: UIView { + self + } + + var hidingViews: [UIView] { + [rowContentView] + } + + func didStartSkeleton() { + isLoading = true + } + + func didStopSkeleton() { + isLoading = false + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/View/StakingSetupAmountStyles.swift b/novawallet/Modules/Staking/StakingSetupAmount/View/StakingSetupAmountStyles.swift new file mode 100644 index 0000000000..c7768a03b3 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/View/StakingSetupAmountStyles.swift @@ -0,0 +1,15 @@ +extension BorderedLabelView.Style { + static let counter = BorderedLabelView.Style( + text: .init( + textColor: R.color.colorTextTertiary()!, + font: .semiBoldCaps2 + ), + background: .init( + shadowOpacity: 0, + strokeWidth: 0, + fillColor: R.color.colorBlockBackground()!, + highlightedFillColor: R.color.colorBlockBackground()!, + rounding: .init(radius: 6, corners: .allCorners) + ) + ) +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/View/StakingTypeAccountView.swift b/novawallet/Modules/Staking/StakingSetupAmount/View/StakingTypeAccountView.swift new file mode 100644 index 0000000000..150435c46a --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/View/StakingTypeAccountView.swift @@ -0,0 +1,43 @@ +import UIKit +import SoraUI + +final class StakingTypeAccountView: GenericStakingTypeAccountView, BindableView { + var iconImageView: UIImageView { rowContentView.titleView.fView } + private var imageViewModel: ImageViewModelProtocol? + let imageWidth: CGFloat = 24 + + override func configure() { + super.configure() + + iconImageView.snp.makeConstraints { make in + make.width.equalTo(imageWidth) + } + + iconImageView.contentMode = .scaleAspectFit + } + + func bind(viewModel: StakingTypeAccountViewModel) { + imageViewModel?.cancel(on: iconImageView) + imageViewModel = viewModel.imageViewModel + iconImageView.image = nil + + viewModel.imageViewModel?.loadImage( + on: iconImageView, + targetSize: CGSize(width: imageWidth, height: imageWidth), + animated: true + ) + titleLabel.text = viewModel.title + subtitleLabel.text = viewModel.subtitle + + if viewModel.isRecommended { + subtitleLabel.apply(style: .init( + textColor: R.color.colorTextPositive(), + font: .caption1 + )) + } else { + subtitleLabel.apply(style: .caption1Secondary) + } + + iconImageView.isHidden = viewModel.imageViewModel == nil + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/View/StakingTypeValidatorView.swift b/novawallet/Modules/Staking/StakingSetupAmount/View/StakingTypeValidatorView.swift new file mode 100644 index 0000000000..0ca33e6245 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/View/StakingTypeValidatorView.swift @@ -0,0 +1,39 @@ +import UIKit +import SnapKit + +final class StakingTypeValidatorView: GenericStakingTypeAccountView, BindableView { + var counterLabel: BorderedLabelView { rowContentView.titleView.fView } + + override func configure() { + super.configure() + + counterLabel.contentInsets = .init(top: 6, left: 6, bottom: 5, right: 6) + counterLabel.apply(style: .counter) + counterLabel.titleLabel.textAlignment = .center + counterLabel.snp.makeConstraints { + $0.width.greaterThanOrEqualTo(counterLabel.snp.height) + } + genericViewSkeletonSize = CGSize(width: 24, height: 24) + } + + func bind(viewModel: DirectStakingTypeAccountViewModel) { + if let count = viewModel.count { + counterLabel.isHidden = false + counterLabel.titleLabel.text = count + } else { + counterLabel.isHidden = true + } + + titleLabel.text = viewModel.title + subtitleLabel.text = viewModel.subtitle + + if viewModel.isRecommended { + subtitleLabel.apply(style: .init( + textColor: R.color.colorTextPositive(), + font: .caption1 + )) + } else { + subtitleLabel.apply(style: .caption1Secondary) + } + } +} diff --git a/novawallet/Modules/Staking/StakingType/Model/DirectStakingTypeViewModel.swift b/novawallet/Modules/Staking/StakingType/Model/DirectStakingTypeViewModel.swift new file mode 100644 index 0000000000..8b5507036c --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/Model/DirectStakingTypeViewModel.swift @@ -0,0 +1,12 @@ +struct DirectStakingTypeViewModel { + let title: String + let subtile: String + let validator: ValidatorModel? + + struct ValidatorModel { + let title: String + let subtitle: String + let isRecommended: Bool + let count: String? + } +} diff --git a/novawallet/Modules/Staking/StakingType/Model/PoolAccountViewModel.swift b/novawallet/Modules/Staking/StakingType/Model/PoolAccountViewModel.swift new file mode 100644 index 0000000000..3e6383d1d8 --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/Model/PoolAccountViewModel.swift @@ -0,0 +1,5 @@ +struct PoolAccountViewModel { + let name: String + let icon: ImageViewModelProtocol? + let recommended: Bool +} diff --git a/novawallet/Modules/Staking/StakingType/Model/PoolStakingTypeViewModel.swift b/novawallet/Modules/Staking/StakingType/Model/PoolStakingTypeViewModel.swift new file mode 100644 index 0000000000..228637f25d --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/Model/PoolStakingTypeViewModel.swift @@ -0,0 +1,12 @@ +struct PoolStakingTypeViewModel { + let title: String + let subtile: String + let poolAccount: PoolAccountModel? + + struct PoolAccountModel { + let icon: ImageViewModelProtocol? + let title: String + let subtitle: String? + let isRecommended: Bool + } +} diff --git a/novawallet/Modules/Staking/StakingType/Model/StakingSelectionMethod.swift b/novawallet/Modules/Staking/StakingType/Model/StakingSelectionMethod.swift new file mode 100644 index 0000000000..b49a650b95 --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/Model/StakingSelectionMethod.swift @@ -0,0 +1,51 @@ +import Foundation + +enum StakingSelectionMethod { + case recommendation(RelaychainStakingRecommendation?) + case manual(RelaychainStakingManual) + + var isRecommendation: Bool { + switch self { + case .recommendation: + return true + case .manual: + return false + } + } + + var shouldUseRecommendationStyle: Bool { + switch self { + case .recommendation: + return true + case let .manual(manual): + return manual.usedRecommendation + } + } + + var selectedStakingOption: SelectedStakingOption? { + switch self { + case let .recommendation(recommendation): + return recommendation?.staking + case let .manual(manual): + return manual.staking + } + } + + var restrictions: RelaychainStakingRestrictions? { + switch self { + case let .recommendation(recommendation): + return recommendation?.restrictions + case let .manual(manual): + return manual.restrictions + } + } + + var recommendation: RelaychainStakingRecommendation? { + switch self { + case let .recommendation(recommendation): + return recommendation + case .manual: + return nil + } + } +} diff --git a/novawallet/Modules/Staking/StakingType/Model/StakingTypeSelectedStakingViewModelFactory.swift b/novawallet/Modules/Staking/StakingType/Model/StakingTypeSelectedStakingViewModelFactory.swift new file mode 100644 index 0000000000..b0244ae8cf --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/Model/StakingTypeSelectedStakingViewModelFactory.swift @@ -0,0 +1,83 @@ +import Foundation +import BigInt + +final class SelectedStakingTypeViewModelFactory { + private lazy var countFormatter = NumberFormatter.quantity.localizableResource() + private lazy var poolIconFactory = NominationPoolsIconFactory() +} + +extension SelectedStakingTypeViewModelFactory: SelectedStakingViewModelFactoryProtocol { + func createRecommended( + for stakingType: SelectedStakingOption, + locale: Locale + ) -> RecommendedStakingTypeViewModel { + switch stakingType { + case .direct: + return RecommendedStakingTypeViewModel( + title: R.string.localizable.stakingDirectStaking(preferredLanguages: locale.rLanguages), + subtitle: R.string.localizable.commonRecommended(preferredLanguages: locale.rLanguages) + ) + case .pool: + return RecommendedStakingTypeViewModel( + title: R.string.localizable.stakingPoolStaking(preferredLanguages: locale.rLanguages), + subtitle: R.string.localizable.commonRecommended(preferredLanguages: locale.rLanguages) + ) + } + } + + func createValidator( + for validators: PreparedValidators, + displaysRecommended: Bool, + locale: Locale + ) -> DirectStakingTypeViewModel.ValidatorModel { + let strings = R.string.localizable.self + + if displaysRecommended { + return .init( + title: strings.stakingTypeValidatorsTitle(preferredLanguages: locale.rLanguages), + subtitle: strings.stakingTypeRecommendedValidatorsSubtitle(preferredLanguages: locale.rLanguages), + isRecommended: true, + count: countFormatter.value(for: locale).string(from: NSNumber(value: validators.targets.count)) ?? "" + ) + } else { + let validatorsString = strings.stakingSetupAmountDirectTypeSubtitle( + validators.targets.count, + validators.maxTargets, + preferredLanguages: locale.rLanguages + ) + + return .init( + title: strings.stakingTypeValidatorsTitle(preferredLanguages: locale.rLanguages), + subtitle: validatorsString, + isRecommended: false, + count: countFormatter.value(for: locale).string(from: NSNumber(value: validators.targets.count)) ?? "" + ) + } + } + + func createPool( + for pool: NominationPools.SelectedPool, + chainAsset: ChainAsset, + displaysRecommended: Bool, + locale: Locale + ) -> PoolStakingTypeViewModel.PoolAccountModel { + let iconViewModel = poolIconFactory.createIconViewModel( + for: chainAsset, + poolId: pool.poolId, + bondedAccountId: pool.bondedAccountId + ) + + let title = pool.title(for: chainAsset.chain.chainFormat) ?? "" + + let subtitle = displaysRecommended ? R.string.localizable.commonRecommended( + preferredLanguages: locale.rLanguages + ) : nil + + return PoolStakingTypeViewModel.PoolAccountModel( + icon: iconViewModel, + title: title, + subtitle: subtitle, + isRecommended: displaysRecommended + ) + } +} diff --git a/novawallet/Modules/Staking/StakingType/Model/StakingTypeSelection.swift b/novawallet/Modules/Staking/StakingType/Model/StakingTypeSelection.swift new file mode 100644 index 0000000000..9d5806acf4 --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/Model/StakingTypeSelection.swift @@ -0,0 +1,4 @@ +enum StakingTypeSelection { + case direct + case nominationPool +} diff --git a/novawallet/Modules/Staking/StakingType/Model/StakingTypeViewModelFactory.swift b/novawallet/Modules/Staking/StakingType/Model/StakingTypeViewModelFactory.swift new file mode 100644 index 0000000000..59e0bf9d2c --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/Model/StakingTypeViewModelFactory.swift @@ -0,0 +1,137 @@ +import BigInt +import SoraFoundation +import SubstrateSdk + +protocol StakingTypeViewModelFactoryProtocol { + func directStakingViewModel( + minStake: BigUInt?, + chainAsset: ChainAsset, + method: StakingSelectionMethod?, + locale: Locale + ) -> DirectStakingTypeViewModel + + func nominationPoolViewModel( + minStake: BigUInt?, + chainAsset: ChainAsset, + method: StakingSelectionMethod?, + locale: Locale + ) -> PoolStakingTypeViewModel + + func minStake( + minStake: BigUInt?, + chainAsset: ChainAsset, + locale: Locale + ) -> String +} + +final class StakingTypeViewModelFactory: StakingTypeViewModelFactoryProtocol { + let balanceViewModelFactory: BalanceViewModelFactoryProtocol + let stakingViewModelFactory: SelectedStakingViewModelFactoryProtocol + + init( + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + stakingViewModelFactory: SelectedStakingViewModelFactoryProtocol + ) { + self.balanceViewModelFactory = balanceViewModelFactory + self.stakingViewModelFactory = stakingViewModelFactory + } + + func directStakingViewModel( + minStake: BigUInt?, + chainAsset: ChainAsset, + method: StakingSelectionMethod?, + locale: Locale + ) -> DirectStakingTypeViewModel { + let strings = R.string.localizable.self + let title = strings.stakingTypeDirect(preferredLanguages: locale.rLanguages) + let amountDecimal = minStake.map { + Decimal.fromSubstrateAmount($0, precision: chainAsset.assetDisplayInfo.assetPrecision) + } ?? nil + let amount = amountDecimal.map { balanceViewModelFactory.amountFromValue($0).value(for: locale) } ?? nil + let minStakeString = amount.map { + strings.stakingTypeMinimumStake($0, preferredLanguages: locale.rLanguages) + } ?? nil + + let rewardsString = strings.stakingTypeAutoRewards(preferredLanguages: locale.rLanguages) + let govString = chainAsset.chain.hasGovernance ? + strings.stakingTypeGovReuseTokens(preferredLanguages: locale.rLanguages) : nil + let managmentString = strings.stakingTypeStakingManagment(preferredLanguages: locale.rLanguages) + + let subtitle = [ + minStakeString, + rewardsString, + govString, + managmentString + ] + .compactMap { $0 } + .joined(separator: .returnKey) + + guard + let method = method, + case let .direct(validators) = method.selectedStakingOption else { + return .init(title: title, subtile: subtitle, validator: nil) + } + + let validatorViewModel = stakingViewModelFactory.createValidator( + for: validators, + displaysRecommended: method.shouldUseRecommendationStyle, + locale: locale + ) + + return .init(title: title, subtile: subtitle, validator: validatorViewModel) + } + + func nominationPoolViewModel( + minStake: BigUInt?, + chainAsset: ChainAsset, + method: StakingSelectionMethod?, + locale: Locale + ) -> PoolStakingTypeViewModel { + let strings = R.string.localizable.self + let title = strings.stakingTypeNominationPool(preferredLanguages: locale.rLanguages) + let amountDecimal = minStake.map { + Decimal.fromSubstrateAmount($0, precision: chainAsset.assetDisplayInfo.assetPrecision) + } ?? nil + let amount = amountDecimal.map { balanceViewModelFactory.amountFromValue($0).value(for: locale) } ?? nil + let minStakeString = amount.map { + strings.stakingTypeMinimumStake($0, preferredLanguages: locale.rLanguages) + } ?? nil + let rewardsString = strings.stakingTypeManualRewards(preferredLanguages: locale.rLanguages) + + let subtitle = [ + minStakeString, + rewardsString + ] + .compactMap { $0 } + .joined(separator: .returnKey) + + guard + let method = method, + case let .pool(selectedPool) = method.selectedStakingOption else { + return .init(title: title, subtile: subtitle, poolAccount: nil) + } + + let poolViewModel = stakingViewModelFactory.createPool( + for: selectedPool, + chainAsset: chainAsset, + displaysRecommended: method.shouldUseRecommendationStyle, + locale: locale + ) + + return .init(title: title, subtile: subtitle, poolAccount: poolViewModel) + } + + func minStake( + minStake: BigUInt?, + chainAsset: ChainAsset, + locale: Locale + ) -> String { + guard let minStake = minStake else { + return "" + } + let strings = R.string.localizable.self + let amountDecimal = Decimal.fromSubstrateAmount(minStake, precision: chainAsset.assetDisplayInfo.assetPrecision) + let amount = amountDecimal.map { balanceViewModelFactory.amountFromValue($0).value(for: locale) } ?? "" + return strings.stakingTypeMinimumStake(amount, preferredLanguages: locale.rLanguages) + } +} diff --git a/novawallet/Modules/Staking/StakingType/Model/ValidatorViewModel.swift b/novawallet/Modules/Staking/StakingType/Model/ValidatorViewModel.swift new file mode 100644 index 0000000000..c23796afc0 --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/Model/ValidatorViewModel.swift @@ -0,0 +1,4 @@ +enum ValidatorAccountViewModel { + case recommended(maxCount: Int) + case selected(count: Int, maxCount: Int) +} diff --git a/novawallet/Modules/Staking/StakingType/StakingSelectValidatorsDelegate.swift b/novawallet/Modules/Staking/StakingType/StakingSelectValidatorsDelegate.swift new file mode 100644 index 0000000000..a20f79f742 --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/StakingSelectValidatorsDelegate.swift @@ -0,0 +1,3 @@ +protocol StakingSelectValidatorsDelegateProtocol: AnyObject { + func changeValidatorsSelection(validatorList: [SelectedValidatorInfo], maxTargets: Int) +} diff --git a/novawallet/Modules/Staking/StakingType/StakingTypeBannerView.swift b/novawallet/Modules/Staking/StakingType/StakingTypeBannerView.swift new file mode 100644 index 0000000000..a66760022a --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/StakingTypeBannerView.swift @@ -0,0 +1,73 @@ +import SoraUI + +final class StakingTypeBannerView: StakingTypeBaseBannerView { + let radioSelectorView = RadioSelectorView() + let titleLabel = UILabel(style: .boldTitle2Primary, numberOfLines: 1) + let detailsLabel = UILabel(style: .regularSubhedlineSecondary) + let accountView: ActionView = .create { + $0.isHidden = true + } + + let stackView: UIStackView = .create { + $0.axis = .vertical + $0.layoutMargins = UIEdgeInsets(top: 16, left: 12, bottom: 16, right: 12) + $0.isLayoutMarginsRelativeArrangement = true + $0.spacing = 16 + } + + var contentInsets: UIEdgeInsets { + get { + stackView.layoutMargins + } + set { + stackView.layoutMargins = newValue + setNeedsLayout() + } + } + + func setEnabledStyle(_ isEnabled: Bool) { + if isEnabled { + stackView.alpha = 1.0 + } else { + stackView.alpha = 0.5 + } + } + + override func setupLayout() { + super.setupLayout() + + addSubview(stackView) + stackView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + let descriptionStack = UIView.vStack(alignment: .fill, distribution: .fill, spacing: 16, [ + UIView.hStack(spacing: 12, [ + radioSelectorView, + titleLabel + ]), + UIView.hStack([ + FlexibleSpaceView(), + detailsLabel + ]) + ]) + + radioSelectorView.snp.makeConstraints { + $0.width.height.equalTo(24) + } + + detailsLabel.snp.makeConstraints { + $0.leading.equalTo(titleLabel.snp.leading) + } + + descriptionStack.layoutMargins = .init(top: 0, left: 4, bottom: 0, right: 4) + descriptionStack.isLayoutMarginsRelativeArrangement = true + stackView.addArrangedSubview(descriptionStack) + stackView.setCustomSpacing(20, after: descriptionStack) + stackView.addArrangedSubview(accountView) + } + + func setAction(viewModel: ActionView.TModel) { + accountView.bind(viewModel: viewModel) + } +} diff --git a/novawallet/Modules/Staking/StakingType/StakingTypeBaseBannerView.swift b/novawallet/Modules/Staking/StakingType/StakingTypeBaseBannerView.swift new file mode 100644 index 0000000000..b728724483 --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/StakingTypeBaseBannerView.swift @@ -0,0 +1,85 @@ +import SoraUI + +class StakingTypeBaseBannerView: UIView { + let backgroundView: RoundedView = .create { view in + view.applyFilledBackgroundStyle() + + view.cornerRadius = 12 + view.roundingCorners = .allCorners + view.fillColor = .black + view.highlightedFillColor = .black + + view.layer.cornerRadius = 12 + view.clipsToBounds = true + } + + let imageView = UIImageView() + + let borderView: RoundedView = .create { view in + view.applyStrokedBackgroundStyle() + view.cornerRadius = 12 + view.roundingCorners = .allCorners + + view.strokeWidth = Constants.borderWidth + view.strokeColor = R.color.colorStakingTypeCardBorder()! + view.highlightedStrokeColor = R.color.colorActiveBorder()! + } + + var imageOffsets = Constants.imageOffsets { + didSet { + updateImageConstraints() + } + } + + var imageSize = Constants.imageSize { + didSet { + updateImageConstraints() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setupLayout() { + addSubview(backgroundView) + backgroundView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + + backgroundView.addSubview(imageView) + imageView.snp.makeConstraints { + $0.trailing.equalToSuperview().offset(imageOffsets.right) + $0.top.equalToSuperview().offset(imageOffsets.top) + $0.size.equalTo(imageSize) + } + + addSubview(borderView) + borderView.snp.makeConstraints { + $0.edges.equalToSuperview() + } + } + + private func updateImageConstraints() { + imageView.snp.updateConstraints { + $0.trailing.equalToSuperview().offset(imageOffsets.right) + $0.top.equalToSuperview().offset(imageOffsets.top) + $0.size.equalTo(imageSize) + } + } +} + +extension StakingTypeBaseBannerView { + private enum Constants { + static let imageOffsets: (top: CGFloat, right: CGFloat) = (top: -18, right: 26) + static let imageSize: CGSize = .init(width: 125, height: 111) + static let borderWidth: CGFloat = 1 + } +} diff --git a/novawallet/Modules/Staking/StakingType/StakingTypeInteractor.swift b/novawallet/Modules/Staking/StakingType/StakingTypeInteractor.swift new file mode 100644 index 0000000000..01ec567490 --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/StakingTypeInteractor.swift @@ -0,0 +1,118 @@ +import UIKit +import RobinHood +import BigInt + +final class StakingTypeInteractor: AnyProviderAutoCleaning, AnyCancellableCleaning { + weak var presenter: StakingTypeInteractorOutputProtocol? + let directStakingRestrictionsBuilder: RelaychainStakingRestrictionsBuilding + let nominationPoolsRestrictionsBuilder: RelaychainStakingRestrictionsBuilding + let directStakingRecommendationMediator: RelaychainStakingRecommendationMediating + let nominationPoolRecommendationMediator: RelaychainStakingRecommendationMediating + let selectedAccount: ChainAccountResponse + let chainAsset: ChainAsset + let stakingSelectionMethod: StakingSelectionMethod + let amount: BigUInt + + init( + selectedAccount: ChainAccountResponse, + chainAsset: ChainAsset, + amount: BigUInt, + stakingSelectionMethod: StakingSelectionMethod, + directStakingRestrictionsBuilder: RelaychainStakingRestrictionsBuilding, + nominationPoolsRestrictionsBuilder: RelaychainStakingRestrictionsBuilding, + directStakingRecommendationMediator: RelaychainStakingRecommendationMediating, + nominationPoolRecommendationMediator: RelaychainStakingRecommendationMediating + ) { + self.selectedAccount = selectedAccount + self.chainAsset = chainAsset + self.amount = amount + self.stakingSelectionMethod = stakingSelectionMethod + self.directStakingRestrictionsBuilder = directStakingRestrictionsBuilder + self.nominationPoolsRestrictionsBuilder = nominationPoolsRestrictionsBuilder + self.directStakingRecommendationMediator = directStakingRecommendationMediator + self.nominationPoolRecommendationMediator = nominationPoolRecommendationMediator + } + + deinit { + directStakingRestrictionsBuilder.stop() + nominationPoolsRestrictionsBuilder.stop() + directStakingRecommendationMediator.stopRecommending() + nominationPoolRecommendationMediator.stopRecommending() + } + + private func provideDirectStakingRecommendation() { + nominationPoolRecommendationMediator.delegate = nil + + directStakingRecommendationMediator.delegate = self + directStakingRecommendationMediator.startRecommending() + directStakingRecommendationMediator.update(amount: amount) + } + + private func provideNominationPoolStakingRecommendation() { + directStakingRecommendationMediator.delegate = nil + + nominationPoolRecommendationMediator.delegate = self + nominationPoolRecommendationMediator.startRecommending() + nominationPoolRecommendationMediator.update(amount: amount) + } +} + +extension StakingTypeInteractor: StakingTypeInteractorInputProtocol { + func setup() { + [ + directStakingRestrictionsBuilder, + nominationPoolsRestrictionsBuilder + ].forEach { + $0.delegate = self + $0.start() + } + } + + func change(stakingTypeSelection: StakingTypeSelection) { + switch stakingTypeSelection { + case .direct: + provideDirectStakingRecommendation() + case .nominationPool: + provideNominationPoolStakingRecommendation() + } + } +} + +extension StakingTypeInteractor: RelaychainStakingRestrictionsBuilderDelegate { + func restrictionsBuilder( + _ builder: RelaychainStakingRestrictionsBuilding, + didPrepare restrictions: RelaychainStakingRestrictions + ) { + if builder === directStakingRestrictionsBuilder { + presenter?.didReceive(directStakingRestrictions: restrictions) + } else if builder === nominationPoolsRestrictionsBuilder { + presenter?.didReceive(nominationPoolRestrictions: restrictions) + } + } + + func restrictionsBuilder( + _: RelaychainStakingRestrictionsBuilding, + didReceive error: Error + ) { + presenter?.didReceive(error: .restrictions(error)) + } +} + +extension StakingTypeInteractor: RelaychainStakingRecommendationDelegate { + func didReceive( + recommendation: RelaychainStakingRecommendation, + amount _: BigUInt + ) { + let model = RelaychainStakingManual( + staking: recommendation.staking, + restrictions: recommendation.restrictions, + usedRecommendation: true + ) + + presenter?.didReceive(method: .manual(model)) + } + + func didReceiveRecommendation(error: Error) { + presenter?.didReceive(error: .recommendation(error)) + } +} diff --git a/novawallet/Modules/Staking/StakingType/StakingTypePresenter.swift b/novawallet/Modules/Staking/StakingType/StakingTypePresenter.swift new file mode 100644 index 0000000000..1747622fdc --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/StakingTypePresenter.swift @@ -0,0 +1,344 @@ +import Foundation +import SoraFoundation +import BigInt + +final class StakingTypePresenter { + weak var view: StakingTypeViewProtocol? + weak var delegate: StakingTypeDelegate? + + let wireframe: StakingTypeWireframeProtocol + let interactor: StakingTypeInteractorInputProtocol + let viewModelFactory: StakingTypeViewModelFactoryProtocol + let chainAsset: ChainAsset + let canChangeType: Bool + let amount: BigUInt + + private var nominationPoolRestrictions: RelaychainStakingRestrictions? + private var directStakingRestrictions: RelaychainStakingRestrictions? + private var directStakingAvailable: Bool = false + private var method: StakingSelectionMethod? + private var selection: StakingTypeSelection + private var hasChanges: Bool = false + + init( + interactor: StakingTypeInteractorInputProtocol, + wireframe: StakingTypeWireframeProtocol, + chainAsset: ChainAsset, + amount: BigUInt, + canChangeType: Bool, + initialMethod: StakingSelectionMethod, + viewModelFactory: StakingTypeViewModelFactoryProtocol, + localizationManager: LocalizationManagerProtocol, + delegate: StakingTypeDelegate? + ) { + self.chainAsset = chainAsset + self.interactor = interactor + self.wireframe = wireframe + self.viewModelFactory = viewModelFactory + self.delegate = delegate + self.amount = amount + self.canChangeType = canChangeType + method = initialMethod + + switch initialMethod.selectedStakingOption { + case .direct: + selection = .direct + case .pool, .none: + selection = .nominationPool + } + + self.localizationManager = localizationManager + } + + private func updateDirectStakingAvailable() { + guard let restrictions = directStakingRestrictions else { + return + } + + if let minRewardableStake = restrictions.minRewardableStake { + directStakingAvailable = amount >= minRewardableStake + } else if let minJoinStake = restrictions.minJoinStake { + directStakingAvailable = amount >= minJoinStake + } else { + directStakingAvailable = true + } + } + + private func provideDirectStakingViewModel() { + guard let restrictions = directStakingRestrictions else { + return + } + let viewModel = viewModelFactory.directStakingViewModel( + minStake: restrictions.minRewardableStake ?? restrictions.minJoinStake, + chainAsset: chainAsset, + method: method, + locale: selectedLocale + ) + + let available = selection == .direct || canChangeType && directStakingAvailable + + view?.didReceiveDirectStakingBanner(viewModel: viewModel, available: available) + } + + private func provideNominationPoolViewModel() { + guard let restrictions = nominationPoolRestrictions else { + return + } + + let viewModel = viewModelFactory.nominationPoolViewModel( + minStake: restrictions.minRewardableStake ?? restrictions.minJoinStake, + chainAsset: chainAsset, + method: method, + locale: selectedLocale + ) + + let available = selection == .nominationPool || canChangeType + + view?.didReceivePoolBanner(viewModel: viewModel, available: available) + } + + private func updateView() { + provideDirectStakingViewModel() + provideNominationPoolViewModel() + provideStakingSelection() + + if hasChanges, method != nil { + view?.didReceiveSaveChangesState(available: true) + } else { + view?.didReceiveSaveChangesState(available: false) + } + } + + private func provideStakingSelection() { + view?.didReceive(stakingTypeSelection: selection) + } + + private func showDirectStakingNotAvailableAlert(minStake: String) { + let languages = selectedLocale.rLanguages + let cancelActionTitle = R.string.localizable.commonBack(preferredLanguages: languages) + let cancelAction = AlertPresentableAction(title: cancelActionTitle, style: .cancel) { [weak self] in + self?.wireframe.complete(from: self?.view) + } + + let viewModel = AlertPresentableViewModel( + title: R.string.localizable.stakingTypeDirectStakingAlertTitle(preferredLanguages: languages), + message: R.string.localizable.stakingTypeDirectStakingAlertMessage( + minStake, + preferredLanguages: languages + ), + actions: [cancelAction], + closeAction: nil + ) + + wireframe.present(viewModel: viewModel, style: .alert, from: view) + } + + private func showSaveChangesAlert() { + let languages = selectedLocale.rLanguages + let closeActionTitle = R.string.localizable.commonClose(preferredLanguages: languages) + let cancelActionTitle = R.string.localizable.commonCancel(preferredLanguages: languages) + let closeAction = AlertPresentableAction(title: closeActionTitle, style: .destructive) { [weak self] in + self?.wireframe.complete(from: self?.view) + } + + let viewModel = AlertPresentableViewModel( + title: nil, + message: R.string.localizable.commonCloseWhenChangesConfirmation(preferredLanguages: languages), + actions: [closeAction], + closeAction: cancelActionTitle + ) + + wireframe.present(viewModel: viewModel, style: .actionSheet, from: view) + } + + private func presentAlreadyStakingAlert(for type: StakingTypeSelection) { + let backAction = AlertPresentableAction( + title: R.string.localizable.commonBack(preferredLanguages: selectedLocale.rLanguages), + style: .normal + ) { [weak self] in + self?.wireframe.complete(from: self?.view) + } + + let message: String + + switch type { + case .direct: + message = R.string.localizable.stakingStartAlreadyStakingDirect( + preferredLanguages: selectedLocale.rLanguages + ) + case .nominationPool: + message = R.string.localizable.stakingStartAlreadyStakingPool( + preferredLanguages: selectedLocale.rLanguages + ) + } + + let viewModel = AlertPresentableViewModel( + title: R.string.localizable.stakingStartAlreadyStakingTitle(preferredLanguages: selectedLocale.rLanguages), + message: message, + actions: [backAction], + closeAction: nil + ) + + wireframe.present(viewModel: viewModel, style: .alert, from: view) + } +} + +extension StakingTypePresenter: StakingTypePresenterProtocol { + func setup() { + interactor.setup() + updateView() + } + + func selectValidators() { + guard let method = method, case let .direct(validators) = method.selectedStakingOption else { + return + } + + let fullValidatorList = CustomValidatorsFullList( + allValidators: validators.electedAndPrefValidators.electedToSelectedValidators(), + preferredValidators: validators.electedAndPrefValidators.preferredValidators + ) + + let recommendedValidatorList = validators.recommendedValidators + + let groups = SelectionValidatorGroups( + fullValidatorList: fullValidatorList, + recommendedValidatorList: recommendedValidatorList + ) + + let hasIdentity = fullValidatorList.allValidators.contains { $0.hasIdentity } + let selectionParams = ValidatorsSelectionParams( + maxNominations: validators.maxTargets, + hasIdentity: hasIdentity + ) + + let delegateFacade = StakingSetupTypeEntityFacade( + selectedMethod: method, + delegate: delegate + ) + + wireframe.showValidators( + from: view, + selectionValidatorGroups: groups, + selectedValidatorList: SharedList(items: validators.targets), + validatorsSelectionParams: selectionParams, + delegate: delegateFacade + ) + } + + func selectNominationPool() { + guard let method = method, case let .pool(selectedPool) = method.selectedStakingOption else { + return + } + + let delegateFacade = StakingSetupTypeEntityFacade( + selectedMethod: method, + delegate: delegate + ) + + wireframe.showNominationPoolsList( + from: view, + amount: amount, + delegate: delegateFacade, + selectedPool: selectedPool + ) + } + + func change(stakingTypeSelection: StakingTypeSelection) { + guard canChangeType else { + presentAlreadyStakingAlert(for: stakingTypeSelection) + return + } + + guard let restrictions = directStakingRestrictions else { + return + } + + switch stakingTypeSelection { + case .direct: + if directStakingAvailable { + selection = .direct + method = nil + + provideStakingSelection() + provideNominationPoolViewModel() + } else { + let minStake = viewModelFactory.minStake( + minStake: restrictions.minRewardableStake ?? restrictions.minJoinStake, + chainAsset: chainAsset, + locale: selectedLocale + ) + showDirectStakingNotAvailableAlert(minStake: minStake) + return + } + case .nominationPool: + selection = .nominationPool + method = nil + + provideStakingSelection() + provideDirectStakingViewModel() + } + + interactor.change(stakingTypeSelection: selection) + } + + func save() { + guard let method = method else { + return + } + delegate?.changeStakingType(method: method) + wireframe.complete(from: view) + } + + func back() { + if hasChanges { + showSaveChangesAlert() + } else { + wireframe.complete(from: view) + } + } +} + +extension StakingTypePresenter: StakingTypeInteractorOutputProtocol { + func didReceive(nominationPoolRestrictions: RelaychainStakingRestrictions) { + self.nominationPoolRestrictions = nominationPoolRestrictions + provideNominationPoolViewModel() + } + + func didReceive(directStakingRestrictions: RelaychainStakingRestrictions) { + self.directStakingRestrictions = directStakingRestrictions + updateDirectStakingAvailable() + provideDirectStakingViewModel() + } + + func didReceive(method: StakingSelectionMethod) { + self.method = method + hasChanges = true + updateView() + } + + func didReceive(error: StakingTypeError) { + switch error { + case .restrictions: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.setup() + } + case .recommendation: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + guard let self = self else { + return + } + + self.interactor.change(stakingTypeSelection: self.selection) + } + } + } +} + +extension StakingTypePresenter: Localizable { + func applyLocalization() { + if view?.isSetup == true { + updateView() + } + } +} diff --git a/novawallet/Modules/Staking/StakingType/StakingTypeProtocols.swift b/novawallet/Modules/Staking/StakingType/StakingTypeProtocols.swift new file mode 100644 index 0000000000..850854b4ea --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/StakingTypeProtocols.swift @@ -0,0 +1,51 @@ +import BigInt + +protocol StakingTypeViewProtocol: ControllerBackedProtocol { + func didReceivePoolBanner(viewModel: PoolStakingTypeViewModel, available: Bool) + func didReceiveDirectStakingBanner(viewModel: DirectStakingTypeViewModel, available: Bool) + func didReceive(stakingTypeSelection: StakingTypeSelection) + func didReceiveSaveChangesState(available: Bool) +} + +protocol StakingTypePresenterProtocol: AnyObject { + func setup() + func selectValidators() + func selectNominationPool() + func change(stakingTypeSelection: StakingTypeSelection) + func save() + func back() +} + +protocol StakingTypeInteractorInputProtocol: AnyObject { + func setup() + func change(stakingTypeSelection: StakingTypeSelection) +} + +protocol StakingTypeInteractorOutputProtocol: AnyObject { + func didReceive(nominationPoolRestrictions: RelaychainStakingRestrictions) + func didReceive(directStakingRestrictions: RelaychainStakingRestrictions) + func didReceive(method: StakingSelectionMethod) + func didReceive(error: StakingTypeError) +} + +protocol StakingTypeWireframeProtocol: AlertPresentable, CommonRetryable { + func complete(from view: ControllerBackedProtocol?) + func showNominationPoolsList( + from view: ControllerBackedProtocol?, + amount: BigUInt, + delegate: StakingSetupTypeEntityFacade, + selectedPool: NominationPools.SelectedPool? + ) + func showValidators( + from view: ControllerBackedProtocol?, + selectionValidatorGroups: SelectionValidatorGroups, + selectedValidatorList: SharedList, + validatorsSelectionParams: ValidatorsSelectionParams, + delegate: StakingSetupTypeEntityFacade + ) +} + +enum StakingTypeError: Error { + case restrictions(Error) + case recommendation(Error) +} diff --git a/novawallet/Modules/Staking/StakingType/StakingTypeViewController.swift b/novawallet/Modules/Staking/StakingType/StakingTypeViewController.swift new file mode 100644 index 0000000000..80dbe90feb --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/StakingTypeViewController.swift @@ -0,0 +1,176 @@ +import UIKit +import SoraFoundation + +final class StakingTypeViewController: UIViewController, ViewHolder { + typealias RootViewType = StakingTypeViewLayout + + let presenter: StakingTypePresenterProtocol + private var saveChangesAvailable: Bool = false + + init( + presenter: StakingTypePresenterProtocol, + localizationManager: LocalizationManagerProtocol + ) { + self.presenter = presenter + super.init(nibName: nil, bundle: nil) + + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = StakingTypeViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupNavigationItem() + setupLocalization() + setupHandlers() + presenter.setup() + } + + private func setupLocalization() { + title = R.string.localizable.stakingTypeTitle(preferredLanguages: selectedLocale.rLanguages) + navigationItem.rightBarButtonItem?.title = R.string.localizable.commonDone( + preferredLanguages: selectedLocale.rLanguages) + } + + private func setupNavigationItem() { + let title = R.string.localizable.commonDone(preferredLanguages: selectedLocale.rLanguages) + navigationItem.rightBarButtonItem = UIBarButtonItem( + title: title, + style: .plain, + target: self, + action: #selector(doneAction) + ) + navigationItem.rightBarButtonItem?.tintColor = R.color.colorButtonTextAccent() + navigationItem.rightBarButtonItem?.isEnabled = false + + let backBarButtonItem = UIBarButtonItem( + image: R.image.iconBack()!, + style: .plain, + target: self, + action: #selector(backAction) + ) + backBarButtonItem.imageInsets = .init(top: 0, left: -8, bottom: 0, right: 0) + navigationItem.leftBarButtonItem = backBarButtonItem + } + + private func setupHandlers() { + let directStakingTapGesture = UITapGestureRecognizer( + target: self, + action: #selector(directBannerAction) + ) + directStakingTapGesture.delegate = self + rootView.directStakingBannerView.addGestureRecognizer(directStakingTapGesture) + + let poolStakingTapGesture = UITapGestureRecognizer( + target: self, + action: #selector(poolBannerAction) + ) + poolStakingTapGesture.delegate = self + rootView.poolStakingBannerView.addGestureRecognizer(poolStakingTapGesture) + + rootView.poolStakingBannerView.accountView.addTarget( + self, + action: #selector(nominationPoolAction), + for: .touchUpInside + ) + rootView.directStakingBannerView.accountView.addTarget( + self, + action: #selector(validatorsAction), + for: .touchUpInside + ) + } + + @objc private func validatorsAction() { + presenter.selectValidators() + } + + @objc private func nominationPoolAction() { + presenter.selectNominationPool() + } + + @objc private func poolBannerAction() { + presenter.change(stakingTypeSelection: .nominationPool) + } + + @objc private func directBannerAction() { + presenter.change(stakingTypeSelection: .direct) + } + + @objc private func doneAction() { + presenter.save() + } + + @objc private func backAction() { + presenter.back() + } + + private func updateBannerSelection( + activeBanner: StakingTypeBannerView, + inactiveBanner: StakingTypeBannerView + ) { + activeBanner.borderView.isHighlighted = true + activeBanner.radioSelectorView.selected = true + activeBanner.accountView.isHidden = false + + inactiveBanner.borderView.isHighlighted = false + inactiveBanner.radioSelectorView.selected = false + inactiveBanner.accountView.isHidden = true + } +} + +extension StakingTypeViewController: StakingTypeViewProtocol { + func didReceivePoolBanner(viewModel: PoolStakingTypeViewModel, available: Bool) { + rootView.bind(poolStakingTypeViewModel: viewModel) + rootView.poolStakingBannerView.setEnabledStyle(available) + } + + func didReceiveDirectStakingBanner(viewModel: DirectStakingTypeViewModel, available: Bool) { + rootView.bind(directStakingTypeViewModel: viewModel) + rootView.directStakingBannerView.setEnabledStyle(available) + } + + func didReceive(stakingTypeSelection: StakingTypeSelection) { + switch stakingTypeSelection { + case .direct: + updateBannerSelection( + activeBanner: rootView.directStakingBannerView, + inactiveBanner: rootView.poolStakingBannerView + ) + case .nominationPool: + updateBannerSelection( + activeBanner: rootView.poolStakingBannerView, + inactiveBanner: rootView.directStakingBannerView + ) + } + } + + func didReceiveSaveChangesState(available: Bool) { + navigationItem.rightBarButtonItem?.isEnabled = available + } +} + +extension StakingTypeViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} + +extension StakingTypeViewController: UIGestureRecognizerDelegate { + func gestureRecognizer(_: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if touch.view is UIControl { + return false + } + return true + } +} diff --git a/novawallet/Modules/Staking/StakingType/StakingTypeViewFactory.swift b/novawallet/Modules/Staking/StakingType/StakingTypeViewFactory.swift new file mode 100644 index 0000000000..9a327658e7 --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/StakingTypeViewFactory.swift @@ -0,0 +1,101 @@ +import Foundation +import SoraFoundation +import BigInt +import SubstrateSdk + +protocol StakingTypeDelegate: AnyObject { + func changeStakingType(method: StakingSelectionMethod) +} + +enum StakingTypeViewFactory { + static func createView( + state: RelaychainStartStakingStateProtocol, + method: StakingSelectionMethod, + amount: BigUInt, + delegate: StakingTypeDelegate? + ) -> StakingTypeViewProtocol? { + guard let currencyManager = CurrencyManager.shared else { + return nil + } + guard let interactor = createInteractor(state: state, method: method, amount: amount) else { + return nil + } + + let wireframe = StakingTypeWireframe(state: state) + let priceAssetInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: state.chainAsset.assetDisplayInfo, + priceAssetInfoFactory: priceAssetInfoFactory + ) + + let viewModelFactory = StakingTypeViewModelFactory( + balanceViewModelFactory: balanceViewModelFactory, + stakingViewModelFactory: SelectedStakingTypeViewModelFactory() + ) + + let presenter = StakingTypePresenter( + interactor: interactor, + wireframe: wireframe, + chainAsset: state.chainAsset, + amount: amount, + canChangeType: state.stakingType == nil, + initialMethod: method, + viewModelFactory: viewModelFactory, + localizationManager: LocalizationManager.shared, + delegate: delegate + ) + + let view = StakingTypeViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + interactor.presenter = presenter + + return view + } + + private static func createInteractor( + state: RelaychainStartStakingStateProtocol, + method: StakingSelectionMethod, + amount: BigUInt + ) -> StakingTypeInteractor? { + let request = state.chainAsset.chain.accountRequest() + + guard let selectedAccount = SelectedWalletSettings.shared.value?.fetch(for: request) else { + return nil + } + + let recommendationFactory = StakingRecommendationMediatorFactory( + chainRegistry: ChainRegistryFacade.sharedRegistry, + operationQueue: OperationManagerFacade.sharedDefaultQueue + ) + + guard + let directStakingRestrictionsBuilder = recommendationFactory.createDirectStakingRestrictionsBuilder( + for: state + ), + let nominationPoolsRestrictionsBuilder = recommendationFactory.createPoolStakingRestrictionsBuilder( + for: state + ), + let directStakingRecommendationMediator = recommendationFactory.createDirectStakingMediator(for: state), + let nominationPoolRecommendationMediator = recommendationFactory.createPoolStakingMediator(for: state) + else { + return nil + } + + let interactor = StakingTypeInteractor( + selectedAccount: selectedAccount, + chainAsset: state.chainAsset, + amount: amount, + stakingSelectionMethod: method, + directStakingRestrictionsBuilder: directStakingRestrictionsBuilder, + nominationPoolsRestrictionsBuilder: nominationPoolsRestrictionsBuilder, + directStakingRecommendationMediator: directStakingRecommendationMediator, + nominationPoolRecommendationMediator: nominationPoolRecommendationMediator + ) + + return interactor + } +} diff --git a/novawallet/Modules/Staking/StakingType/StakingTypeViewLayout.swift b/novawallet/Modules/Staking/StakingType/StakingTypeViewLayout.swift new file mode 100644 index 0000000000..2171b656ff --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/StakingTypeViewLayout.swift @@ -0,0 +1,78 @@ +import UIKit + +final class StakingTypeViewLayout: ScrollableContainerLayoutView { + let poolStakingBannerView: StakingTypeBannerView = .create { + $0.imageView.image = R.image.imageStakingTypePool()! + $0.accountView.genericViewSkeletonSize = CGSize(width: 24, height: 24) + $0.imageSize = .init(width: 241, height: 185) + $0.imageOffsets = (top: 0, right: 0) + $0.accountView.roundedBackgroundView.fillColor = R.color.colorChipsBackground()! + } + + let directStakingBannerView: StakingTypeBannerView = .create { + $0.imageView.image = R.image.imageStakingTypeDirect()! + $0.imageSize = .init(width: 241, height: 185) + $0.imageOffsets = (top: 0, right: 0) + $0.accountView.roundedBackgroundView.fillColor = R.color.colorChipsBackground()! + } + + override func setupLayout() { + super.setupLayout() + + addArrangedSubview(poolStakingBannerView, spacingAfter: 16) + addArrangedSubview(directStakingBannerView) + } + + func bind(poolStakingTypeViewModel viewModel: PoolStakingTypeViewModel) { + poolStakingBannerView.accountView.stopLoadingIfNeeded() + + poolStakingBannerView.titleLabel.text = viewModel.title + poolStakingBannerView.detailsLabel.attributedText = NSAttributedString( + string: viewModel.subtile, + attributes: attributesForDescription + ) + + if let accountModel = viewModel.poolAccount { + poolStakingBannerView.setAction(viewModel: .init( + imageViewModel: accountModel.icon, + title: accountModel.title, + subtitle: accountModel.subtitle, + isRecommended: accountModel.subtitle != nil + )) + } else { + poolStakingBannerView.accountView.startLoadingIfNeeded() + } + } + + func bind(directStakingTypeViewModel viewModel: DirectStakingTypeViewModel) { + directStakingBannerView.accountView.stopLoadingIfNeeded() + + directStakingBannerView.titleLabel.text = viewModel.title + directStakingBannerView.detailsLabel.attributedText = NSAttributedString( + string: viewModel.subtile, + attributes: attributesForDescription + ) + + if let accountModel = viewModel.validator { + directStakingBannerView.setAction(viewModel: .init( + count: accountModel.count, + title: accountModel.title, + subtitle: accountModel.subtitle, + isRecommended: accountModel.isRecommended + )) + } else { + directStakingBannerView.accountView.startLoadingIfNeeded() + } + } + + private var attributesForDescription: [NSAttributedString.Key: Any] { + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.paragraphSpacing = 8 + paragraphStyle.firstLineHeadIndent = 0 + let detailsAttributes: [NSAttributedString.Key: Any] = [ + .paragraphStyle: paragraphStyle + ] + + return detailsAttributes + } +} diff --git a/novawallet/Modules/Staking/StakingType/StakingTypeWireframe.swift b/novawallet/Modules/Staking/StakingType/StakingTypeWireframe.swift new file mode 100644 index 0000000000..96e5768246 --- /dev/null +++ b/novawallet/Modules/Staking/StakingType/StakingTypeWireframe.swift @@ -0,0 +1,62 @@ +import Foundation +import BigInt + +final class StakingTypeWireframe: StakingTypeWireframeProtocol { + let state: RelaychainStartStakingStateProtocol + + init(state: RelaychainStartStakingStateProtocol) { + self.state = state + } + + func complete(from view: ControllerBackedProtocol?) { + view?.controller.navigationController?.popViewController(animated: true) + } + + func showNominationPoolsList( + from view: ControllerBackedProtocol?, + amount: BigUInt, + delegate: StakingSetupTypeEntityFacade, + selectedPool: NominationPools.SelectedPool? + ) { + guard let poolListView = StakingSelectPoolViewFactory.createView( + state: state, + amount: amount, + selectedPool: selectedPool, + delegate: delegate + ) else { + return + } + + delegate.bindToFlow(controller: poolListView.controller) + + view?.controller.navigationController?.pushViewController( + poolListView.controller, + animated: true + ) + } + + func showValidators( + from view: ControllerBackedProtocol?, + selectionValidatorGroups: SelectionValidatorGroups, + selectedValidatorList: SharedList, + validatorsSelectionParams: ValidatorsSelectionParams, + delegate: StakingSetupTypeEntityFacade + ) { + guard let validatorsView = CustomValidatorListViewFactory.createValidatorListView( + for: state, + selectionValidatorGroups: selectionValidatorGroups, + selectedValidatorList: selectedValidatorList, + validatorsSelectionParams: validatorsSelectionParams, + delegate: delegate + ) else { + return + } + + delegate.bindToFlow(controller: validatorsView.controller) + + view?.controller.navigationController?.pushViewController( + validatorsView.controller, + animated: true + ) + } +} diff --git a/novawallet/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmInteractor.swift b/novawallet/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmInteractor.swift index 9495987f2a..7c2d2db86c 100644 --- a/novawallet/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmInteractor.swift +++ b/novawallet/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmInteractor.swift @@ -108,7 +108,7 @@ final class StakingUnbondConfirmInteractor: RuntimeConstantFetching, AccountFetc extension StakingUnbondConfirmInteractor: StakingUnbondConfirmInteractorInputProtocol { func setup() { if let address = selectedAccount.toAddress() { - stashItemProvider = subscribeStashItemProvider(for: address) + stashItemProvider = subscribeStashItemProvider(for: address, chainId: chainAsset.chain.chainId) } else { presenter.didReceiveStashItem(result: .failure(ChainAccountFetchingError.accountNotExists)) } diff --git a/novawallet/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmViewFactory.swift b/novawallet/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmViewFactory.swift index 10d15779b4..6d2a52aa70 100644 --- a/novawallet/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmViewFactory.swift +++ b/novawallet/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmViewFactory.swift @@ -6,7 +6,7 @@ import RobinHood struct StakingUnbondConfirmViewFactory { static func createView( from amount: Decimal, - state: StakingSharedState + state: RelaychainStakingSharedStateProtocol ) -> StakingUnbondConfirmViewProtocol? { guard let interactor = createInteractor(state: state), let currencyManager = CurrencyManager.shared else { @@ -64,7 +64,9 @@ struct StakingUnbondConfirmViewFactory { ) } - private static func createInteractor(state: StakingSharedState) -> StakingUnbondConfirmInteractor? { + private static func createInteractor( + state: RelaychainStakingSharedStateProtocol + ) -> StakingUnbondConfirmInteractor? { let chainAsset = state.stakingOption.chainAsset guard @@ -81,13 +83,12 @@ struct StakingUnbondConfirmViewFactory { let connection = chainRegistry.getConnection(for: chainAsset.chain.chainId), let runtimeService = chainRegistry.getRuntimeProvider( for: chainAsset.chain.chainId - ), - let stakingDurationFactory = try? state.createStakingDurationOperationFactory( - for: chainAsset.chain ) else { return nil } + let stakingDurationFactory = state.createStakingDurationOperationFactory() + let extrinsicServiceFactory = ExtrinsicServiceFactory( runtimeRegistry: runtimeService, engine: connection, @@ -100,7 +101,7 @@ struct StakingUnbondConfirmViewFactory { selectedAccount: selectedAccount, chainAsset: chainAsset, chainRegistry: chainRegistry, - stakingLocalSubscriptionFactory: state.stakingLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.localSubscriptionFactory, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, stakingDurationOperationFactory: stakingDurationFactory, diff --git a/novawallet/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupInteractor.swift b/novawallet/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupInteractor.swift index 30954c851f..4d503213c7 100644 --- a/novawallet/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupInteractor.swift +++ b/novawallet/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupInteractor.swift @@ -71,7 +71,7 @@ final class StakingUnbondSetupInteractor: RuntimeConstantFetching, AccountFetchi extension StakingUnbondSetupInteractor: StakingUnbondSetupInteractorInputProtocol { func setup() { if let address = selectedAccount.toAddress() { - stashItemProvider = subscribeStashItemProvider(for: address) + stashItemProvider = subscribeStashItemProvider(for: address, chainId: chainAsset.chain.chainId) } else { presenter.didReceiveStashItem(result: .failure(ChainAccountFetchingError.accountNotExists)) } diff --git a/novawallet/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupViewFactory.swift b/novawallet/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupViewFactory.swift index 77605c97f5..84aaff504f 100644 --- a/novawallet/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupViewFactory.swift +++ b/novawallet/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupViewFactory.swift @@ -4,7 +4,7 @@ import SoraKeystore import RobinHood struct StakingUnbondSetupViewFactory { - static func createView(for state: StakingSharedState) -> StakingUnbondSetupViewProtocol? { + static func createView(for state: RelaychainStakingSharedStateProtocol) -> StakingUnbondSetupViewProtocol? { let chainAsset = state.stakingOption.chainAsset guard @@ -46,20 +46,19 @@ struct StakingUnbondSetupViewFactory { } private static func createInteractor( - state: StakingSharedState + state: RelaychainStakingSharedStateProtocol ) -> StakingUnbondSetupInteractor? { let chainAsset = state.stakingOption.chainAsset guard let metaAccount = SelectedWalletSettings.shared.value, let selectedAccount = metaAccount.fetch(for: chainAsset.chain.accountRequest()), - let currencyManager = CurrencyManager.shared, - let stakingDurationFactory = try? state.createStakingDurationOperationFactory( - for: chainAsset.chain - ) else { + let currencyManager = CurrencyManager.shared else { return nil } + let stakingDurationFactory = state.createStakingDurationOperationFactory() + let chainRegistry = ChainRegistryFacade.sharedRegistry let operationManager = OperationManagerFacade.sharedManager @@ -81,7 +80,7 @@ struct StakingUnbondSetupViewFactory { selectedAccount: selectedAccount, chainAsset: chainAsset, chainRegistry: chainRegistry, - stakingLocalSubscriptionFactory: state.stakingLocalSubscriptionFactory, + stakingLocalSubscriptionFactory: state.localSubscriptionFactory, walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, priceLocalSubscriptionFactory: PriceProviderFactory.shared, stakingDurationOperationFactory: stakingDurationFactory, diff --git a/novawallet/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupWireframe.swift b/novawallet/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupWireframe.swift index b04f56f424..7112e91f55 100644 --- a/novawallet/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupWireframe.swift +++ b/novawallet/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupWireframe.swift @@ -1,9 +1,9 @@ import Foundation final class StakingUnbondSetupWireframe: StakingUnbondSetupWireframeProtocol { - let state: StakingSharedState + let state: RelaychainStakingSharedStateProtocol - init(state: StakingSharedState) { + init(state: RelaychainStakingSharedStateProtocol) { self.state = state } diff --git a/novawallet/Modules/Staking/StartStakingConfirm/ExtrinsicProxy/StartStakingExtrinsicProxy.swift b/novawallet/Modules/Staking/StartStakingConfirm/ExtrinsicProxy/StartStakingExtrinsicProxy.swift new file mode 100644 index 0000000000..5f2bd1d9f0 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingConfirm/ExtrinsicProxy/StartStakingExtrinsicProxy.swift @@ -0,0 +1,244 @@ +import Foundation +import RobinHood +import BigInt + +protocol StartStakingExtrinsicProxyProtocol { + func estimateFee( + using service: ExtrinsicServiceProtocol, + feeProxy: ExtrinsicFeeProxyProtocol, + stakingOption: SelectedStakingOption, + amount: BigUInt, + feeId: TransactionFeeId + ) + + func submit( + using service: ExtrinsicServiceProtocol, + signer: SigningWrapperProtocol, + stakingOption: SelectedStakingOption, + amount: BigUInt, + closure: @escaping ExtrinsicSubmitClosure + ) +} + +extension StartStakingExtrinsicProxyProtocol { + func estimateFee( + using service: ExtrinsicServiceProtocol, + feeProxy: ExtrinsicFeeProxyProtocol, + stakingOption: SelectedStakingOption, + amount: BigUInt + ) { + let feeId = StartStakingFeeIdFactory.generateFeeId(for: stakingOption, amount: amount) + + estimateFee( + using: service, + feeProxy: feeProxy, + stakingOption: stakingOption, + amount: amount, + feeId: feeId + ) + } +} + +final class StartStakingExtrinsicProxy { + struct DirectStakingParams { + let controller: AccountId + let validators: PreparedValidators + let amount: BigUInt + } + + struct PoolStakingParams { + let pool: NominationPools.SelectedPool + let amount: BigUInt + } + + let runtimeService: RuntimeCodingServiceProtocol + let operationQueue: OperationQueue + let selectedAccount: ChainAccountResponse + + init( + selectedAccount: ChainAccountResponse, + runtimeService: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue + ) { + self.selectedAccount = selectedAccount + self.runtimeService = runtimeService + self.operationQueue = operationQueue + } + + private func createDirectStakingBuilderClosure( + for params: DirectStakingParams, + coderFactory: RuntimeCoderFactoryProtocol + ) -> ExtrinsicBuilderClosure { + { builder in + let bondClosure = try Staking.Bond.appendCall( + for: .accoundId(params.controller), + value: params.amount, + payee: .staked, + codingFactory: coderFactory + ) + + let callFactory = SubstrateCallFactory() + + let targets = params.validators.targets + let nominateCall = try callFactory.nominate(targets: targets) + + return try bondClosure(builder).adding(call: nominateCall) + } + } + + private func createPoolStakingBuilderClosure( + for params: PoolStakingParams + ) -> ExtrinsicBuilderClosure { + { builder in + let call = NominationPools.JoinCall(amount: params.amount, poolId: params.pool.poolId) + + return try builder.adding(call: call.runtimeCall()) + } + } + + private func estimateDirectStakingFee( + service: ExtrinsicServiceProtocol, + feeProxy: ExtrinsicFeeProxyProtocol, + params: DirectStakingParams, + feeId: TransactionFeeId, + coderFactory: RuntimeCoderFactoryProtocol + ) { + let closure = createDirectStakingBuilderClosure( + for: params, + coderFactory: coderFactory + ) + + feeProxy.estimateFee( + using: service, + reuseIdentifier: feeId, + setupBy: closure + ) + } + + private func estimatePoolStakingFee( + service: ExtrinsicServiceProtocol, + feeProxy: ExtrinsicFeeProxyProtocol, + params: PoolStakingParams, + feeId: TransactionFeeId + ) { + let closure = createPoolStakingBuilderClosure(for: params) + + feeProxy.estimateFee( + using: service, + reuseIdentifier: feeId, + setupBy: closure + ) + } + + private func submitDirectStaking( + service: ExtrinsicServiceProtocol, + signer: SigningWrapperProtocol, + params: DirectStakingParams, + coderFactory: RuntimeCoderFactoryProtocol, + closure: @escaping ExtrinsicSubmitClosure + ) { + let builderClosure = createDirectStakingBuilderClosure( + for: params, + coderFactory: coderFactory + ) + + service.submit( + builderClosure, + signer: signer, + runningIn: .main, + completion: closure + ) + } + + private func submitPoolStaking( + service: ExtrinsicServiceProtocol, + signer: SigningWrapperProtocol, + params: PoolStakingParams, + closure: @escaping ExtrinsicSubmitClosure + ) { + let builderClosure = createPoolStakingBuilderClosure(for: params) + + service.submit( + builderClosure, + signer: signer, + runningIn: .main, + completion: closure + ) + } +} + +extension StartStakingExtrinsicProxy: StartStakingExtrinsicProxyProtocol { + func estimateFee( + using service: ExtrinsicServiceProtocol, + feeProxy: ExtrinsicFeeProxyProtocol, + stakingOption: SelectedStakingOption, + amount: BigUInt, + feeId: TransactionFeeId + ) { + switch stakingOption { + case let .direct(preparedValidators): + let controller = selectedAccount.accountId + + runtimeService.fetchCoderFactory( + runningIn: OperationManager(operationQueue: operationQueue), + completion: { [weak self] coderFactory in + self?.estimateDirectStakingFee( + service: service, + feeProxy: feeProxy, + params: .init(controller: controller, validators: preparedValidators, amount: amount), + feeId: feeId, + coderFactory: coderFactory + ) + }, errorClosure: { error in + feeProxy.delegate?.didReceiveFee(result: .failure(error), for: feeId) + } + ) + case let .pool(selectedPool): + estimatePoolStakingFee( + service: service, + feeProxy: feeProxy, + params: .init(pool: selectedPool, amount: amount), + feeId: feeId + ) + } + } + + func submit( + using service: ExtrinsicServiceProtocol, + signer: SigningWrapperProtocol, + stakingOption: SelectedStakingOption, + amount: BigUInt, + closure: @escaping ExtrinsicSubmitClosure + ) { + switch stakingOption { + case let .direct(preparedValidators): + let controller = selectedAccount.accountId + + runtimeService.fetchCoderFactory( + runningIn: OperationManager(operationQueue: operationQueue), + completion: { [weak self] coderFactory in + self?.submitDirectStaking( + service: service, + signer: signer, + params: .init( + controller: controller, + validators: preparedValidators, + amount: amount + ), + coderFactory: coderFactory, + closure: closure + ) + }, errorClosure: { error in + closure(.failure(error)) + } + ) + case let .pool(selectedPool): + submitPoolStaking( + service: service, + signer: signer, + params: .init(pool: selectedPool, amount: amount), + closure: closure + ) + } + } +} diff --git a/novawallet/Modules/Staking/StartStakingConfirm/ExtrinsicProxy/StartStakingFeeIdFactory.swift b/novawallet/Modules/Staking/StartStakingConfirm/ExtrinsicProxy/StartStakingFeeIdFactory.swift new file mode 100644 index 0000000000..cc819547d5 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingConfirm/ExtrinsicProxy/StartStakingFeeIdFactory.swift @@ -0,0 +1,16 @@ +import Foundation +import BigInt + +enum StartStakingFeeIdFactory { + static func generateFeeId( + for stakingOption: SelectedStakingOption, + amount: BigUInt + ) -> TransactionFeeId { + switch stakingOption { + case let .direct(validators): + return "direct" + "\(validators.targets.count)" + "\(amount)" + case let .pool(pool): + return "pool" + "\(pool.poolId)" + "\(amount)" + } + } +} diff --git a/novawallet/Modules/Staking/StartStakingConfirm/Presenter/StartStakingDirectConfirmPresenter.swift b/novawallet/Modules/Staking/StartStakingConfirm/Presenter/StartStakingDirectConfirmPresenter.swift new file mode 100644 index 0000000000..d2e620014b --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingConfirm/Presenter/StartStakingDirectConfirmPresenter.swift @@ -0,0 +1,87 @@ +import Foundation +import SoraFoundation + +final class StartStakingDirectConfirmPresenter: StartStakingConfirmPresenter { + let quantityFormatter: LocalizableResource + let model: PreparedValidators + + var directWireframe: StartStakingDirectConfirmWireframeProtocol? { + wireframe as? StartStakingDirectConfirmWireframeProtocol + } + + init( + model: PreparedValidators, + interactor: StartStakingConfirmInteractorInputProtocol, + wireframe: StartStakingDirectConfirmWireframeProtocol, + amount: Decimal, + chainAsset: ChainAsset, + selectedAccount: MetaChainAccountResponse, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + dataValidatingFactory: StakingDataValidatingFactoryProtocol, + quantityFormatter: LocalizableResource, + localizationManager: LocalizationManagerProtocol, + logger: LoggerProtocol + ) { + self.model = model + self.quantityFormatter = quantityFormatter + + super.init( + interactor: interactor, + wireframe: wireframe, + amount: amount, + chainAsset: chainAsset, + selectedAccount: selectedAccount, + balanceViewModelFactory: balanceViewModelFactory, + dataValidatingFactory: dataValidatingFactory, + localizationManager: localizationManager, + logger: logger + ) + } + + override func provideStakingType() { + let stakingType = R.string.localizable.stakingDirectStaking( + preferredLanguages: selectedLocale.rLanguages + ) + + view?.didReceiveStakingType(viewModel: stakingType) + } + + override func provideStakingDetails() { + let title = R.string.localizable.stakingValidators(preferredLanguages: selectedLocale.rLanguages) + + let selectedString = quantityFormatter.value( + for: selectedLocale + ).string(from: .init(value: model.targets.count)) ?? "" + + let maxString = quantityFormatter.value( + for: selectedLocale + ).string(from: .init(value: model.targets.count)) ?? "" + + let details = R.string.localizable.stakingValidatorInfoNominators( + selectedString, + maxString, + preferredLanguages: selectedLocale.rLanguages + ) + + view?.didReceiveStakingDetails( + title: title, + info: .init(address: "", name: details, imageViewModel: nil) + ) + } + + override func showStakingDetails() { + directWireframe?.showSelectedValidators(from: view, validators: model) + } + + override func createStakingSpecificValidations() -> [DataValidating] { + [ + dataValidatingFactory.canPayFeeSpendingAmountInPlank( + balance: assetBalance?.freeInPlank, + fee: fee, + spendingAmount: amount, + asset: chainAsset.assetDisplayInfo, + locale: selectedLocale + ) + ] + } +} diff --git a/novawallet/Modules/Staking/StartStakingConfirm/Presenter/StartStakingDirectConfirmWireframe.swift b/novawallet/Modules/Staking/StartStakingConfirm/Presenter/StartStakingDirectConfirmWireframe.swift new file mode 100644 index 0000000000..05d6321aac --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingConfirm/Presenter/StartStakingDirectConfirmWireframe.swift @@ -0,0 +1,22 @@ +import Foundation + +final class StartStakingDirectConfirmWireframe: StartStakingConfirmWireframe, + StartStakingDirectConfirmWireframeProtocol { + let stakingState: RelaychainStartStakingStateProtocol + + init(stakingState: RelaychainStartStakingStateProtocol) { + self.stakingState = stakingState + } + + func showSelectedValidators(from view: StartStakingConfirmViewProtocol?, validators: PreparedValidators) { + guard + let listView = StaticValidatorListViewFactory.createView( + validatorList: validators, + stakingState: stakingState + ) else { + return + } + + view?.controller.navigationController?.pushViewController(listView.controller, animated: true) + } +} diff --git a/novawallet/Modules/Staking/StartStakingConfirm/Presenter/StartStakingPoolConfirmPresenter.swift b/novawallet/Modules/Staking/StartStakingConfirm/Presenter/StartStakingPoolConfirmPresenter.swift new file mode 100644 index 0000000000..053d3e7b40 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingConfirm/Presenter/StartStakingPoolConfirmPresenter.swift @@ -0,0 +1,69 @@ +import Foundation +import SoraFoundation + +final class StartStakingPoolConfirmPresenter: StartStakingConfirmPresenter { + let model: NominationPools.SelectedPool + + private lazy var addressViewModelFactory = DisplayAddressViewModelFactory() + + init( + model: NominationPools.SelectedPool, + interactor: StartStakingConfirmInteractorInputProtocol, + wireframe: StartStakingConfirmWireframeProtocol, + amount: Decimal, + chainAsset: ChainAsset, + selectedAccount: MetaChainAccountResponse, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + dataValidatingFactory: StakingDataValidatingFactoryProtocol, + localizationManager: LocalizationManagerProtocol, + logger: LoggerProtocol + ) { + self.model = model + + super.init( + interactor: interactor, + wireframe: wireframe, + amount: amount, + chainAsset: chainAsset, + selectedAccount: selectedAccount, + balanceViewModelFactory: balanceViewModelFactory, + dataValidatingFactory: dataValidatingFactory, + localizationManager: localizationManager, + logger: logger + ) + } + + override func provideStakingType() { + let stakingType = R.string.localizable.stakingPoolStaking( + preferredLanguages: selectedLocale.rLanguages + ) + + view?.didReceiveStakingType(viewModel: stakingType) + } + + override func provideStakingDetails() { + let title = R.string.localizable.stakingPool(preferredLanguages: selectedLocale.rLanguages) + + let viewModel = addressViewModelFactory.createViewModel(from: model, chainAsset: chainAsset) + + view?.didReceiveStakingDetails(title: title, info: viewModel) + } + + override func showStakingDetails() { + if let address = model.bondedAddress(for: chainAsset.chain.chainFormat) { + showDetails(for: address) + } + } + + override func createStakingSpecificValidations() -> [DataValidating] { + [ + dataValidatingFactory.canPayFeeSpendingAmountInPlank( + balance: assetBalance?.transferable, + fee: fee, + spendingAmount: amount, + asset: chainAsset.assetDisplayInfo, + locale: selectedLocale + ) + ] + } +} diff --git a/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmInteractor.swift b/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmInteractor.swift new file mode 100644 index 0000000000..342360e356 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmInteractor.swift @@ -0,0 +1,200 @@ +import UIKit +import RobinHood +import BigInt + +class StartStakingConfirmInteractor: AnyProviderAutoCleaning { + weak var presenter: StartStakingConfirmInteractorOutputProtocol? + + let chainAsset: ChainAsset + let stakingAmount: BigUInt + let stakingOption: SelectedStakingOption + let selectedAccount: ChainAccountResponse + let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol + let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol + let extrinsicService: ExtrinsicServiceProtocol + let extrinsicFeeProxy: ExtrinsicFeeProxyProtocol + let signingWrapper: SigningWrapperProtocol + let extrinsicSubmissionProxy: StartStakingExtrinsicProxyProtocol + let restrictionsBuilder: RelaychainStakingRestrictionsBuilding + + private var priceProvider: StreamableProvider? + private var balanceProvider: StreamableProvider? + + init( + stakingAmount: BigUInt, + stakingOption: SelectedStakingOption, + chainAsset: ChainAsset, + selectedAccount: ChainAccountResponse, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + extrinsicService: ExtrinsicServiceProtocol, + extrinsicFeeProxy: ExtrinsicFeeProxyProtocol, + restrictionsBuilder: RelaychainStakingRestrictionsBuilding, + extrinsicSubmissionProxy: StartStakingExtrinsicProxyProtocol, + signingWrapper: SigningWrapperProtocol, + currencyManager: CurrencyManagerProtocol + ) { + self.stakingAmount = stakingAmount + self.stakingOption = stakingOption + self.chainAsset = chainAsset + self.selectedAccount = selectedAccount + self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory + self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory + self.extrinsicSubmissionProxy = extrinsicSubmissionProxy + self.extrinsicService = extrinsicService + self.extrinsicFeeProxy = extrinsicFeeProxy + self.restrictionsBuilder = restrictionsBuilder + self.signingWrapper = signingWrapper + self.currencyManager = currencyManager + } + + private func performPriceSubscription() { + clear(streamableProvider: &priceProvider) + + guard let priceId = chainAsset.asset.priceId else { + presenter?.didReceive(price: nil) + return + } + + priceProvider = subscribeToPrice(for: priceId, currency: selectedCurrency) + } + + private func performAssetBalanceSubscription() { + clear(streamableProvider: &balanceProvider) + + let chainAssetId = chainAsset.chainAssetId + + balanceProvider = subscribeToAssetBalanceProvider( + for: selectedAccount.accountId, + chainId: chainAssetId.chainId, + assetId: chainAssetId.assetId + ) + } +} + +extension StartStakingConfirmInteractor: StartStakingConfirmInteractorInputProtocol { + func setup() { + extrinsicFeeProxy.delegate = self + restrictionsBuilder.delegate = self + + performAssetBalanceSubscription() + performPriceSubscription() + + restrictionsBuilder.start() + + estimateFee() + } + + func remakeSubscriptions() { + performAssetBalanceSubscription() + performPriceSubscription() + } + + func retryRestrinctions() { + restrictionsBuilder.stop() + restrictionsBuilder.start() + } + + func estimateFee() { + extrinsicSubmissionProxy.estimateFee( + using: extrinsicService, + feeProxy: extrinsicFeeProxy, + stakingOption: stakingOption, + amount: stakingAmount + ) + } + + func submit() { + extrinsicSubmissionProxy.submit( + using: extrinsicService, + signer: signingWrapper, + stakingOption: stakingOption, + amount: stakingAmount + ) { [weak self] result in + switch result { + case let .success(hash): + self?.presenter?.didReceiveConfirmation(hash: hash) + case let .failure(error): + self?.presenter?.didReceive(error: .confirmation(error)) + } + } + } +} + +extension StartStakingConfirmInteractor: ExtrinsicFeeProxyDelegate { + func didReceiveFee(result: Result, for _: TransactionFeeId) { + switch result { + case let .success(info): + presenter?.didReceive(fee: BigUInt(info.fee)) + case let .failure(error): + presenter?.didReceive(error: .fee(error)) + } + } +} + +extension StartStakingConfirmInteractor: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { + func handleAssetBalance( + result: Result, + accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) { + guard + chainId == chainAsset.chain.chainId, + assetId == chainAsset.asset.assetId, + accountId == selectedAccount.accountId else { + return + } + + switch result { + case let .success(balance): + let balance = balance ?? .createZero( + for: .init(chainId: chainId, assetId: assetId), + accountId: accountId + ) + + presenter?.didReceive(assetBalance: balance) + case let .failure(error): + presenter?.didReceive(error: .assetBalance(error)) + } + } +} + +extension StartStakingConfirmInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { + func handlePrice(result: Result, priceId: AssetModel.PriceId) { + if chainAsset.asset.priceId == priceId { + switch result { + case let .success(priceData): + presenter?.didReceive(price: priceData) + case let .failure(error): + presenter?.didReceive(error: .price(error)) + } + } + } +} + +extension StartStakingConfirmInteractor: RelaychainStakingRestrictionsBuilderDelegate { + func restrictionsBuilder( + _: RelaychainStakingRestrictionsBuilding, + didReceive error: Error + ) { + presenter?.didReceive(error: .restrictions(error)) + } + + func restrictionsBuilder( + _: RelaychainStakingRestrictionsBuilding, + didPrepare restrictions: RelaychainStakingRestrictions + ) { + presenter?.didReceive(restrictions: restrictions) + } +} + +extension StartStakingConfirmInteractor: SelectedCurrencyDepending { + func applyCurrency() { + guard presenter != nil, let priceId = chainAsset.asset.priceId else { + return + } + + priceProvider = subscribeToPrice(for: priceId, currency: selectedCurrency) + } +} diff --git a/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmInteractorError.swift b/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmInteractorError.swift new file mode 100644 index 0000000000..2d26f8dd88 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmInteractorError.swift @@ -0,0 +1,9 @@ +import Foundation + +enum StartStakingConfirmInteractorError: Error { + case assetBalance(Error) + case price(Error) + case fee(Error) + case confirmation(Error) + case restrictions(Error) +} diff --git a/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmPresenter.swift b/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmPresenter.swift new file mode 100644 index 0000000000..12d69c4eba --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmPresenter.swift @@ -0,0 +1,257 @@ +import Foundation +import BigInt +import SoraFoundation + +class StartStakingConfirmPresenter { + weak var view: StartStakingConfirmViewProtocol? + let wireframe: StartStakingConfirmWireframeProtocol + let interactor: StartStakingConfirmInteractorInputProtocol + + let chainAsset: ChainAsset + let amount: Decimal + let selectedAccount: MetaChainAccountResponse + let balanceViewModelFactory: BalanceViewModelFactoryProtocol + let dataValidatingFactory: StakingDataValidatingFactoryProtocol + let logger: LoggerProtocol + + var assetBalance: AssetBalance? + var price: PriceData? + var fee: BigUInt? + var restrictions: RelaychainStakingRestrictions? + + private lazy var walletDisplayViewModelFactory = WalletAccountViewModelFactory() + private lazy var addressDisplayViewModelFactory = DisplayAddressViewModelFactory() + + init( + interactor: StartStakingConfirmInteractorInputProtocol, + wireframe: StartStakingConfirmWireframeProtocol, + amount: Decimal, + chainAsset: ChainAsset, + selectedAccount: MetaChainAccountResponse, + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + dataValidatingFactory: StakingDataValidatingFactoryProtocol, + localizationManager: LocalizationManagerProtocol, + logger: LoggerProtocol + ) { + self.interactor = interactor + self.wireframe = wireframe + self.amount = amount + self.chainAsset = chainAsset + self.selectedAccount = selectedAccount + self.balanceViewModelFactory = balanceViewModelFactory + self.dataValidatingFactory = dataValidatingFactory + self.logger = logger + self.localizationManager = localizationManager + } + + func provideAmountViewModel() { + let viewModel = balanceViewModelFactory.balanceFromPrice( + amount, + priceData: price + ).value(for: selectedLocale) + + view?.didReceiveAmount(viewModel: viewModel) + } + + func provideWalletViewModel() { + guard + let viewModel = try? walletDisplayViewModelFactory.createDisplayViewModel( + from: selectedAccount + ) else { + return + } + + view?.didReceiveWallet(viewModel: viewModel.cellViewModel) + } + + func provideAccountViewModel() { + guard let address = selectedAccount.chainAccount.toAddress() else { + return + } + + let viewModel = addressDisplayViewModelFactory.createViewModel(from: address) + view?.didReceiveAccount(viewModel: viewModel) + } + + func provideFeeViewModel() { + if let fee = fee { + let feeDecimal = fee.decimal(precision: chainAsset.asset.precision) + + let viewModel = balanceViewModelFactory.balanceFromPrice(feeDecimal, priceData: price) + .value(for: selectedLocale) + + view?.didReceiveFee(viewModel: viewModel) + } else { + view?.didReceiveFee(viewModel: nil) + } + } + + func showDetails(for address: AccountAddress) { + guard let view = view else { + return + } + + wireframe.presentAccountOptions( + from: view, + address: address, + chain: chainAsset.chain, + locale: selectedLocale + ) + } + + func provideStakingType() { + fatalError("Must be overriden by subsclass") + } + + func provideStakingDetails() { + fatalError("Must be overriden by subsclass") + } + + func showStakingDetails() { + fatalError("Must be overriden by subsclass") + } + + private func updateView() { + provideAmountViewModel() + provideWalletViewModel() + provideAccountViewModel() + provideFeeViewModel() + provideStakingType() + provideStakingDetails() + } + + func createCommonValidations() -> [DataValidating] { + [ + dataValidatingFactory.hasInPlank( + fee: fee, + locale: selectedLocale, + precision: chainAsset.assetDisplayInfo.assetPrecision + ) { [weak self] in + self?.interactor.estimateFee() + }, + dataValidatingFactory.canPayFeeInPlank( + balance: assetBalance?.transferable, + fee: fee, + asset: chainAsset.assetDisplayInfo, + locale: selectedLocale + ), + dataValidatingFactory.canNominateInPlank( + amount: amount, + minimalBalance: restrictions?.minJoinStake, + minNominatorBond: restrictions?.minJoinStake, + precision: chainAsset.asset.precision, + locale: selectedLocale + ), + dataValidatingFactory.allowsNewNominators( + flag: restrictions?.allowsNewStakers ?? true, + locale: selectedLocale + ) + ] + } + + func createStakingSpecificValidations() -> [DataValidating] { + fatalError("Must be overriden by subsclass") + } + + func createValidations() -> [DataValidating] { + createCommonValidations() + createStakingSpecificValidations() + } +} + +extension StartStakingConfirmPresenter: StartStakingConfirmPresenterProtocol { + func setup() { + updateView() + + interactor.setup() + } + + func selectSender() { + if + let address = try? selectedAccount.chainAccount.accountId.toAddress( + using: chainAsset.chain.chainFormat + ) { + showDetails(for: address) + } + } + + func selectStakingDetails() { + showStakingDetails() + } + + func confirm() { + let validations = createValidations() + + DataValidationRunner(validators: validations).runValidation { [weak self] in + self?.view?.didStartLoading() + self?.interactor.submit() + } + } +} + +extension StartStakingConfirmPresenter: StartStakingConfirmInteractorOutputProtocol { + func didReceive(assetBalance: AssetBalance?) { + self.assetBalance = assetBalance + } + + func didReceive(price: PriceData?) { + self.price = price + + provideAmountViewModel() + provideFeeViewModel() + } + + func didReceive(fee: BigUInt?) { + self.fee = fee + + logger.debug("Did receive fee: \(String(describing: fee))") + + provideFeeViewModel() + } + + func didReceiveConfirmation(hash _: String) { + view?.didStopLoading() + + wireframe.presentExtrinsicSubmission(from: view, completionAction: .popBaseAndDismiss, locale: selectedLocale) + } + + func didReceive(restrictions: RelaychainStakingRestrictions) { + self.restrictions = restrictions + + logger.debug("Did receive restrinctions: \(restrictions)") + } + + func didReceive(error: StartStakingConfirmInteractorError) { + logger.error("Did receive error: \(error)") + + switch error { + case .assetBalance, .price: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.remakeSubscriptions() + } + case .restrictions: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryRestrinctions() + } + case .fee: + wireframe.presentFeeStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.estimateFee() + } + case let .confirmation(internalError): + view?.didStopLoading() + + if internalError.isWatchOnlySigning { + wireframe.presentDismissingNoSigningView(from: view) + } else { + _ = wireframe.present(error: internalError, from: view, locale: selectedLocale) + } + } + } +} + +extension StartStakingConfirmPresenter: Localizable { + func applyLocalization() { + if let view = view, view.isSetup { + updateView() + } + } +} diff --git a/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmProtocols.swift b/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmProtocols.swift new file mode 100644 index 0000000000..2223fda00d --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmProtocols.swift @@ -0,0 +1,43 @@ +import Foundation +import BigInt + +protocol StartStakingConfirmViewProtocol: ControllerBackedProtocol, LoadableViewProtocol { + func didReceiveAmount(viewModel: BalanceViewModelProtocol) + func didReceiveWallet(viewModel: StackCellViewModel) + func didReceiveAccount(viewModel: DisplayAddressViewModel) + func didReceiveFee(viewModel: BalanceViewModelProtocol?) + func didReceiveStakingType(viewModel: String) + func didReceiveStakingDetails(title: String, info: DisplayAddressViewModel) +} + +protocol StartStakingConfirmPresenterProtocol: AnyObject { + func setup() + func selectSender() + func selectStakingDetails() + func confirm() +} + +protocol StartStakingConfirmInteractorInputProtocol: AnyObject { + func setup() + func remakeSubscriptions() + func retryRestrinctions() + func estimateFee() + func submit() +} + +protocol StartStakingConfirmInteractorOutputProtocol: AnyObject { + func didReceive(assetBalance: AssetBalance?) + func didReceive(price: PriceData?) + func didReceive(fee: BigUInt?) + func didReceive(restrictions: RelaychainStakingRestrictions) + func didReceiveConfirmation(hash: String) + func didReceive(error: StartStakingConfirmInteractorError) +} + +protocol StartStakingConfirmWireframeProtocol: AlertPresentable, ErrorPresentable, FeeRetryable, + CommonRetryable, AddressOptionsPresentable, MessageSheetPresentable, + ExtrinsicSubmissionPresenting, StakingErrorPresentable {} + +protocol StartStakingDirectConfirmWireframeProtocol: StartStakingConfirmWireframeProtocol { + func showSelectedValidators(from view: StartStakingConfirmViewProtocol?, validators: PreparedValidators) +} diff --git a/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmViewController.swift b/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmViewController.swift new file mode 100644 index 0000000000..5310d3b684 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmViewController.swift @@ -0,0 +1,125 @@ +import UIKit +import SoraFoundation + +final class StartStakingConfirmViewController: UIViewController, ViewHolder { + typealias RootViewType = StartStakingConfirmViewLayout + + let presenter: StartStakingConfirmPresenterProtocol + + init(presenter: StartStakingConfirmPresenterProtocol, localizationManager: LocalizationManagerProtocol) { + self.presenter = presenter + super.init(nibName: nil, bundle: nil) + + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = StartStakingConfirmViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupHandlers() + setupLocalization() + + presenter.setup() + } + + private func setupHandlers() { + rootView.accountCell.addTarget( + self, + action: #selector(actionSelectSender), + for: .touchUpInside + ) + + rootView.stakingDetailsCell.addTarget( + self, + action: #selector(actionSelectDetails), + for: .touchUpInside + ) + + rootView.genericActionView.actionButton.addTarget( + self, + action: #selector(actionConfirm), + for: .touchUpInside + ) + } + + private func setupLocalization() { + let languages = selectedLocale.rLanguages + + title = R.string.localizable.stakingStartTitle(preferredLanguages: languages) + + rootView.walletCell.titleLabel.text = R.string.localizable.commonWallet(preferredLanguages: languages) + rootView.accountCell.titleLabel.text = R.string.localizable.commonAccount(preferredLanguages: languages) + + rootView.feeCell.rowContentView.locale = selectedLocale + + rootView.stakingTypeCell.titleLabel.text = R.string.localizable.stakingTypeTitle(preferredLanguages: languages) + + rootView.genericActionView.actionButton.imageWithTitleView?.title = R.string.localizable + .commonConfirm(preferredLanguages: selectedLocale.rLanguages) + } + + @objc func actionSelectSender() { + presenter.selectSender() + } + + @objc func actionSelectDetails() { + presenter.selectStakingDetails() + } + + @objc func actionConfirm() { + presenter.confirm() + } +} + +extension StartStakingConfirmViewController: StartStakingConfirmViewProtocol { + func didReceiveAmount(viewModel: BalanceViewModelProtocol) { + rootView.amountView.bind(viewModel: viewModel) + } + + func didReceiveWallet(viewModel: StackCellViewModel) { + rootView.walletCell.bind(viewModel: viewModel) + } + + func didReceiveAccount(viewModel: DisplayAddressViewModel) { + rootView.accountCell.bind(viewModel: viewModel.cellViewModel) + } + + func didReceiveFee(viewModel: BalanceViewModelProtocol?) { + rootView.feeCell.rowContentView.bind(viewModel: viewModel) + } + + func didReceiveStakingType(viewModel: String) { + rootView.stakingTypeCell.bind(details: viewModel) + } + + func didReceiveStakingDetails(title: String, info: DisplayAddressViewModel) { + rootView.stakingDetailsCell.titleLabel.text = title + rootView.stakingDetailsCell.detailsLabel.lineBreakMode = info.lineBreakMode + rootView.stakingDetailsCell.bind(viewModel: info.cellViewModel) + } + + func didStartLoading() { + rootView.genericActionView.startLoading() + } + + func didStopLoading() { + rootView.genericActionView.stopLoading() + } +} + +extension StartStakingConfirmViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} diff --git a/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmViewFactory.swift b/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmViewFactory.swift new file mode 100644 index 0000000000..a523ca1afe --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmViewFactory.swift @@ -0,0 +1,225 @@ +import Foundation +import SoraFoundation +import SoraKeystore + +struct StartStakingConfirmViewFactory { + static func createView( + for stakingOption: SelectedStakingOption, + amount: Decimal, + state: RelaychainStartStakingStateProtocol + ) -> StartStakingConfirmViewProtocol? { + guard + let selectedAccount = SelectedWalletSettings.shared.value.fetchMetaChainAccount( + for: state.chainAsset.chain.accountRequest() + ), + let currencyManager = CurrencyManager.shared else { + return nil + } + + switch stakingOption { + case let .direct(preparedValidators): + return createDirectStakingView( + for: preparedValidators, + amount: amount, + state: state, + selectedAccount: selectedAccount, + currencyManager: currencyManager + ) + case let .pool(selectedPool): + return createPoolStakingView( + for: selectedPool, + amount: amount, + state: state, + selectedAccount: selectedAccount, + currencyManager: currencyManager + ) + } + } + + private static func createDirectStakingView( + for validators: PreparedValidators, + amount: Decimal, + state: RelaychainStartStakingStateProtocol, + selectedAccount: MetaChainAccountResponse, + currencyManager: CurrencyManagerProtocol + ) -> StartStakingConfirmViewProtocol? { + guard + let interactor = createInteractor( + for: .direct(validators), + amount: amount, + state: state, + selectedAccount: selectedAccount, + currencyManager: currencyManager + ) else { + return nil + } + + let wireframe = StartStakingDirectConfirmWireframe(stakingState: state) + + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: state.chainAsset.assetDisplayInfo, + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager) + ) + + let dataValidatingFactory = StakingDataValidatingFactory( + presentable: wireframe, + balanceFactory: balanceViewModelFactory + ) + + let presenter = StartStakingDirectConfirmPresenter( + model: validators, + interactor: interactor, + wireframe: wireframe, + amount: amount, + chainAsset: state.chainAsset, + selectedAccount: selectedAccount, + balanceViewModelFactory: balanceViewModelFactory, + dataValidatingFactory: dataValidatingFactory, + quantityFormatter: NumberFormatter.quantity.localizableResource(), + localizationManager: LocalizationManager.shared, + logger: Logger.shared + ) + + let view = StartStakingConfirmViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + interactor.presenter = presenter + dataValidatingFactory.view = view + + return view + } + + private static func createPoolStakingView( + for pool: NominationPools.SelectedPool, + amount: Decimal, + state: RelaychainStartStakingStateProtocol, + selectedAccount: MetaChainAccountResponse, + currencyManager: CurrencyManagerProtocol + ) -> StartStakingConfirmViewProtocol? { + guard + let interactor = createInteractor( + for: .pool(pool), + amount: amount, + state: state, + selectedAccount: selectedAccount, + currencyManager: currencyManager + ) else { + return nil + } + + let wireframe = StartStakingConfirmWireframe() + + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: state.chainAsset.assetDisplayInfo, + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager) + ) + + let dataValidatingFactory = StakingDataValidatingFactory( + presentable: wireframe, + balanceFactory: balanceViewModelFactory + ) + + let presenter = StartStakingPoolConfirmPresenter( + model: pool, + interactor: interactor, + wireframe: wireframe, + amount: amount, + chainAsset: state.chainAsset, + selectedAccount: selectedAccount, + balanceViewModelFactory: balanceViewModelFactory, + dataValidatingFactory: dataValidatingFactory, + localizationManager: LocalizationManager.shared, + logger: Logger.shared + ) + + let view = StartStakingConfirmViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + interactor.presenter = presenter + dataValidatingFactory.view = view + + return view + } + + private static func createInteractor( + for stakingOption: SelectedStakingOption, + amount: Decimal, + state: RelaychainStartStakingStateProtocol, + selectedAccount: MetaChainAccountResponse, + currencyManager: CurrencyManagerProtocol + ) -> StartStakingConfirmInteractor? { + guard + let amountInPlank = amount.toSubstrateAmount( + precision: state.chainAsset.assetDisplayInfo.assetPrecision + ) else { + return nil + } + + let chainId = state.chainAsset.chain.chainId + + guard + let runtimeService = ChainRegistryFacade.sharedRegistry.getRuntimeProvider(for: chainId), + let connection = ChainRegistryFacade.sharedRegistry.getConnection(for: chainId) else { + return nil + } + + let extrinsicService = ExtrinsicServiceFactory( + runtimeRegistry: runtimeService, + engine: connection, + operationManager: OperationManagerFacade.sharedManager + ).createService(account: selectedAccount.chainAccount, chain: state.chainAsset.chain) + + let signer = SigningWrapperFactory(keystore: Keychain()).createSigningWrapper( + for: selectedAccount.metaId, + accountResponse: selectedAccount.chainAccount + ) + + let extrinsicProxy = StartStakingExtrinsicProxy( + selectedAccount: selectedAccount.chainAccount, + runtimeService: runtimeService, + operationQueue: OperationManagerFacade.sharedDefaultQueue + ) + + guard let restrictionsBuilder = createRestrictionsBuilder(for: stakingOption, state: state) else { + return nil + } + + return .init( + stakingAmount: amountInPlank, + stakingOption: stakingOption, + chainAsset: state.chainAsset, + selectedAccount: selectedAccount.chainAccount, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactory.shared, + priceLocalSubscriptionFactory: PriceProviderFactory.shared, + extrinsicService: extrinsicService, + extrinsicFeeProxy: ExtrinsicFeeProxy(), + restrictionsBuilder: restrictionsBuilder, + extrinsicSubmissionProxy: extrinsicProxy, + signingWrapper: signer, + currencyManager: currencyManager + ) + } + + private static func createRestrictionsBuilder( + for stakingOption: SelectedStakingOption, + state: RelaychainStartStakingStateProtocol + ) -> RelaychainStakingRestrictionsBuilding? { + let factory = StakingRecommendationMediatorFactory( + chainRegistry: ChainRegistryFacade.sharedRegistry, + operationQueue: OperationManagerFacade.sharedDefaultQueue + ) + + switch stakingOption { + case .direct: + return factory.createDirectStakingRestrictionsBuilder(for: state) + case .pool: + return factory.createPoolStakingRestrictionsBuilder(for: state) + } + } +} diff --git a/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmViewLayout.swift b/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmViewLayout.swift new file mode 100644 index 0000000000..b7b58b318f --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmViewLayout.swift @@ -0,0 +1,38 @@ +import UIKit + +final class StartStakingConfirmViewLayout: SCLoadableActionLayoutView { + let amountView = MultilineBalanceView() + + let senderTableView = StackTableView() + + let walletCell = StackTableCell() + + let accountCell: StackInfoTableCell = .create { + $0.detailsLabel.lineBreakMode = .byTruncatingMiddle + } + + let feeCell = StackNetworkFeeCell() + + let stakingTableView = StackTableView() + let stakingTypeCell = StackTableCell() + + let stakingDetailsCell: StackInfoTableCell = .create { + $0.detailsLabel.lineBreakMode = .byTruncatingMiddle + $0.iconImageView.contentMode = .scaleAspectFit + } + + override func setupLayout() { + super.setupLayout() + + addArrangedSubview(amountView, spacingAfter: 24) + + addArrangedSubview(senderTableView, spacingAfter: 8) + senderTableView.addArrangedSubview(walletCell) + senderTableView.addArrangedSubview(accountCell) + senderTableView.addArrangedSubview(feeCell) + + addArrangedSubview(stakingTableView) + stakingTableView.addArrangedSubview(stakingTypeCell) + stakingTableView.addArrangedSubview(stakingDetailsCell) + } +} diff --git a/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmWireframe.swift b/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmWireframe.swift new file mode 100644 index 0000000000..8a683b2968 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingConfirm/StartStakingConfirmWireframe.swift @@ -0,0 +1,3 @@ +import Foundation + +class StartStakingConfirmWireframe: StartStakingConfirmWireframeProtocol, ModalAlertPresenting {} diff --git a/novawallet/Modules/Staking/StartStakingInfo/Model/AccountExistense.swift b/novawallet/Modules/Staking/StartStakingInfo/Model/AccountExistense.swift new file mode 100644 index 0000000000..1a783238ef --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/Model/AccountExistense.swift @@ -0,0 +1,4 @@ +enum AccountExistense { + case assetBalance(AssetBalance) + case noAccount +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/Model/DefaultStakingRewardDestination.swift b/novawallet/Modules/Staking/StartStakingInfo/Model/DefaultStakingRewardDestination.swift new file mode 100644 index 0000000000..627f42305b --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/Model/DefaultStakingRewardDestination.swift @@ -0,0 +1,5 @@ +enum DefaultStakingRewardDestination { + case balance + case stake + case manual +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/Model/StartStakingStateProtocol.swift b/novawallet/Modules/Staking/StartStakingInfo/Model/StartStakingStateProtocol.swift new file mode 100644 index 0000000000..77bf8f4365 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/Model/StartStakingStateProtocol.swift @@ -0,0 +1,13 @@ +import BigInt + +protocol StartStakingStateProtocol { + var minStake: BigUInt? { get } + var eraDuration: TimeInterval? { get } + var unstakingTime: TimeInterval? { get } + var nextEraStartTime: TimeInterval? { get } + var maxApy: Decimal? { get } + var rewardsAutoPayoutThresholdAmount: BigUInt? { get } + var govThresholdAmount: BigUInt? { get } + var shouldHaveGovInfo: Bool { get } + var rewardsDestination: DefaultStakingRewardDestination { get } +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/Model/StartStakingViewModel.swift b/novawallet/Modules/Staking/StartStakingInfo/Model/StartStakingViewModel.swift new file mode 100644 index 0000000000..a67080acf3 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/Model/StartStakingViewModel.swift @@ -0,0 +1,14 @@ +import Foundation + +struct StartStakingViewModel { + let title: AccentTextModel + let paragraphs: [ParagraphView.Model] + let wikiUrl: StartStakingUrlModel + let termsUrl: StartStakingUrlModel +} + +struct StartStakingUrlModel { + let text: String + let url: URL + let urlName: String +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/Model/StartStakingViewModelFactory.swift b/novawallet/Modules/Staking/StartStakingInfo/Model/StartStakingViewModelFactory.swift new file mode 100644 index 0000000000..df314a6100 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/Model/StartStakingViewModelFactory.swift @@ -0,0 +1,315 @@ +import Foundation +import BigInt +import SoraFoundation + +protocol StartStakingViewModelFactoryProtocol { + func earnupModel( + earnings: Decimal?, + chainAsset: ChainAsset, + locale: Locale + ) -> AccentTextModel + func stakeModel( + minStake: BigUInt?, + nextEra: TimeInterval, + chainAsset: ChainAsset, + locale: Locale + ) -> ParagraphView.Model + func unstakeModel(unstakePeriod: TimeInterval, locale: Locale) -> ParagraphView.Model + func rewardModel( + amount: BigUInt?, + chainAsset: ChainAsset, + eraDuration: TimeInterval, + destination: DefaultStakingRewardDestination, + locale: Locale + ) -> ParagraphView.Model + func govModel( + amount: BigUInt?, + chainAsset: ChainAsset, + locale: Locale + ) -> ParagraphView.Model + func recommendationModel(locale: Locale) -> ParagraphView.Model + func testNetworkModel( + chain: ChainModel, + locale: Locale + ) -> ParagraphView.Model + func wikiModel(url: URL, chain: ChainModel, locale: Locale) -> StartStakingUrlModel + func termsModel(url: URL, locale: Locale) -> StartStakingUrlModel + func balance(amount: BigUInt?, priceData: PriceData?, chainAsset: ChainAsset, locale: Locale) -> String + func noAccount(chain: ChainModel, locale: Locale) -> String +} + +struct StartStakingViewModelFactory: StartStakingViewModelFactoryProtocol { + let balanceViewModelFactory: BalanceViewModelFactoryProtocol + let estimatedEarningsFormatter: LocalizableResource + + init( + balanceViewModelFactory: BalanceViewModelFactoryProtocol, + estimatedEarningsFormatter: LocalizableResource + ) { + self.balanceViewModelFactory = balanceViewModelFactory + self.estimatedEarningsFormatter = estimatedEarningsFormatter + } + + func earnupModel( + earnings: Decimal?, + chainAsset: ChainAsset, + locale: Locale + ) -> AccentTextModel { + let amount = earnings.map { estimatedEarningsFormatter.value(for: locale).stringFromDecimal($0) } ?? "" + let token = chainAsset.asset.displayInfo.symbol + let value = R.string.localizable.stakingStartEarnUp(amount ?? "", preferredLanguages: locale.rLanguages) + let text = R.string.localizable.stakingStartEarnUpTitle( + value, + token, + preferredLanguages: locale.rLanguages + ) + + let textWithAccents = AccentTextModel( + text: text, + accents: [value] + ) + return textWithAccents + } + + func stakeModel( + minStake: BigUInt?, + nextEra: TimeInterval, + chainAsset: ChainAsset, + locale: Locale + ) -> ParagraphView.Model { + let separator = R.string.localizable.commonAnd(preferredLanguages: locale.rLanguages) + let timePreposition = R.string.localizable.commonTimeIn(preferredLanguages: locale.rLanguages) + let time = nextEra.localizedDaysHours( + for: locale, + preposition: timePreposition, + separator: separator, + roundsDown: false + ) + + let precision = chainAsset.assetDisplayInfo.assetPrecision + let textWithAccents: AccentTextModel + + if + let minStake = minStake, + minStake > 0, + let amountDecimal = Decimal.fromSubstrateAmount(minStake, precision: precision) { + let amount = balanceViewModelFactory.amountFromValue(amountDecimal).value(for: locale) + let text = R.string.localizable.stakingStartStake(amount, time, preferredLanguages: locale.rLanguages) + textWithAccents = AccentTextModel( + text: text, + accents: [amount, time] + ) + } else { + let text = R.string.localizable.stakingStartStakeWithoutMinimumStake( + time, + preferredLanguages: locale.rLanguages + ) + textWithAccents = AccentTextModel( + text: text, + accents: [time] + ) + } + + return .init( + image: R.image.coin(), + text: textWithAccents + ) + } + + func unstakeModel( + unstakePeriod: TimeInterval, + locale: Locale + ) -> ParagraphView.Model { + let separator = R.string.localizable.commonAnd(preferredLanguages: locale.rLanguages) + let preposition = R.string.localizable.commonTimePeriodAfter(preferredLanguages: locale.rLanguages) + let unstakePeriodString = unstakePeriod.localizedDaysHours( + for: locale, + preposition: preposition, + separator: separator, + roundsDown: false + ) + + let text = R.string.localizable.stakingStartUnstake(unstakePeriodString, preferredLanguages: locale.rLanguages) + let textWithAccents = AccentTextModel( + text: text, + accents: [unstakePeriodString] + ) + return .init( + image: R.image.clock(), + text: textWithAccents + ) + } + + func rewardModel( + amount: BigUInt?, + chainAsset: ChainAsset, + eraDuration: TimeInterval, + destination: DefaultStakingRewardDestination, + locale: Locale + ) -> ParagraphView.Model { + let separator = R.string.localizable.commonAnd(preferredLanguages: locale.rLanguages) + let preposition = R.string.localizable.commonTimePeriodEvery(preferredLanguages: locale.rLanguages) + let rewardIntervals = eraDuration.localizedDaysHours( + for: locale, + preposition: preposition, + separator: separator, + shortcutHandler: EverydayShortcut(), + roundsDown: false + ) + + let text: String + + if let amount = amount { + let decimalAmount = Decimal.fromSubstrateAmount( + amount, + precision: Int16(chainAsset.asset.precision) + ) ?? 0.0 + let formattedAmount = balanceViewModelFactory.amountFromValue(decimalAmount).value(for: locale) + text = R.string.localizable.stakingStartRewardsDirectStaking( + rewardIntervals, + formattedAmount, + preferredLanguages: locale.rLanguages + ) + } else { + switch destination { + case .balance: + text = R.string.localizable.stakingStartRewardsBalance( + rewardIntervals, + preferredLanguages: locale.rLanguages + ) + case .stake: + text = R.string.localizable.stakingStartRewardsRestake( + rewardIntervals, + preferredLanguages: locale.rLanguages + ) + case .manual: + text = R.string.localizable.stakingStartRewardsManualClaim( + rewardIntervals, + preferredLanguages: locale.rLanguages + ) + } + } + + let textWithAccents = AccentTextModel( + text: text, + accents: [rewardIntervals] + ) + return .init( + image: R.image.cup(), + text: textWithAccents + ) + } + + func govModel( + amount: BigUInt?, + chainAsset: ChainAsset, + locale: Locale + ) -> ParagraphView.Model { + let action: String + let text: String + + if let amount = amount { + let decimalAmount = Decimal.fromSubstrateAmount( + amount, + precision: Int16(chainAsset.asset.precision) + ) ?? 0.0 + let formattedAmount = balanceViewModelFactory.amountFromValue(decimalAmount).value(for: locale) + action = R.string.localizable.stakingStartGovNominationDirectStakingAction( + preferredLanguages: locale.rLanguages + ) + text = R.string.localizable.stakingStartGovDirectStaking( + formattedAmount, + action, + preferredLanguages: locale.rLanguages + ) + } else { + action = R.string.localizable.stakingStartGovNominationPoolAction(preferredLanguages: locale.rLanguages) + text = R.string.localizable.stakingStartGovNominationPool(action, preferredLanguages: locale.rLanguages) + } + + let textWithAccents = AccentTextModel( + text: text, + accents: [action] + ) + return .init( + image: R.image.speaker(), + text: textWithAccents + ) + } + + func recommendationModel(locale: Locale) -> ParagraphView.Model { + let action = R.string.localizable.stakingStartChangesAction(preferredLanguages: locale.rLanguages) + let text = R.string.localizable.stakingStartChanges(action, preferredLanguages: locale.rLanguages) + let textWithAccents = AccentTextModel( + text: text, + accents: [action] + ) + return .init( + image: R.image.ring(), + text: textWithAccents + ) + } + + func testNetworkModel( + chain: ChainModel, + locale: Locale + ) -> ParagraphView.Model { + let description = R.string.localizable.stakingStartTestNetworkDescription(preferredLanguages: locale.rLanguages) + let value = R.string.localizable.stakingStartTestNetworkTokenValue(preferredLanguages: locale.rLanguages) + let text = R.string.localizable.stakingStartTestNetwork( + chain.name, + description, + value, + preferredLanguages: locale.rLanguages + ) + let textWithAccents = AccentTextModel( + text: text, + accents: [description, value] + ) + + return .init(image: R.image.system(), text: textWithAccents) + } + + func wikiModel( + url: URL, + chain: ChainModel, + locale: Locale + ) -> StartStakingUrlModel { + let linkName = R.string.localizable.stakingStartWikiLink(preferredLanguages: locale.rLanguages) + let text = R.string.localizable.stakingStartWiki(chain.name, linkName, preferredLanguages: locale.rLanguages) + + return .init(text: text, url: url, urlName: linkName) + } + + func termsModel(url: URL, locale: Locale) -> StartStakingUrlModel { + let linkName = R.string.localizable.stakingStartTermsLink(preferredLanguages: locale.rLanguages) + let text = R.string.localizable.stakingStartTerms(linkName, preferredLanguages: locale.rLanguages) + + return .init(text: text, url: url, urlName: linkName) + } + + func balance(amount: BigUInt?, priceData: PriceData?, chainAsset: ChainAsset, locale: Locale) -> String { + let balance = balanceViewModelFactory.balanceWithPriceIfPossible( + amount: amount, + priceData: priceData, + chainAsset: chainAsset + ).value(for: locale) + + if let price = balance.price { + return R.string.localizable.stakingStartBalanceWithFiat( + balance.amount, + price, + preferredLanguages: locale.rLanguages + ) + } else { + return R.string.localizable.stakingStartBalance( + balance.amount, + preferredLanguages: locale.rLanguages + ) + } + } + + func noAccount(chain: ChainModel, locale: Locale) -> String { + R.string.localizable.stakingStartNoAccount(chain.name, preferredLanguages: locale.rLanguages) + } +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/ParachainStaking/StartStakingInfoParachainPresenter.swift b/novawallet/Modules/Staking/StartStakingInfo/ParachainStaking/StartStakingInfoParachainPresenter.swift new file mode 100644 index 0000000000..fd144b5a4d --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/ParachainStaking/StartStakingInfoParachainPresenter.swift @@ -0,0 +1,182 @@ +import SoraFoundation +import BigInt + +final class StartStakingInfoParachainPresenter: StartStakingInfoBasePresenter { + let interactor: StartStakingInfoParachainInteractorInputProtocol + + private var state: State { + didSet { + if state != oldValue { + provideViewModel(state: state) + } + } + } + + init( + chainAsset: ChainAsset, + interactor: StartStakingInfoParachainInteractorInputProtocol, + wireframe: StartStakingInfoWireframeProtocol, + startStakingViewModelFactory: StartStakingViewModelFactoryProtocol, + balanceDerivationFactory: StakingTypeBalanceFactoryProtocol, + localizationManager: LocalizationManagerProtocol, + applicationConfig: ApplicationConfigProtocol, + logger: LoggerProtocol + ) { + state = .init(chainAsset: chainAsset) + self.interactor = interactor + + super.init( + chainAsset: chainAsset, + interactor: interactor, + wireframe: wireframe, + startStakingViewModelFactory: startStakingViewModelFactory, + balanceDerivationFactory: balanceDerivationFactory, + localizationManager: localizationManager, + applicationConfig: applicationConfig, + logger: logger + ) + } + + override func setup() { + super.setup() + view?.didReceive(viewModel: .loading) + } +} + +extension StartStakingInfoParachainPresenter: StartStakingInfoParachainInteractorOutputProtocol { + func didReceive(networkInfo: ParachainStaking.NetworkInfo?) { + state.networkInfo = networkInfo + } + + func didReceive(error: ParachainStartStakingInfoError) { + logger.error("Did receive error: \(error)") + + switch error { + case .networkInfo: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryNetworkStakingInfo() + } + case .createState: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.setup() + } + case .calculator: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.remakeCalculator() + } + case .stakingDuration: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryStakingDuration() + } + case .rewardPaymentDelay: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryRewardPaymentDelay() + } + case .blockNumber, .parastakingRound: + break + } + } + + func didReceive(parastakingRound roundInfo: ParachainStaking.RoundInfo?) { + state.roundInfo = roundInfo + } + + func didReceive(calculator: ParaStakingRewardCalculatorEngineProtocol) { + state.maxApy = calculator.calculateMaxEarnings(amount: 1, period: .year) + } + + func didReceive(blockNumber: BlockNumber?) { + state.update(blockNumber: blockNumber) + } + + func didReceive(stakingDuration: ParachainStakingDuration) { + if shouldUpdateEraDuration( + for: stakingDuration.round, + oldValue: state.stakingDuration?.round + ) { + state.stakingDuration = stakingDuration + } + } + + func didReceive(rewardPaymentDelay: UInt32) { + state.rewardPaymentDelay = rewardPaymentDelay + } +} + +extension StartStakingInfoParachainPresenter { + struct State: StartStakingStateProtocol, Equatable { + let chainAsset: ChainAsset + + var networkInfo: ParachainStaking.NetworkInfo? + var roundInfo: ParachainStaking.RoundInfo? + var maxApy: Decimal? + private(set) var blockNumber: BlockNumber? + var stakingDuration: ParachainStakingDuration? + var rewardPaymentDelay: UInt32? + var rewardsDestination: DefaultStakingRewardDestination { .balance } + + var minStake: BigUInt? { + guard let networkInfo = networkInfo else { + return nil + } + + return max(networkInfo.minStakeForRewards, networkInfo.minTechStake) + } + + var govThresholdAmount: BigUInt? { nil } + + var shouldHaveGovInfo: Bool { + chainAsset.chain.hasGovernance + } + + var rewardsAutoPayoutThresholdAmount: BigUInt? { nil } + + var nextEraStartTime: TimeInterval? { + guard let roundCountdown = roundCountdown, + let roundInfo = roundInfo, + let rewardPaymentDelay = rewardPaymentDelay else { + return nil + } + + return roundCountdown.timeIntervalTillStart(targetEra: roundInfo.current + 1 + rewardPaymentDelay) + } + + var roundCountdown: RoundCountdown? { + guard let blockNumber = blockNumber, + let roundInfo = roundInfo, + let stakingDuration = stakingDuration else { + return nil + } + return RoundCountdown( + roundInfo: roundInfo, + blockTime: stakingDuration.block, + currentBlock: blockNumber, + createdAtDate: Date() + ) + } + + var unstakingTime: TimeInterval? { + guard let stakingDuration = stakingDuration else { + return nil + } + + return stakingDuration.unstaking + } + + var eraDuration: TimeInterval? { + guard let stakingDuration = stakingDuration else { + return nil + } + + return stakingDuration.round + } + + mutating func update(blockNumber: BlockNumber?) { + guard let blockNumber = blockNumber, self.blockNumber == nil else { + return + } + + self.blockNumber = blockNumber + } + } +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/ParachainStaking/StartStakingInfoParachainWireframe.swift b/novawallet/Modules/Staking/StartStakingInfo/ParachainStaking/StartStakingInfoParachainWireframe.swift new file mode 100644 index 0000000000..f7eb79e58e --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/ParachainStaking/StartStakingInfoParachainWireframe.swift @@ -0,0 +1,23 @@ +final class StartStakingInfoParachainWireframe: StartStakingInfoWireframe { + let state: ParachainStakingSharedStateProtocol + + init(state: ParachainStakingSharedStateProtocol) { + self.state = state + } + + override func showSetupAmount(from view: ControllerBackedProtocol?) { + guard let stakeView = ParaStkStakeSetupViewFactory.createView( + with: state, + initialDelegator: nil, + initialScheduledRequests: nil, + delegationIdentities: nil + ) else { + return + } + + view?.controller.navigationController?.pushViewController( + stakeView.controller, + animated: true + ) + } +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/ParachainStaking/StartStakingParachainInteractor.swift b/novawallet/Modules/Staking/StartStakingInfo/ParachainStaking/StartStakingParachainInteractor.swift new file mode 100644 index 0000000000..18a995cea9 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/ParachainStaking/StartStakingParachainInteractor.swift @@ -0,0 +1,296 @@ +import RobinHood +import Foundation + +final class StartStakingParachainInteractor: StartStakingInfoBaseInteractor, AnyCancellableCleaning, + RuntimeConstantFetching { + var chainRegistry: ChainRegistryProtocol { state.chainRegistry } + + let state: ParachainStakingSharedStateProtocol + let networkInfoFactory: ParaStkNetworkInfoOperationFactoryProtocol + let eventCenter: EventCenterProtocol + let durationOperationFactory: ParaStkDurationOperationFactoryProtocol + + var generalLocalSubscriptionFactory: GeneralStorageSubscriptionFactoryProtocol { + state.generalLocalSubscriptionFactory + } + + var stakingLocalSubscriptionFactory: ParachainStakingLocalSubscriptionFactoryProtocol { + state.stakingLocalSubscriptionFactory + } + + private var roundInfoProvider: AnyDataProvider? + private var blockNumberProvider: AnyDataProvider? + private var networkInfoCancellable: CancellableCall? + private var rewardCalculatorCancellable: CancellableCall? + private var durationCancellable: CancellableCall? + private var rewardPaymentDelayCancellable: CancellableCall? + + weak var presenter: StartStakingInfoParachainInteractorOutputProtocol? { + didSet { + basePresenter = presenter + } + } + + init( + state: ParachainStakingSharedStateProtocol, + selectedWalletSettings: SelectedWalletSettings, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + currencyManager: CurrencyManagerProtocol, + networkInfoFactory: ParaStkNetworkInfoOperationFactoryProtocol, + durationOperationFactory: ParaStkDurationOperationFactoryProtocol, + operationQueue: OperationQueue, + eventCenter: EventCenterProtocol + ) { + self.state = state + self.networkInfoFactory = networkInfoFactory + self.eventCenter = eventCenter + self.durationOperationFactory = durationOperationFactory + + super.init( + selectedWalletSettings: selectedWalletSettings, + selectedChainAsset: state.stakingOption.chainAsset, + walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, + priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, + currencyManager: currencyManager, + operationQueue: operationQueue + ) + } + + deinit { + state.throttle() + + clear(cancellable: &networkInfoCancellable) + clear(cancellable: &rewardCalculatorCancellable) + clear(cancellable: &durationCancellable) + clear(cancellable: &rewardPaymentDelayCancellable) + } + + private func provideNetworkInfo() { + clear(cancellable: &networkInfoCancellable) + let chainId = selectedChainAsset.chain.chainId + + guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + presenter?.didReceive(error: .networkInfo(ChainRegistryError.runtimeMetadaUnavailable)) + return + } + + let collatorService = state.collatorService + let rewardService = state.rewardCalculationService + + let wrapper = networkInfoFactory.networkStakingOperation( + for: collatorService, + rewardCalculatorService: rewardService, + runtimeService: runtimeService + ) + + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard self?.networkInfoCancellable === wrapper else { + return + } + + do { + let info = try wrapper.targetOperation.extractNoCancellableResultData() + self?.presenter?.didReceive(networkInfo: info) + } catch { + self?.presenter?.didReceive(error: .networkInfo(error)) + } + + self?.networkInfoCancellable = nil + } + } + + networkInfoCancellable = wrapper + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } + + func provideRewardCalculator() { + clear(cancellable: &rewardCalculatorCancellable) + + let calculatorService = state.rewardCalculationService + + let operation = calculatorService.fetchCalculatorOperation() + + operation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard self?.rewardCalculatorCancellable === operation else { + return + } + + self?.rewardCalculatorCancellable = nil + + do { + let engine = try operation.extractNoCancellableResultData() + self?.presenter?.didReceive(calculator: engine) + } catch { + self?.presenter?.didReceive(error: .calculator(error)) + } + } + } + + rewardCalculatorCancellable = operation + + operationQueue.addOperation(operation) + } + + private func provideStakingDurationInfo() { + clear(cancellable: &durationCancellable) + + let blockTimeService = state.blockTimeService + + let chainId = selectedChainAsset.chain.chainId + + guard + let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + presenter?.didReceive(error: .stakingDuration(ChainRegistryError.runtimeMetadaUnavailable)) + return + } + + guard let connection = chainRegistry.getConnection(for: chainId) else { + presenter?.didReceive(error: .stakingDuration(ChainRegistryError.connectionUnavailable)) + return + } + + let wrapper = durationOperationFactory.createDurationOperation( + from: runtimeService, + connection: connection, + blockTimeEstimationService: blockTimeService + ) + + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard self?.durationCancellable === wrapper else { + return + } + + self?.durationCancellable = nil + + do { + let info = try wrapper.targetOperation.extractNoCancellableResultData() + self?.presenter?.didReceive(stakingDuration: info) + } catch { + self?.presenter?.didReceive(error: .stakingDuration(error)) + } + } + } + + durationCancellable = wrapper + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } + + private func provideRewardPaymentDelay() { + clear(cancellable: &rewardPaymentDelayCancellable) + + let chainId = selectedChainAsset.chain.chainId + + guard + let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + presenter?.didReceive(error: .rewardPaymentDelay(ChainRegistryError.runtimeMetadaUnavailable)) + return + } + + rewardPaymentDelayCancellable = fetchConstant( + for: ParachainStaking.rewardPaymentDelay, + runtimeCodingService: runtimeService, + operationManager: OperationManager(operationQueue: operationQueue) + ) { [weak self] (result: Result) in + DispatchQueue.main.async { + switch result { + case let .success(value): + self?.presenter?.didReceive(rewardPaymentDelay: value) + case let .failure(error): + self?.presenter?.didReceive(error: .rewardPaymentDelay(error)) + } + } + } + } + + private func performRoundInfoSubscription() { + let chainId = selectedChainAsset.chain.chainId + roundInfoProvider = subscribeToRound(for: chainId) + } + + private func performBlockNumberSubscription() { + let chainId = selectedChainAsset.chain.chainId + blockNumberProvider = subscribeToBlockNumber(for: chainId) + } + + override func setup() { + super.setup() + + state.setup(for: selectedAccount?.chainAccount.accountId) + eventCenter.add(observer: self, dispatchIn: .main) + + provideNetworkInfo() + provideRewardCalculator() + provideStakingDurationInfo() + provideRewardPaymentDelay() + performRoundInfoSubscription() + performBlockNumberSubscription() + } +} + +extension StartStakingParachainInteractor: EventVisitorProtocol { + func processEraStakersInfoChanged(event _: EraStakersInfoChanged) { + provideNetworkInfo() + } + + func processBlockTimeChanged(event _: BlockTimeChanged) { + provideStakingDurationInfo() + } +} + +extension StartStakingParachainInteractor: ParastakingLocalStorageSubscriber, + ParastakingLocalStorageHandler { + func handleParastakingRound(result: Result, for chainId: ChainModel.Id) { + guard selectedChainAsset.chain.chainId == chainId else { + return + } + + switch result { + case let .success(roundInfo): + presenter?.didReceive(parastakingRound: roundInfo) + case let .failure(error): + presenter?.didReceive(error: .parastakingRound(error)) + } + } +} + +extension StartStakingParachainInteractor: GeneralLocalStorageSubscriber, GeneralLocalStorageHandler { + func handleBlockNumber( + result: Result, + chainId: ChainModel.Id + ) { + guard selectedChainAsset.chain.chainId == chainId else { + return + } + + switch result { + case let .success(blockNumber): + presenter?.didReceive(blockNumber: blockNumber) + case let .failure(error): + presenter?.didReceive(error: .blockNumber(error)) + } + } +} + +extension StartStakingParachainInteractor: StartStakingInfoParachainInteractorInputProtocol { + func retryNetworkStakingInfo() { + provideNetworkInfo() + } + + func remakeCalculator() { + provideRewardCalculator() + } + + func retryStakingDuration() { + provideStakingDurationInfo() + } + + func retryRewardPaymentDelay() { + provideRewardPaymentDelay() + } +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/RelaychainStaking/StartStakingInfoRelaychainPresenter.swift b/novawallet/Modules/Staking/StartStakingInfo/RelaychainStaking/StartStakingInfoRelaychainPresenter.swift new file mode 100644 index 0000000000..2317b56bf2 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/RelaychainStaking/StartStakingInfoRelaychainPresenter.swift @@ -0,0 +1,188 @@ +import Foundation +import SoraFoundation +import BigInt + +final class StartStakingInfoRelaychainPresenter: StartStakingInfoBasePresenter { + let interactor: StartStakingInfoRelaychainInteractorInputProtocol + + private var state: State { + didSet { + provideViewModel(state: state) + } + } + + init( + selectedStakingType: StakingType?, + chainAsset: ChainAsset, + interactor: StartStakingInfoRelaychainInteractorInputProtocol, + wireframe: StartStakingInfoWireframeProtocol, + startStakingViewModelFactory: StartStakingViewModelFactoryProtocol, + balanceDerivationFactory: StakingTypeBalanceFactoryProtocol, + localizationManager: LocalizationManagerProtocol, + applicationConfig: ApplicationConfigProtocol, + logger: LoggerProtocol + ) { + state = .init(stakingType: selectedStakingType, chainAsset: chainAsset) + self.interactor = interactor + + super.init( + chainAsset: chainAsset, + interactor: interactor, + wireframe: wireframe, + startStakingViewModelFactory: startStakingViewModelFactory, + balanceDerivationFactory: balanceDerivationFactory, + localizationManager: localizationManager, + applicationConfig: applicationConfig, + logger: logger + ) + } + + override func setup() { + super.setup() + + view?.didReceive(viewModel: .loading) + } +} + +extension StartStakingInfoRelaychainPresenter: StartStakingInfoRelaychainInteractorOutputProtocol { + func didReceive(eraCountdown: EraCountdown?) { + if shouldUpdateEraDuration( + for: eraCountdown?.eraTimeInterval, + oldValue: state.eraCountdown?.eraTimeInterval + ) { + state.eraCountdown = eraCountdown + } + } + + func didReceive(networkInfo: NetworkStakingInfo) { + state.networkInfo = networkInfo + } + + func didReceive(directStakingMinStake: BigUInt) { + state.directStakingMinimumStake = directStakingMinStake + } + + func didReceive(nominationPoolMinStake: BigUInt?) { + state.nominationPoolMinimumStake = nominationPoolMinStake + } + + func didReceive(error: RelaychainStartStakingInfoError) { + logger.error("Did receive error: \(error)") + + switch error { + case .directStakingMinStake: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryDirectStakingMinStake() + } + case .createState: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.setup() + } + case .eraCountdown: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryEraCompletionTime() + } + case .nominationPoolsMinStake: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryNominationPoolsMinStake() + } + case .calculator: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.remakeCalculator() + } + } + } + + func didReceive(calculator: RewardCalculatorEngineProtocol) { + state.maxApy = calculator.calculateMaxEarnings(amount: 1, isCompound: true, period: .year) + } +} + +extension StartStakingInfoRelaychainPresenter { + struct State: StartStakingStateProtocol { + let stakingType: StakingType? + let chainAsset: ChainAsset + + var networkInfo: NetworkStakingInfo? + var eraCountdown: EraCountdown? + var maxApy: Decimal? + var nominationPoolMinimumStake: BigUInt? + var directStakingMinimumStake: BigUInt? + + var rewardsDestination: DefaultStakingRewardDestination { + switch stakingType { + case .nominationPools: + return .manual + default: + return .stake + } + } + + var minStake: BigUInt? { + let stakingClass = stakingType.map { StakingClass(stakingType: $0) } + + switch stakingClass { + case .relaychain: + return directStakingMinimumStake + case .nominationPools: + return nominationPoolMinimumStake + default: + if let nominationPoolMinimumStake = nominationPoolMinimumStake, + let directStakingMinimumStake = directStakingMinimumStake { + return min(nominationPoolMinimumStake, directStakingMinimumStake) + } else { + return directStakingMinimumStake + } + } + } + + var nextEraStartTime: TimeInterval? { + guard let eraCountdown = eraCountdown else { + return nil + } + + return eraCountdown.timeIntervalTillStart(targetEra: eraCountdown.currentEra + 2) + } + + var eraDuration: TimeInterval? { + guard let eraCountdown = eraCountdown else { + return nil + } + + return eraCountdown.eraTimeInterval + } + + var rewardsAutoPayoutThresholdAmount: BigUInt? { + guard + stakingType == nil, + nominationPoolMinimumStake != nil, + let directStakingMinimumStake = directStakingMinimumStake, + let minStake = minStake else { + return nil + } + + return directStakingMinimumStake <= minStake ? nil : directStakingMinimumStake + } + + var govThresholdAmount: BigUInt? { + rewardsAutoPayoutThresholdAmount + } + + var shouldHaveGovInfo: Bool { + switch stakingType { + case .nominationPools: + return false + default: + return chainAsset.chain.hasGovernance + } + } + + var unstakingTime: TimeInterval? { + guard let networkInfo = networkInfo else { + return nil + } + + return networkInfo.stakingDuration.unlocking + } + } +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/RelaychainStaking/StartStakingInfoRelaychainWireframe.swift b/novawallet/Modules/Staking/StartStakingInfo/RelaychainStaking/StartStakingInfoRelaychainWireframe.swift new file mode 100644 index 0000000000..a63ffe3ab3 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/RelaychainStaking/StartStakingInfoRelaychainWireframe.swift @@ -0,0 +1,15 @@ +final class StartStakingInfoRelaychainWireframe: StartStakingInfoWireframe { + let state: RelaychainStartStakingStateProtocol + + init(state: RelaychainStartStakingStateProtocol) { + self.state = state + } + + override func showSetupAmount(from view: ControllerBackedProtocol?) { + guard let setupAmountView = StakingSetupAmountViewFactory.createView(for: state) else { + return + } + + view?.controller.navigationController?.pushViewController(setupAmountView.controller, animated: true) + } +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/RelaychainStaking/StartStakingRelaychainInteractor.swift b/novawallet/Modules/Staking/StartStakingInfo/RelaychainStaking/StartStakingRelaychainInteractor.swift new file mode 100644 index 0000000000..7f804368c1 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/RelaychainStaking/StartStakingRelaychainInteractor.swift @@ -0,0 +1,290 @@ +import RobinHood +import BigInt +import Foundation +import SubstrateSdk + +final class StartStakingRelaychainInteractor: StartStakingInfoBaseInteractor, AnyCancellableCleaning { + let state: RelaychainStartStakingStateProtocol + let chainRegistry: ChainRegistryProtocol + let networkInfoOperationFactory: NetworkStakingInfoOperationFactoryProtocol + let eraCoundownOperationFactory: EraCountdownOperationFactoryProtocol + let eventCenter: EventCenterProtocol + + var stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol { + state.relaychainLocalSubscriptionFactory + } + + var npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol { + state.npLocalSubscriptionFactory + } + + private var minNominatorBondProvider: AnyDataProvider? + private var bagListSizeProvider: AnyDataProvider? + private var minJoinBondProvider: AnyDataProvider? + private var eraCompletionTimeCancellable: CancellableCall? + private var networkInfoCancellable: CancellableCall? + private var rewardCalculatorCancellable: CancellableCall? + private var directStakingMinStakeBuilder: DirectStakingMinStakeBuilder? + + weak var presenter: StartStakingInfoRelaychainInteractorOutputProtocol? { + didSet { + basePresenter = presenter + } + } + + init( + state: RelaychainStartStakingStateProtocol, + selectedWalletSettings: SelectedWalletSettings, + chainRegistry: ChainRegistryProtocol, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + currencyManager: CurrencyManagerProtocol, + networkInfoOperationFactory: NetworkStakingInfoOperationFactoryProtocol, + eraCoundownOperationFactory: EraCountdownOperationFactoryProtocol, + eventCenter: EventCenterProtocol, + operationQueue: OperationQueue + ) { + self.state = state + self.chainRegistry = chainRegistry + self.networkInfoOperationFactory = networkInfoOperationFactory + self.eraCoundownOperationFactory = eraCoundownOperationFactory + self.eventCenter = eventCenter + + super.init( + selectedWalletSettings: selectedWalletSettings, + selectedChainAsset: state.chainAsset, + walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, + priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, + currencyManager: currencyManager, + operationQueue: operationQueue + ) + } + + deinit { + state.throttle() + + clear(cancellable: &networkInfoCancellable) + clear(cancellable: &eraCompletionTimeCancellable) + clear(cancellable: &rewardCalculatorCancellable) + } + + private func provideNetworkStakingInfo() { + clear(cancellable: &networkInfoCancellable) + + let chain = selectedChainAsset.chain + let chainId = chain.chainId + + guard + let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + presenter?.didReceive(error: .directStakingMinStake(ChainRegistryError.runtimeMetadaUnavailable)) + return + } + + let eraValidatorService = state.eraValidatorService + + let wrapper = networkInfoOperationFactory.networkStakingOperation( + for: eraValidatorService, + runtimeService: runtimeService + ) + + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard self?.networkInfoCancellable === wrapper else { + return + } + + self?.networkInfoCancellable = nil + + do { + let info = try wrapper.targetOperation.extractNoCancellableResultData() + self?.directStakingMinStakeBuilder?.apply(param1: info) + self?.presenter?.didReceive(networkInfo: info) + } catch { + self?.presenter?.didReceive(error: .directStakingMinStake(error)) + } + } + } + + networkInfoCancellable = wrapper + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } + + private func performMinNominatorBondSubscription() { + clear(dataProvider: &minNominatorBondProvider) + minNominatorBondProvider = subscribeToMinNominatorBond(for: selectedChainAsset.chain.chainId) + } + + private func performMinJoinBondSubscription() { + clear(dataProvider: &minJoinBondProvider) + + if state.supportsPoolStaking() { + minJoinBondProvider = subscribeMinJoinBond(for: selectedChainAsset.chain.chainId) + } else { + presenter?.didReceive(nominationPoolMinStake: nil) + } + } + + private func performBagListSizeSubscription() { + clear(dataProvider: &bagListSizeProvider) + bagListSizeProvider = subscribeBagsListSize(for: selectedChainAsset.chain.chainId) + } + + private func setupState() { + do { + let account = SelectedWalletSettings.shared.value.fetch(for: selectedChainAsset.chain.accountRequest()) + try state.setup(for: account?.accountId) + } catch { + presenter?.didReceive(error: .createState(error)) + } + } + + private func provideEraCompletionTime() { + clear(cancellable: &eraCompletionTimeCancellable) + + let chainId = selectedChainAsset.chain.chainId + + guard let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + presenter?.didReceive(error: .eraCountdown(ChainRegistryError.runtimeMetadaUnavailable)) + return + } + + guard let connection = chainRegistry.getConnection(for: chainId) else { + presenter?.didReceive(error: .eraCountdown(ChainRegistryError.connectionUnavailable)) + return + } + + let operationWrapper = eraCoundownOperationFactory.fetchCountdownOperationWrapper( + for: connection, + runtimeService: runtimeService + ) + + operationWrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard self?.eraCompletionTimeCancellable === operationWrapper else { + return + } + + self?.eraCompletionTimeCancellable = nil + + do { + let result = try operationWrapper.targetOperation.extractNoCancellableResultData() + self?.presenter?.didReceive(eraCountdown: result) + } catch { + self?.presenter?.didReceive(error: .eraCountdown(error)) + } + } + } + + eraCompletionTimeCancellable = operationWrapper + + operationQueue.addOperations(operationWrapper.allOperations, waitUntilFinished: false) + } + + private func provideRewardCalculator() { + clear(cancellable: &rewardCalculatorCancellable) + + let calculatorService = state.relaychainRewardCalculatorService + + let operation = calculatorService.fetchCalculatorOperation() + + operation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard self?.rewardCalculatorCancellable === operation else { + return + } + do { + let engine = try operation.extractNoCancellableResultData() + self?.presenter?.didReceive(calculator: engine) + } catch { + self?.presenter?.didReceive(error: .calculator(error)) + } + } + } + + rewardCalculatorCancellable = operation + + operationQueue.addOperation(operation) + } + + override func setup() { + super.setup() + + setupState() + + directStakingMinStakeBuilder = DirectStakingMinStakeBuilder { [weak self] minStake in + self?.presenter?.didReceive(directStakingMinStake: minStake) + } + + provideRewardCalculator() + provideNetworkStakingInfo() + performMinNominatorBondSubscription() + performBagListSizeSubscription() + performMinJoinBondSubscription() + provideEraCompletionTime() + + eventCenter.add(observer: self, dispatchIn: .main) + } +} + +extension StartStakingRelaychainInteractor: StakingLocalStorageSubscriber, StakingLocalSubscriptionHandler { + func handleMinNominatorBond(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(bond): + directStakingMinStakeBuilder?.apply(param3: bond) + case let .failure(error): + presenter?.didReceive(error: .directStakingMinStake(error)) + } + } + + func handleBagListSize(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(size): + directStakingMinStakeBuilder?.apply(param2: size) + case let .failure(error): + presenter?.didReceive(error: .directStakingMinStake(error)) + } + } +} + +extension StartStakingRelaychainInteractor: NPoolsLocalStorageSubscriber, NPoolsLocalSubscriptionHandler { + func handleMinJoinBond(result: Result, chainId _: ChainModel.Id) { + switch result { + case let .success(minJoinBond): + presenter?.didReceive(nominationPoolMinStake: minJoinBond) + case let .failure(error): + presenter?.didReceive(error: .nominationPoolsMinStake(error)) + } + } +} + +extension StartStakingRelaychainInteractor: StartStakingInfoRelaychainInteractorInputProtocol { + func retryDirectStakingMinStake() { + provideNetworkStakingInfo() + performMinNominatorBondSubscription() + performBagListSizeSubscription() + } + + func retryEraCompletionTime() { + provideEraCompletionTime() + } + + func remakeCalculator() { + provideRewardCalculator() + } + + func retryNominationPoolsMinStake() { + performMinJoinBondSubscription() + } +} + +extension StartStakingRelaychainInteractor: EventVisitorProtocol { + func processEraStakersInfoChanged(event _: EraStakersInfoChanged) { + provideNetworkStakingInfo() + } + + func processBlockTimeChanged(event _: BlockTimeChanged) { + provideNetworkStakingInfo() + provideEraCompletionTime() + } +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoBaseInteractor.swift b/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoBaseInteractor.swift new file mode 100644 index 0000000000..0a7d6adc87 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoBaseInteractor.swift @@ -0,0 +1,135 @@ +import UIKit +import RobinHood +import BigInt + +class StartStakingInfoBaseInteractor: StartStakingInfoInteractorInputProtocol, AnyProviderAutoCleaning { + weak var basePresenter: StartStakingInfoInteractorOutputProtocol? + let selectedChainAsset: ChainAsset + let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol + let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol + let selectedWalletSettings: SelectedWalletSettings + + private(set) var priceProvider: StreamableProvider? + private(set) var balanceProvider: StreamableProvider? + private(set) var selectedAccount: MetaChainAccountResponse? + private(set) var operationQueue: OperationQueue + + init( + selectedWalletSettings: SelectedWalletSettings, + selectedChainAsset: ChainAsset, + walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol, + priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, + currencyManager: CurrencyManagerProtocol, + operationQueue: OperationQueue + ) { + self.selectedWalletSettings = selectedWalletSettings + self.selectedChainAsset = selectedChainAsset + self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory + self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory + self.operationQueue = operationQueue + self.currencyManager = currencyManager + } + + private func performPriceSubscription() { + clear(streamableProvider: &priceProvider) + + guard let priceId = selectedChainAsset.asset.priceId else { + basePresenter?.didReceive(price: nil) + return + } + + priceProvider = subscribeToPrice(for: priceId, currency: selectedCurrency) + } + + private func performAssetBalanceSubscription() { + clear(streamableProvider: &balanceProvider) + + let chainAssetId = selectedChainAsset.chainAssetId + + guard let accountId = selectedAccount?.chainAccount.accountId else { + return + } + + balanceProvider = subscribeToAssetBalanceProvider( + for: accountId, + chainId: chainAssetId.chainId, + assetId: chainAssetId.assetId + ) + } + + private func setupSelectedAccount() { + guard let wallet = selectedWalletSettings.value else { + return + } + + selectedAccount = wallet.fetchMetaChainAccount( + for: selectedChainAsset.chain.accountRequest() + ) + + basePresenter?.didReceive(wallet: wallet, chainAccountId: selectedAccount?.chainAccount.accountId) + } + + func setup() { + setupSelectedAccount() + + performAssetBalanceSubscription() + performPriceSubscription() + } + + func remakeSubscriptions() { + performAssetBalanceSubscription() + performPriceSubscription() + } +} + +extension StartStakingInfoBaseInteractor: WalletLocalStorageSubscriber, + WalletLocalSubscriptionHandler { + func handleAssetBalance( + result: Result, + accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) { + guard + chainId == selectedChainAsset.chain.chainId, + assetId == selectedChainAsset.asset.assetId, + accountId == selectedAccount?.chainAccount.accountId else { + return + } + + switch result { + case let .success(balance): + let balance = balance ?? .createZero( + for: .init(chainId: chainId, assetId: assetId), + accountId: accountId + ) + basePresenter?.didReceive(assetBalance: balance) + case let .failure(error): + basePresenter?.didReceive(baseError: .assetBalance(error)) + } + } +} + +extension StartStakingInfoBaseInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { + func handlePrice(result: Result, priceId: AssetModel.PriceId) { + if selectedChainAsset.asset.priceId == priceId { + switch result { + case let .success(priceData): + basePresenter?.didReceive(price: priceData) + case let .failure(error): + basePresenter?.didReceive(baseError: .price(error)) + } + } + } +} + +extension StartStakingInfoBaseInteractor: SelectedCurrencyDepending { + func applyCurrency() { + guard basePresenter != nil, + let priceId = selectedChainAsset.asset.priceId else { + return + } + + priceProvider = subscribeToPrice(for: priceId, currency: selectedCurrency) + } +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoBasePresenter.swift b/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoBasePresenter.swift new file mode 100644 index 0000000000..bf7602ee7b --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoBasePresenter.swift @@ -0,0 +1,223 @@ +import Foundation +import SoraFoundation +import BigInt + +class StartStakingInfoBasePresenter: StartStakingInfoInteractorOutputProtocol, StartStakingInfoPresenterProtocol { + weak var view: StartStakingInfoViewProtocol? + let wireframe: StartStakingInfoWireframeProtocol + let baseInteractor: StartStakingInfoInteractorInputProtocol + let startStakingViewModelFactory: StartStakingViewModelFactoryProtocol + let balanceDerivationFactory: StakingTypeBalanceFactoryProtocol + let applicationConfig: ApplicationConfigProtocol + let chainAsset: ChainAsset + let logger: LoggerProtocol + + private(set) var price: PriceData? + private(set) var accountExistense: AccountExistense? + private var state: StartStakingStateProtocol? + private var wallet: MetaAccountModel? + + init( + chainAsset: ChainAsset, + interactor: StartStakingInfoInteractorInputProtocol, + wireframe: StartStakingInfoWireframeProtocol, + startStakingViewModelFactory: StartStakingViewModelFactoryProtocol, + balanceDerivationFactory: StakingTypeBalanceFactoryProtocol, + localizationManager: LocalizationManagerProtocol, + applicationConfig: ApplicationConfigProtocol, + logger: LoggerProtocol + ) { + self.chainAsset = chainAsset + baseInteractor = interactor + self.wireframe = wireframe + self.startStakingViewModelFactory = startStakingViewModelFactory + self.balanceDerivationFactory = balanceDerivationFactory + self.applicationConfig = applicationConfig + self.logger = logger + self.localizationManager = localizationManager + } + + func provideBalanceModel() { + guard let accountExistense = accountExistense else { + return + } + + switch accountExistense { + case let .assetBalance(balance): + guard let availableBalance = balanceDerivationFactory.getAvailableBalance(from: balance) else { + return + } + + let viewModel = startStakingViewModelFactory.balance( + amount: availableBalance, + priceData: price, + chainAsset: chainAsset, + locale: selectedLocale + ) + view?.didReceive(balance: viewModel) + case .noAccount: + let viewModel = startStakingViewModelFactory.noAccount(chain: chainAsset.chain, locale: selectedLocale) + view?.didReceive(balance: viewModel) + } + } + + func shouldUpdateEraDuration(for newValue: TimeInterval?, oldValue: TimeInterval?) -> Bool { + guard let oldValue = oldValue else { + return true + } + + guard let newValue = newValue else { + return false + } + + if newValue > oldValue { + return true + } else { + return oldValue - newValue > StartStakingInfoConstants.eraDurationReduceThreshold + } + } + + // swiftlint:disable:next function_body_length + func provideViewModel(state: StartStakingStateProtocol) { + self.state = state + + guard + let eraDuration = state.eraDuration, + let unstakingTime = state.unstakingTime, + let nextEraStartTime = state.nextEraStartTime, + let minStake = state.minStake, + let maxApy = state.maxApy else { + return + } + + let title = startStakingViewModelFactory.earnupModel( + earnings: maxApy, + chainAsset: chainAsset, + locale: selectedLocale + ) + let wikiUrl = startStakingViewModelFactory.wikiModel( + url: chainAsset.chain.stakingWiki ?? applicationConfig.websiteURL, + chain: chainAsset.chain, + locale: selectedLocale + ) + let termsUrl = startStakingViewModelFactory.termsModel( + url: applicationConfig.termsURL, + locale: selectedLocale + ) + let testnetModel = chainAsset.chain.isTestnet ? startStakingViewModelFactory.testNetworkModel( + chain: chainAsset.chain, + locale: selectedLocale + ) : nil + + let govModel = state.shouldHaveGovInfo ? startStakingViewModelFactory.govModel( + amount: state.govThresholdAmount, + chainAsset: chainAsset, + locale: selectedLocale + ) : nil + + let paragraphs = [ + testnetModel, + startStakingViewModelFactory.stakeModel( + minStake: minStake, + nextEra: nextEraStartTime, + chainAsset: chainAsset, + locale: selectedLocale + ), + startStakingViewModelFactory.unstakeModel(unstakePeriod: unstakingTime, locale: selectedLocale), + startStakingViewModelFactory.rewardModel( + amount: state.rewardsAutoPayoutThresholdAmount, + chainAsset: chainAsset, + eraDuration: eraDuration, + destination: state.rewardsDestination, + locale: selectedLocale + ), + govModel, + startStakingViewModelFactory.recommendationModel(locale: selectedLocale) + ].compactMap { $0 } + + let model = StartStakingViewModel( + title: title, + paragraphs: paragraphs, + wikiUrl: wikiUrl, + termsUrl: termsUrl + ) + + view?.didReceive(viewModel: .loaded(value: model)) + } + + // MARK: - StartStakingInfoInteractorOutputProtocol + + func didReceive(price: PriceData?) { + self.price = price + provideBalanceModel() + } + + func didReceive(assetBalance: AssetBalance) { + accountExistense = .assetBalance(assetBalance) + provideBalanceModel() + } + + func didReceive(wallet: MetaAccountModel, chainAccountId: AccountId?) { + self.wallet = wallet + if chainAccountId == nil { + accountExistense = .noAccount + provideBalanceModel() + } + } + + func didReceive(baseError error: BaseStartStakingInfoError) { + logger.error("Did receive error: \(error)") + + switch error { + case .assetBalance, .price: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.baseInteractor.remakeSubscriptions() + } + } + } + + // MARK: - StartStakingInfoPresenterProtocol + + func setup() { + baseInteractor.setup() + } + + func startStaking() { + guard let view = view, + let wallet = wallet, + let accountExistense = accountExistense else { + return + } + + switch accountExistense { + case .noAccount: + let message = R.string.localizable.commonChainAccountMissingMessageFormat( + chainAsset.chain.name, + preferredLanguages: selectedLocale.rLanguages + ) + + wireframe.presentAddAccount( + from: view, + chainName: chainAsset.chain.name, + message: message, + locale: selectedLocale + ) { [weak self] in + self?.wireframe.showWalletDetails( + from: view, + wallet: wallet + ) + } + case .assetBalance: + wireframe.showSetupAmount(from: view) + } + } +} + +extension StartStakingInfoBasePresenter: Localizable { + func applyLocalization() { + if view?.isSetup == true { + provideBalanceModel() + state.map(provideViewModel) + } + } +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoConstants.swift b/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoConstants.swift new file mode 100644 index 0000000000..6552770692 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoConstants.swift @@ -0,0 +1,5 @@ +import Foundation + +enum StartStakingInfoConstants { + static let eraDurationReduceThreshold = TimeInterval(10).secondsFromMinutes +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoProtocols.swift b/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoProtocols.swift new file mode 100644 index 0000000000..21a234eacf --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoProtocols.swift @@ -0,0 +1,84 @@ +import BigInt + +protocol StartStakingInfoViewProtocol: AnyObject, ControllerBackedProtocol { + func didReceive(viewModel: LoadableViewModelState) + func didReceive(balance: String) +} + +protocol StartStakingInfoPresenterProtocol: AnyObject { + func setup() + func startStaking() +} + +protocol StartStakingInfoInteractorInputProtocol: AnyObject { + func setup() + func remakeSubscriptions() +} + +protocol StartStakingInfoInteractorOutputProtocol: AnyObject { + func didReceive(price: PriceData?) + func didReceive(assetBalance: AssetBalance) + func didReceive(baseError: BaseStartStakingInfoError) + func didReceive(wallet: MetaAccountModel, chainAccountId: AccountId?) +} + +protocol StartStakingInfoRelaychainInteractorInputProtocol: StartStakingInfoInteractorInputProtocol { + func retryDirectStakingMinStake() + func retryEraCompletionTime() + func retryNominationPoolsMinStake() + func remakeCalculator() +} + +protocol StartStakingInfoRelaychainInteractorOutputProtocol: StartStakingInfoInteractorOutputProtocol { + func didReceive(networkInfo: NetworkStakingInfo) + func didReceive(directStakingMinStake: BigUInt) + func didReceive(nominationPoolMinStake: BigUInt?) + func didReceive(eraCountdown: EraCountdown?) + func didReceive(error: RelaychainStartStakingInfoError) + func didReceive(calculator: RewardCalculatorEngineProtocol) +} + +protocol StartStakingInfoParachainInteractorInputProtocol: StartStakingInfoInteractorInputProtocol { + func retryNetworkStakingInfo() + func remakeCalculator() + func retryStakingDuration() + func retryRewardPaymentDelay() +} + +protocol StartStakingInfoParachainInteractorOutputProtocol: StartStakingInfoInteractorOutputProtocol { + func didReceive(networkInfo: ParachainStaking.NetworkInfo?) + func didReceive(error: ParachainStartStakingInfoError) + func didReceive(parastakingRound: ParachainStaking.RoundInfo?) + func didReceive(calculator: ParaStakingRewardCalculatorEngineProtocol) + func didReceive(blockNumber: BlockNumber?) + func didReceive(stakingDuration: ParachainStakingDuration) + func didReceive(rewardPaymentDelay: UInt32) +} + +protocol StartStakingInfoWireframeProtocol: CommonRetryable, AlertPresentable, NoAccountSupportPresentable { + func showWalletDetails(from view: ControllerBackedProtocol?, wallet: MetaAccountModel) + func showSetupAmount(from view: ControllerBackedProtocol?) +} + +enum BaseStartStakingInfoError: Error { + case assetBalance(Error?) + case price(Error) +} + +enum RelaychainStartStakingInfoError: Error { + case createState(Error) + case eraCountdown(Error) + case directStakingMinStake(Error) + case nominationPoolsMinStake(Error) + case calculator(Error) +} + +enum ParachainStartStakingInfoError: Error { + case networkInfo(Error) + case createState(Error) + case parastakingRound(Error) + case calculator(Error) + case blockNumber(Error) + case stakingDuration(Error) + case rewardPaymentDelay(Error) +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoViewController.swift b/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoViewController.swift new file mode 100644 index 0000000000..165e4c5e0b --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoViewController.swift @@ -0,0 +1,119 @@ +import UIKit +import SoraFoundation + +final class StartStakingInfoViewController: UIViewController, ViewHolder { + typealias RootViewType = StartStakingInfoViewLayout + + let presenter: StartStakingInfoPresenterProtocol + private var viewModel: LoadableViewModelState? + private var balance = "" + + let themeColor: UIColor + + init( + presenter: StartStakingInfoPresenterProtocol, + localizationManager: LocalizationManagerProtocol, + themeColor: UIColor + ) { + self.presenter = presenter + self.themeColor = themeColor + + super.init(nibName: nil, bundle: nil) + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + let headerStyle = ParagraphView.Style.createHeaderStyle(for: themeColor) + let paragraphStyle = ParagraphView.Style.createParagraphStyle(for: themeColor) + + let layout = StartStakingInfoViewLayout( + headerStyle: headerStyle, + paragraphStyle: paragraphStyle + ) + + view = layout + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupLocalization() + setupHandlers() + presenter.setup() + } + + private func setupLocalization() { + rootView.updateBalanceButton( + text: balance, + locale: selectedLocale + ) + + guard let viewModel = viewModel?.value else { + return + } + + rootView.updateContent( + title: viewModel.title, + paragraphs: viewModel.paragraphs, + wikiUrl: viewModel.wikiUrl, + termsUrl: viewModel.termsUrl + ) + } + + private func setupHandlers() { + rootView.actionView.actionButton.addTarget(self, action: #selector(startStakingAction), for: .touchUpInside) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if viewModel?.isLoading == true { + rootView.updateLoadingState() + rootView.skeletonView?.restartSkrulling() + } + } + + @objc private func startStakingAction() { + presenter.startStaking() + } +} + +extension StartStakingInfoViewController: StartStakingInfoViewProtocol { + func didReceive(viewModel: LoadableViewModelState) { + switch viewModel { + case .loading: + rootView.startLoadingIfNeeded() + rootView.actionView.startLoading() + case let .cached(value), let .loaded(value): + rootView.stopLoadingIfNeeded() + rootView.actionView.stopLoading() + rootView.updateContent( + title: value.title, + paragraphs: value.paragraphs, + wikiUrl: value.wikiUrl, + termsUrl: value.termsUrl + ) + } + + self.viewModel = viewModel + } + + func didReceive(balance: String) { + self.balance = balance + rootView.updateBalanceButton(text: balance, locale: selectedLocale) + } +} + +extension StartStakingInfoViewController: Localizable { + func applyLocalization() { + guard isViewLoaded else { + return + } + setupLocalization() + } +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoViewFactory.swift b/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoViewFactory.swift new file mode 100644 index 0000000000..25a1993e26 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoViewFactory.swift @@ -0,0 +1,226 @@ +import Foundation +import SubstrateSdk +import SoraFoundation +import RobinHood + +struct StartStakingInfoViewFactory { + static func createView(chainAsset: ChainAsset, selectedStakingType: StakingType?) -> StartStakingInfoViewProtocol? { + let optMainStakingType = chainAsset.asset.stakings?.sorted { type1, type2 in + type1.isMorePreferred(than: type2) + }.first + + guard let mainStakingType = optMainStakingType else { + return nil + } + + switch mainStakingType { + case .relaychain: + return createRelaychainView( + chainAsset: chainAsset, + consensus: .babe, + selectedStakingType: selectedStakingType + ) + case .auraRelaychain: + return createRelaychainView( + chainAsset: chainAsset, + consensus: .auraGeneral, + selectedStakingType: selectedStakingType + ) + case .azero: + return createRelaychainView( + chainAsset: chainAsset, + consensus: .auraAzero, + selectedStakingType: selectedStakingType + ) + case .parachain, .turing: + return createParachainView( + for: .init( + chainAsset: chainAsset, + type: selectedStakingType ?? mainStakingType + ) + ) + case .unsupported, .nominationPools: + return nil + } + } + + private static func createRelaychainView( + chainAsset: ChainAsset, + consensus: ConsensusType, + selectedStakingType: StakingType? + ) -> StartStakingInfoViewProtocol? { + let operationQueue = OperationManagerFacade.sharedDefaultQueue + + let stateFactory = StakingSharedStateFactory( + storageFacade: SubstrateDataStorageFacade.shared, + chainRegistry: ChainRegistryFacade.sharedRegistry, + eventCenter: EventCenter.shared, + syncOperationQueue: operationQueue, + repositoryOperationQueue: operationQueue, + logger: Logger.shared + ) + + guard + let state = try? stateFactory.createStartRelaychainStaking( + for: chainAsset, + consensus: consensus, + selectedStakingType: selectedStakingType + ), + let currencyManager = CurrencyManager.shared else { + return nil + } + + let interactor = createRelaychainInteractor( + state: state, + currencyManager: currencyManager, + operationQueue: operationQueue + ) + + let wireframe = StartStakingInfoRelaychainWireframe(state: state) + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: chainAsset.assetDisplayInfo, + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager) + ) + let startStakingViewModelFactory = StartStakingViewModelFactory( + balanceViewModelFactory: balanceViewModelFactory, + estimatedEarningsFormatter: NumberFormatter.percentBase.localizableResource() + ) + + let presenter = StartStakingInfoRelaychainPresenter( + selectedStakingType: state.stakingType, + chainAsset: chainAsset, + interactor: interactor, + wireframe: wireframe, + startStakingViewModelFactory: startStakingViewModelFactory, + balanceDerivationFactory: StakingTypeBalanceFactory(stakingType: state.stakingType), + localizationManager: LocalizationManager.shared, + applicationConfig: ApplicationConfig.shared, + logger: Logger.shared + ) + + let view = StartStakingInfoViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared, + themeColor: chainAsset.chain.themeColor ?? R.color.colorPolkadotBrand()! + ) + + presenter.view = view + interactor.presenter = presenter + + return view + } + + private static func createRelaychainInteractor( + state: RelaychainStartStakingStateProtocol, + currencyManager: CurrencyManagerProtocol, + operationQueue: OperationQueue + ) -> StartStakingRelaychainInteractor { + let selectedWalletSettings = SelectedWalletSettings.shared + let walletLocalSubscriptionFactory = WalletLocalSubscriptionFactory.shared + let priceLocalSubscriptionFactory = PriceProviderFactory.shared + + let networkOperationFactory = state.createNetworkInfoOperationFactory(for: operationQueue) + let eraCountdownFactory = state.createEraCountdownOperationFactory(for: operationQueue) + + return StartStakingRelaychainInteractor( + state: state, + selectedWalletSettings: selectedWalletSettings, + chainRegistry: ChainRegistryFacade.sharedRegistry, + walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, + priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, + currencyManager: currencyManager, + networkInfoOperationFactory: networkOperationFactory, + eraCoundownOperationFactory: eraCountdownFactory, + eventCenter: EventCenter.shared, + operationQueue: operationQueue + ) + } + + private static func createParachainView( + for stakingOption: Multistaking.ChainAssetOption + ) -> StartStakingInfoViewProtocol? { + let operationQueue = OperationManagerFacade.sharedDefaultQueue + + let stateFactory = StakingSharedStateFactory( + storageFacade: SubstrateDataStorageFacade.shared, + chainRegistry: ChainRegistryFacade.sharedRegistry, + eventCenter: EventCenter.shared, + syncOperationQueue: operationQueue, + repositoryOperationQueue: operationQueue, + logger: Logger.shared + ) + + guard + let state = try? stateFactory.createParachain(for: stakingOption), + let currencyManager = CurrencyManager.shared else { + return nil + } + + let interactor = createParachainInteractor(state: state, currencyManager: currencyManager) + + let wireframe = StartStakingInfoParachainWireframe(state: state) + let balanceViewModelFactory = BalanceViewModelFactory( + targetAssetInfo: stakingOption.chainAsset.assetDisplayInfo, + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager) + ) + let startStakingViewModelFactory = StartStakingViewModelFactory( + balanceViewModelFactory: balanceViewModelFactory, + estimatedEarningsFormatter: NumberFormatter.percentBase.localizableResource() + ) + + let presenter = StartStakingInfoParachainPresenter( + chainAsset: stakingOption.chainAsset, + interactor: interactor, + wireframe: wireframe, + startStakingViewModelFactory: startStakingViewModelFactory, + balanceDerivationFactory: StakingTypeBalanceFactory(stakingType: stakingOption.type), + localizationManager: LocalizationManager.shared, + applicationConfig: ApplicationConfig.shared, + logger: Logger.shared + ) + + let view = StartStakingInfoViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared, + themeColor: stakingOption.chainAsset.chain.themeColor ?? R.color.colorPolkadotBrand()! + ) + + presenter.view = view + interactor.presenter = presenter + + return view + } + + private static func createParachainInteractor( + state: ParachainStakingSharedStateProtocol, + currencyManager: CurrencyManagerProtocol + ) -> StartStakingParachainInteractor { + let selectedWalletSettings = SelectedWalletSettings.shared + let walletLocalSubscriptionFactory = WalletLocalSubscriptionFactory.shared + let priceLocalSubscriptionFactory = PriceProviderFactory.shared + let operationQueue = OperationManagerFacade.sharedDefaultQueue + let operationManager = OperationManager(operationQueue: operationQueue) + + let storageRequestFactory = StorageRequestFactory( + remoteFactory: StorageKeyFactory(), + operationManager: operationManager + ) + + let stakingDurationFactory = ParaStkDurationOperationFactory( + storageRequestFactory: storageRequestFactory, + blockTimeOperationFactory: BlockTimeOperationFactory(chain: state.stakingOption.chainAsset.chain) + ) + + return StartStakingParachainInteractor( + state: state, + selectedWalletSettings: selectedWalletSettings, + walletLocalSubscriptionFactory: walletLocalSubscriptionFactory, + priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, + currencyManager: currencyManager, + networkInfoFactory: ParaStkNetworkInfoOperationFactory(), + durationOperationFactory: stakingDurationFactory, + operationQueue: operationQueue, + eventCenter: EventCenter.shared + ) + } +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoWireframe.swift b/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoWireframe.swift new file mode 100644 index 0000000000..73c4dc0485 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/StartStakingInfoWireframe.swift @@ -0,0 +1,15 @@ +import Foundation + +class StartStakingInfoWireframe: StartStakingInfoWireframeProtocol { + func showWalletDetails(from view: ControllerBackedProtocol?, wallet: MetaAccountModel) { + guard let accountManagementView = AccountManagementViewFactory.createView(for: wallet.identifier) else { + return + } + + view?.controller.navigationController?.pushViewController(accountManagementView.controller, animated: true) + } + + func showSetupAmount(from _: ControllerBackedProtocol?) { + fatalError("Must be overriden by subsclass") + } +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/View/ParagraphView.swift b/novawallet/Modules/Staking/StartStakingInfo/View/ParagraphView.swift new file mode 100644 index 0000000000..b3e279eeea --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/View/ParagraphView.swift @@ -0,0 +1,61 @@ +import UIKit + +final class ParagraphView: RowView { + var imageView: UIImageView { rowContentView.imageView } + var detailsLabel: UILabel { rowContentView.detailsLabel } + var style: Style = .defaultStyle + + override init(frame: CGRect) { + super.init(frame: frame) + + configure() + } + + private func configure() { + rowContentView.stackView.alignment = .top + rowContentView.iconWidth = 32 + rowContentView.spacing = 16 + isUserInteractionEnabled = false + } +} + +extension ParagraphView { + struct Model { + let image: UIImage? + let text: AccentTextModel + } + + typealias Style = MultiColorTextStyle + + func bind(viewModel: Model) { + imageView.image = viewModel.image + detailsLabel.bind( + model: viewModel.text, + with: style + ) + } +} + +extension ParagraphView.Style { + static let defaultStyle = ParagraphView.Style( + textColor: R.color.colorTextPrimary()!, + accentTextColor: R.color.colorPolkadotBrand()!, + font: .semiBoldTitle3 + ) + + static func createParagraphStyle(for themeColor: UIColor) -> ParagraphView.Style { + ParagraphView.Style( + textColor: R.color.colorTextPrimary()!, + accentTextColor: themeColor, + font: .semiBoldTitle3 + ) + } + + static func createHeaderStyle(for themeColor: UIColor) -> ParagraphView.Style { + ParagraphView.Style( + textColor: R.color.colorTextPrimary()!, + accentTextColor: themeColor, + font: .boldTitle1 + ) + } +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/View/StartStakingInfoViewLayout.swift b/novawallet/Modules/Staking/StartStakingInfo/View/StartStakingInfoViewLayout.swift new file mode 100644 index 0000000000..043884d3eb --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/View/StartStakingInfoViewLayout.swift @@ -0,0 +1,291 @@ +import UIKit +import SoraUI + +final class StartStakingInfoViewLayout: ScrollableContainerLayoutView { + let headerStyle: MultiColorTextStyle + let paragraphStyle: MultiColorTextStyle + var skeletonView: SkrullableView? + + var header: StackTableHeaderCell = .create { + $0.titleLabel.textAlignment = .center + } + + lazy var wikiView: UITextView = .create { + $0.textAlignment = .center + $0.linkTextAttributes = [.foregroundColor: R.color.colorTextSecondary()!, + .font: UIFont.semiBoldCallout] + $0.font = .regularCallout + $0.textColor = R.color.colorTextTertiary() + $0.isScrollEnabled = false + $0.backgroundColor = .clear + $0.isEditable = false + $0.textContainerInset = .zero + } + + lazy var termsView: UITextView = .create { + $0.textAlignment = .center + $0.linkTextAttributes = [.foregroundColor: R.color.colorTextSecondary()!, + .font: UIFont.semiBoldCallout] + $0.font = .regularCallout + $0.textColor = R.color.colorTextTertiary() + $0.isScrollEnabled = false + $0.backgroundColor = .clear + $0.isEditable = false + $0.textContainerInset = .zero + } + + let footer: BlurBackgroundView = .create { + $0.sideLength = 12 + $0.cornerCut = [.topLeft, .topRight] + $0.borderWidth = Constants.footerBorderWidth + $0.borderColor = R.color.colorContainerBorder()! + } + + let actionView = LoadableActionView() + + let balanceLabel = UILabel(style: .regularSubhedlineSecondary, textAlignment: .center, numberOfLines: 1) + var paragraphViews: [ParagraphView] = [] + + init(headerStyle: MultiColorTextStyle, paragraphStyle: MultiColorTextStyle) { + self.headerStyle = headerStyle + self.paragraphStyle = paragraphStyle + + super.init(frame: .zero) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + if skeletonView != nil { + updateLoadingState() + skeletonView?.restartSkrulling() + } + } + + override func setupStyle() { + super.setupStyle() + + actionView.actionButton.applyEnabledStyle(colored: headerStyle.accentTextColor) + } + + override func setupLayout() { + super.setupLayout() + + stackView.layoutMargins = Constants.containerInsets + + let footerContentView = UIView.vStack(spacing: Constants.footerSpacing, [ + actionView, + balanceLabel + ]) + footer.addSubview(footerContentView) + + actionView.snp.makeConstraints { + $0.height.equalTo(Constants.actionViewHeight) + } + + footerContentView.snp.makeConstraints { + $0.edges.equalToSuperview().inset(Constants.footerInsets) + } + + addSubview(footer) + footer.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + $0.height.equalTo(Constants.footerHeight + Constants.footerBorderWidth) + $0.bottom.equalToSuperview().offset(Constants.footerBorderWidth) + } + } + + func updateContent( + title: AccentTextModel, + paragraphs: [ParagraphView.Model], + wikiUrl: StartStakingUrlModel, + termsUrl: StartStakingUrlModel + ) { + let arrangedSubviews = stackView.arrangedSubviews + + arrangedSubviews.forEach { + stackView.removeArrangedSubview($0) + $0.removeFromSuperview() + } + + stackView.spacing = Constants.containerSpacing + + set(title: title) + set(paragraphs: paragraphs) + setWiki(urlModel: wikiUrl) + setTerms(urlModel: termsUrl) + } + + func updateBalanceButton(text: String, locale: Locale) { + balanceLabel.text = text + actionView.actionButton.imageWithTitleView?.title = R.string.localizable.stakingStartTitle( + preferredLanguages: locale.rLanguages) + } + + private func set(title: AccentTextModel) { + header.titleLabel.bind(model: title, with: headerStyle) + header.titleLabel.numberOfLines = 0 + stackView.addArrangedSubview(header) + } + + private func set(paragraphs: [ParagraphView.Model]) { + paragraphs.forEach { + let view = ParagraphView(frame: .zero) + view.style = paragraphStyle + view.bind(viewModel: $0) + view.contentInsets = .zero + paragraphViews.append(view) + stackView.addArrangedSubview(view) + } + } + + private func setWiki(urlModel model: StartStakingUrlModel) { + wikiView.bind(url: model.url, urlText: model.urlName, in: model.text) + addArrangedSubview(wikiView) + stackView.setCustomSpacing(Constants.wikiAndTermsSpacing, after: wikiView) + } + + private func setTerms(urlModel model: StartStakingUrlModel) { + termsView.bind(url: model.url, urlText: model.urlName, in: model.text) + addArrangedSubview(termsView) + } +} + +extension StartStakingInfoViewLayout: SkeletonableView { + var skeletonSuperview: UIView { + containerView + } + + var hidingViews: [UIView] { + [ + header, + wikiView, + termsView + ] + paragraphViews + } + + // swiftlint:disable:next function_body_length + func createSkeletons(for spaceSize: CGSize) -> [Skeletonable] { + let offsetX: CGFloat = 18 + let offsetY: CGFloat = 39 + let spacing: CGFloat = 55 + let iconSize = CGSize(width: 28, height: 28) + let titleSize = CGSize(width: 155, height: 10) + let subtitleSize = CGSize(width: 126, height: 10) + + let headerFirstLineSize = CGSize(width: 145, height: 16) + let headerSecondLineSize = CGSize(width: 185, height: 16) + let headerThirdLineSize = CGSize(width: 109, height: 16) + + let topOffsetY = safeAreaInsets.top + 23 + + let headerFirstLineOffset = CGPoint( + x: spaceSize.width / 2 - headerFirstLineSize.width / 2, + y: topOffsetY + ) + + let headerFirstLineSkeleton = SingleSkeleton.createRow( + on: containerView, + containerView: containerView, + spaceSize: spaceSize, + offset: headerFirstLineOffset, + size: headerFirstLineSize + ) + let headerSecondLineOffset = CGPoint( + x: spaceSize.width / 2 - headerSecondLineSize.width / 2, + y: headerFirstLineOffset.y + headerFirstLineSize.height + 17 + ) + let headerSecondLineSkeleton = SingleSkeleton.createRow( + on: containerView, + containerView: containerView, + spaceSize: spaceSize, + offset: headerSecondLineOffset, + size: headerSecondLineSize + ) + + let headerThirdLineOffset = CGPoint( + x: spaceSize.width / 2 - headerThirdLineSize.width / 2, + y: headerSecondLineOffset.y + headerSecondLineSize.height + 17 + ) + let headerThirdLineSkeleton = SingleSkeleton.createRow( + on: containerView, + containerView: containerView, + spaceSize: spaceSize, + offset: headerThirdLineOffset, + size: headerThirdLineSize + ) + + let compoundSkeletons: [[Skeletonable]] = (0 ..< 5).map { index in + let iconOffset = CGPoint( + x: offsetX, + y: headerThirdLineOffset.y + offsetY + CGFloat(index) * (iconSize.height + spacing) + ) + + let iconSkeleton = SingleSkeleton.createRow( + on: containerView, + containerView: containerView, + spaceSize: spaceSize, + offset: iconOffset, + size: iconSize + ) + + let titleOffset = CGPoint( + x: iconOffset.x + iconSize.width + 18, + y: iconOffset.y + 7 + ) + + let titleSkeleton = SingleSkeleton.createRow( + on: containerView, + containerView: containerView, + spaceSize: spaceSize, + offset: titleOffset, + size: titleSize + ) + + let subtitleOffset = CGPoint( + x: titleOffset.x, + y: titleOffset.y + titleSize.height + 15 + ) + + let subtitleSkeleton = SingleSkeleton.createRow( + on: containerView, + containerView: containerView, + spaceSize: spaceSize, + offset: subtitleOffset, + size: subtitleSize + ) + + return [iconSkeleton, titleSkeleton, subtitleSkeleton] + } + + return [ + headerFirstLineSkeleton, + headerSecondLineSkeleton, + headerThirdLineSkeleton + ] + compoundSkeletons.flatMap { $0 } + } +} + +extension StartStakingInfoViewLayout { + enum Constants { + static let footerHeight: CGFloat = 144 + static let footerContentSpace: CGFloat = 28 + static let footerSpacing: CGFloat = 12 + static let footerInsets = UIEdgeInsets(top: 16, left: 16, bottom: 31, right: 16) + static let containerInsets = UIEdgeInsets( + top: 12, + left: 16, + bottom: footerHeight, + right: 16 + ) + static let containerSpacing: CGFloat = 32 + static let wikiAndTermsSpacing: CGFloat = 16 + static let actionViewHeight: CGFloat = UIConstants.actionHeight + static let footerBorderWidth: CGFloat = 1 + } +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/View/UILabel+bind.swift b/novawallet/Modules/Staking/StartStakingInfo/View/UILabel+bind.swift new file mode 100644 index 0000000000..e28d04202a --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/View/UILabel+bind.swift @@ -0,0 +1,40 @@ +import UIKit + +struct MultiColorTextStyle { + var textColor: UIColor + var accentTextColor: UIColor + var font: UIFont +} + +struct AccentTextModel { + let text: String + let accents: [String] +} + +extension UILabel { + func bind( + model: AccentTextModel, + with style: MultiColorTextStyle + ) { + let attributedString = NSAttributedString( + string: model.text, + attributes: [.foregroundColor: style.textColor, + .font: style.font] + ) + + let highlightingAttributes: [NSAttributedString.Key: Any] = [ + .foregroundColor: style.accentTextColor + ] + + let decorators = model.accents.map { + HighlightingAttributedStringDecorator( + pattern: $0, + attributes: highlightingAttributes, + includeSeparator: true + ) + } + + attributedText = CompoundAttributedStringDecorator(decorators: decorators) + .decorate(attributedString: attributedString) + } +} diff --git a/novawallet/Modules/Staking/StartStakingInfo/View/UITextView+bind.swift b/novawallet/Modules/Staking/StartStakingInfo/View/UITextView+bind.swift new file mode 100644 index 0000000000..29d3f6f7d6 --- /dev/null +++ b/novawallet/Modules/Staking/StartStakingInfo/View/UITextView+bind.swift @@ -0,0 +1,27 @@ +import UIKit +import Foundation + +extension UITextView { + func bind(url: URL, urlText: String, in text: String) { + let font = UIFont.regularCallout + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + let attributedString = NSMutableAttributedString( + string: text, + attributes: [.foregroundColor: R.color.colorTextTertiary()!, + .font: font, + .paragraphStyle: paragraphStyle] + ) + if let range = text.range(of: urlText) { + let imageSize = CGSize(width: 20, height: 20) + let nsRange = NSRange(range, in: text) + attributedString.addAttribute(.link, value: url, range: nsRange) + let imageAttachment = NSTextAttachment() + imageAttachment.image = R.image.iconLinkChevron()!.tinted(with: R.color.colorTextSecondary()!) + let centerImageY = 2 * font.descender - imageSize.height / 2 + font.capHeight + imageAttachment.bounds = .init(origin: .init(x: 0, y: centerImageY), size: imageSize) + attributedString.append(.init(attachment: imageAttachment)) + } + attributedText = attributedString + } +} diff --git a/novawallet/Modules/Staking/Validation/NominationPoolDataValidatorFactory.swift b/novawallet/Modules/Staking/Validation/NominationPoolDataValidatorFactory.swift new file mode 100644 index 0000000000..a81185634f --- /dev/null +++ b/novawallet/Modules/Staking/Validation/NominationPoolDataValidatorFactory.swift @@ -0,0 +1,455 @@ +import SoraFoundation +import BigInt + +struct ExistentialDepositValidationParams { + let stakingAmount: Decimal? + let assetBalance: AssetBalance? + let fee: BigUInt? + let existentialDeposit: BigUInt? + let amountUpdateClosure: (Decimal) -> Void +} + +struct MinStakeCrossedParams { + let stakedAmountInPlank: BigUInt? + let minStake: BigUInt? + let unstakeAllHandler: () -> Void +} + +protocol NominationPoolDataValidatorFactoryProtocol: BaseDataValidatingFactoryProtocol { + func nominationPoolHasApy( + pool: NominationPools.SelectedPool, + locale: Locale + ) -> DataValidating + + func selectedPoolIsOpen( + for pool: NominationPools.PoolStats?, + locale: Locale + ) -> DataValidating + + func selectedPoolIsNotFull( + for pool: NominationPools.PoolStats?, + maxMembers: UInt32?, + locale: Locale + ) -> DataValidating + + func hasPoolMemberUnstakeSpace( + for poolMember: NominationPools.PoolMember?, + limits: NominationPools.UnstakeLimits?, + eraCountdown: EraCountdownDisplayProtocol?, + locale: Locale + ) -> DataValidating + + func hasLedgerUnstakeSpace( + for ledger: StakingLedger?, + limits: NominationPools.UnstakeLimits?, + eraCountdown: EraCountdownDisplayProtocol?, + locale: Locale + ) -> DataValidating + + func minStakeNotCrossed( + for inputAmount: Decimal, + params: MinStakeCrossedParams, + chainAsset: ChainAsset, + locale: Locale + ) -> DataValidating + + func canUnstake( + for inputAmount: Decimal, + stakedAmountInPlank: BigUInt?, + chainAsset: ChainAsset, + locale: Locale + ) -> DataValidating + + func hasProfitAfterClaim( + rewards: BigUInt?, + fee: BigUInt?, + chainAsset: ChainAsset, + locale: Locale + ) -> DataValidating + + func poolStakingNotViolatingExistentialDeposit( + for params: ExistentialDepositValidationParams, + chainAsset: ChainAsset, + locale: Locale + ) -> DataValidating + + func nominationPoolIsNotDestroing( + pool: NominationPools.BondedPool?, + locale: Locale + ) -> DataValidating + + func nominationPoolIsNotFullyUnbonding( + poolMember: NominationPools.PoolMember?, + locale: Locale + ) -> DataValidating +} + +final class NominationPoolDataValidatorFactory { + weak var view: (ControllerBackedProtocol & Localizable)? + let presentable: NominationPoolErrorPresentable + var basePresentable: BaseErrorPresentable { presentable } + + let balanceFactory: BalanceViewModelFactoryProtocol + + init(presentable: NominationPoolErrorPresentable, balanceFactory: BalanceViewModelFactoryProtocol) { + self.presentable = presentable + self.balanceFactory = balanceFactory + } +} + +extension NominationPoolDataValidatorFactory: NominationPoolDataValidatorFactoryProtocol { + func nominationPoolHasApy( + pool: NominationPools.SelectedPool, + locale: Locale + ) -> DataValidating { + WarningConditionViolation(onWarning: { [weak self] delegate in + guard let view = self?.view else { + return + } + self?.presentable.presentNominationPoolHasNoApy( + from: view, + action: { + delegate.didCompleteWarningHandling() + }, + locale: locale + ) + }, preservesCondition: { + if let apy = pool.maxApy, apy > 0 { + return true + } else { + return false + } + }) + } + + func selectedPoolIsOpen( + for pool: NominationPools.PoolStats?, + locale: Locale + ) -> DataValidating { + ErrorConditionViolation(onError: { [weak self] in + guard let view = self?.view else { + return + } + + self?.presentable.presentPoolIsNotOpen(from: view, locale: locale) + + }, preservesCondition: { + pool?.state == .open + }) + } + + func nominationPoolIsNotDestroing( + pool: NominationPools.BondedPool?, + locale: Locale + ) -> DataValidating { + ErrorConditionViolation(onError: { [weak self] in + guard let view = self?.view else { + return + } + + self?.presentable.presentNominationPoolIsDestroing( + from: view, + locale: locale + ) + + }, preservesCondition: { + guard let pool = pool else { + return false + } + + return pool.state != .destroying + }) + } + + func selectedPoolIsNotFull( + for pool: NominationPools.PoolStats?, + maxMembers: UInt32?, + locale: Locale + ) -> DataValidating { + ErrorConditionViolation(onError: { [weak self] in + guard let view = self?.view else { + return + } + + self?.presentable.presentPoolIsFull(from: view, locale: locale) + }, preservesCondition: { + guard let pool = pool else { + return false + } + + guard let maxMembers = maxMembers else { + return true + } + + return pool.membersCount < maxMembers + }) + } + + func nominationPoolIsNotFullyUnbonding( + poolMember: NominationPools.PoolMember?, + locale: Locale + ) -> DataValidating { + ErrorConditionViolation(onError: { [weak self] in + guard let view = self?.view else { + return + } + self?.presentable.presentPoolIsFullyUnbonding(from: view, locale: locale) + }, preservesCondition: { + guard let poolMember = poolMember else { + return false + } + return poolMember.points > 0 + }) + } + + func hasPoolMemberUnstakeSpace( + for poolMember: NominationPools.PoolMember?, + limits: NominationPools.UnstakeLimits?, + eraCountdown: EraCountdownDisplayProtocol?, + locale: Locale + ) -> DataValidating { + ErrorConditionViolation(onError: { [weak self] in + let minRedeemEra = (poolMember?.unbondingEras ?? []).min { $0.key.value < $1.key.value } + + let timeInteraval: TimeInterval? = minRedeemEra.flatMap { + eraCountdown?.timeIntervalTillStart(targetEra: $0.key.value) + } + + let timeString = timeInteraval?.localizedDaysHours(for: locale) ?? "" + + self?.presentable.presentNoUnstakeSpace( + from: self?.view, + unstakeAfter: timeString, + locale: locale + ) + + }, preservesCondition: { + guard + let poolMember = poolMember, + let limits = limits, + let eraCountdown = eraCountdown else { + return false + } + + let targetEra = eraCountdown.activeEra + limits.bondingDuration + + let hasSpace = poolMember.unbondingEras.count < limits.poolMemberMaxUnlockings + let hasEraUnstaking = poolMember.unbondingEras.contains { $0.key.value == targetEra } + + return hasSpace || hasEraUnstaking + }) + } + + func hasLedgerUnstakeSpace( + for stakingLedger: StakingLedger?, + limits: NominationPools.UnstakeLimits?, + eraCountdown: EraCountdownDisplayProtocol?, + locale: Locale + ) -> DataValidating { + ErrorConditionViolation(onError: { [weak self] in + let minRedeemEra = (stakingLedger?.unlocking ?? []).min { $0.era < $1.era } + + let timeInteraval: TimeInterval? = minRedeemEra.flatMap { + eraCountdown?.timeIntervalTillStart(targetEra: $0.era) + } + let timeString = timeInteraval?.localizedDaysHours(for: locale) ?? "" + + self?.presentable.presentNoUnstakeSpace( + from: self?.view, + unstakeAfter: timeString, + locale: locale + ) + + }, preservesCondition: { + guard + let stakingLedger = stakingLedger, + let limits = limits, + let eraCountdown = eraCountdown else { + return false + } + + let hasSpace = stakingLedger.unlocking.count < limits.globalMaxUnlockings + let hasRedeemable = stakingLedger.redeemable(inEra: eraCountdown.activeEra) > 0 + + return hasSpace || hasRedeemable + }) + } + + func minStakeNotCrossed( + for inputAmount: Decimal, + params: MinStakeCrossedParams, + chainAsset: ChainAsset, + locale: Locale + ) -> DataValidating { + let inputAmountInPlank = inputAmount.toSubstrateAmount( + precision: chainAsset.assetDisplayInfo.assetPrecision + ) ?? 0 + + let stakedAmountInPlank = params.stakedAmountInPlank + let minStake = params.minStake + + return WarningConditionViolation(onWarning: { [weak self] delegate in + guard let balanceFactory = self?.balanceFactory else { + return + } + + let stakedAmount = stakedAmountInPlank ?? 0 + let diff = stakedAmount >= inputAmountInPlank ? stakedAmount - inputAmountInPlank : 0 + + let minStakeDecimal = (minStake ?? 0).decimal(precision: chainAsset.asset.precision) + let diffDecimal = diff.decimal(precision: chainAsset.asset.precision) + + let minStakeString = balanceFactory.amountFromValue(minStakeDecimal).value(for: locale) + let diffString = balanceFactory.amountFromValue(diffDecimal).value(for: locale) + + self?.presentable.presentCrossedMinStake( + from: self?.view, + minStake: minStakeString, + remaining: diffString, + action: { + params.unstakeAllHandler() + delegate.didCompleteWarningHandling() + }, + locale: locale + ) + + }, preservesCondition: { + guard + let stakedAmountInPlank = stakedAmountInPlank, + let minStake = minStake, + stakedAmountInPlank >= inputAmountInPlank else { + return false + } + + let diff = stakedAmountInPlank - inputAmountInPlank + + return diff == 0 || diff >= minStake + }) + } + + func canUnstake( + for inputAmount: Decimal, + stakedAmountInPlank: BigUInt?, + chainAsset: ChainAsset, + locale: Locale + ) -> DataValidating { + let inputAmountInPlank = inputAmount.toSubstrateAmount( + precision: chainAsset.assetDisplayInfo.assetPrecision + ) ?? 0 + + return ErrorConditionViolation(onError: { [weak self] in + guard let view = self?.view else { + return + } + + self?.presentable.presentUnstakeAmountToHigh(from: view, locale: locale) + + }, preservesCondition: { + inputAmountInPlank > 0 && inputAmountInPlank <= (stakedAmountInPlank ?? 0) + }) + } + + func hasProfitAfterClaim( + rewards: BigUInt?, + fee: BigUInt?, + chainAsset _: ChainAsset, + locale: Locale + ) -> DataValidating { + WarningConditionViolation(onWarning: { [weak self] delegate in + guard let view = self?.view else { + return + } + + self?.presentable.presentNoProfitAfterClaimRewards( + from: view, + action: { + delegate.didCompleteWarningHandling() + }, + locale: locale + ) + }, preservesCondition: { + guard let rewards = rewards, let fee = fee else { + return false + } + + return rewards > fee + }) + } + + // swiftlint:disable:next function_body_length + func poolStakingNotViolatingExistentialDeposit( + for params: ExistentialDepositValidationParams, + chainAsset: ChainAsset, + locale: Locale + ) -> DataValidating { + let fee = params.fee ?? 0 + let minBalance = params.existentialDeposit ?? 0 + let feeAndMinBalance = fee + minBalance + + let precision = chainAsset.asset.precision + + return WarningConditionViolation(onWarning: { [weak self] delegate in + guard + let view = self?.view, + let assetBalance = params.assetBalance, + let balanceFactory = self?.balanceFactory else { + return + } + + let maxStake = assetBalance.totalInPlank > feeAndMinBalance ? + assetBalance.totalInPlank - feeAndMinBalance : 0 + let maxStakeDecimal = maxStake.decimal(precision: precision) + let maxStakeString = balanceFactory.amountFromValue( + maxStakeDecimal + ).value(for: locale) + + let availableBalanceString = balanceFactory.amountFromValue( + assetBalance.transferable.decimal(precision: precision) + ).value(for: locale) + + let feeString = balanceFactory.amountFromValue( + fee.decimal(precision: precision) + ).value(for: locale) + + let minBalanceString = balanceFactory.amountFromValue( + minBalance.decimal(precision: precision) + ).value(for: locale) + + let errorParams = NPoolsEDViolationErrorParams( + availableBalance: availableBalanceString, + minimumBalance: minBalanceString, + fee: feeString, + maxStake: maxStakeString + ) + + let action: (() -> Void)? + + if maxStakeDecimal > 0 { + action = { + params.amountUpdateClosure(maxStakeDecimal) + delegate.didCompleteWarningHandling() + } + } else { + action = nil + } + + self?.presentable.presentExistentialDepositViolation( + from: view, + params: errorParams, + action: action, + locale: locale + ) + + }, preservesCondition: { + guard + let assetBalance = params.assetBalance, + let stakingAmountInPlank = params.stakingAmount?.toSubstrateAmount( + precision: Int16(bitPattern: precision) + ) else { + return true + } + + return stakingAmountInPlank + fee + minBalance <= assetBalance.totalInPlank + }) + } +} diff --git a/novawallet/Modules/Staking/Validation/RelaychainStakingValidatorFacade.swift b/novawallet/Modules/Staking/Validation/RelaychainStakingValidatorFacade.swift new file mode 100644 index 0000000000..f9b87475c0 --- /dev/null +++ b/novawallet/Modules/Staking/Validation/RelaychainStakingValidatorFacade.swift @@ -0,0 +1,210 @@ +import Foundation +import BigInt +import SoraFoundation + +typealias RelaychainStakingErrorPresentable = StakingErrorPresentable & NominationPoolErrorPresentable + +struct RelaychainStakingValidationParams { + let chainAsset: ChainAsset + let stakingAmount: Decimal? + let availableBalance: BigUInt? + let assetBalance: AssetBalance? + let fee: BigUInt? + let existentialDeposit: BigUInt? + let feeRefreshClosure: () -> Void + let stakeUpdateClosure: (Decimal) -> Void +} + +protocol RelaychainStakingValidatorFacadeProtocol { + func createValidations( + from stakingMethod: StakingSelectionMethod, + params: RelaychainStakingValidationParams, + locale: Locale + ) -> [DataValidating] +} + +final class RelaychainStakingValidatorFacade { + let directStakingValidatingFactory: StakingDataValidatingFactory + let poolStakingValidatingFactory: NominationPoolDataValidatorFactory + + var view: (ControllerBackedProtocol & Localizable)? { + get { + directStakingValidatingFactory.view + } + + set { + directStakingValidatingFactory.view = newValue + poolStakingValidatingFactory.view = newValue + } + } + + init( + presentable: RelaychainStakingErrorPresentable, + balanceFactory: BalanceViewModelFactoryProtocol + ) { + directStakingValidatingFactory = StakingDataValidatingFactory( + presentable: presentable, + balanceFactory: balanceFactory + ) + + poolStakingValidatingFactory = NominationPoolDataValidatorFactory( + presentable: presentable, + balanceFactory: balanceFactory + ) + } + + private func createCommonValidations( + params: RelaychainStakingValidationParams, + restrictions: RelaychainStakingRestrictions?, + locale: Locale + ) -> [DataValidating] { + let assetDisplayInfo = params.chainAsset.assetDisplayInfo + + return [ + directStakingValidatingFactory.hasInPlank( + fee: params.fee, + locale: locale, + precision: assetDisplayInfo.assetPrecision + ) { + params.feeRefreshClosure() + }, + directStakingValidatingFactory.canSpendAmountInPlank( + balance: params.availableBalance, + spendingAmount: params.stakingAmount, + asset: assetDisplayInfo, + locale: locale + ), + directStakingValidatingFactory.canPayFeeInPlank( + balance: params.assetBalance?.transferable, + fee: params.fee, + asset: assetDisplayInfo, + locale: locale + ), + directStakingValidatingFactory.canPayFeeSpendingAmountInPlank( + balance: params.availableBalance, + fee: params.fee, + spendingAmount: params.stakingAmount, + asset: assetDisplayInfo, + locale: locale + ), + directStakingValidatingFactory.allowsNewNominators( + flag: restrictions?.allowsNewStakers ?? false, + locale: locale + ), + directStakingValidatingFactory.canNominateInPlank( + amount: params.stakingAmount, + minimalBalance: restrictions?.minJoinStake, + minNominatorBond: restrictions?.minJoinStake, + precision: params.chainAsset.asset.precision, + locale: locale + ) + ] + } + + private func createPoolValidations( + for selectedPool: NominationPools.SelectedPool, + params: RelaychainStakingValidationParams, + locale: Locale + ) -> [DataValidating] { + [ + poolStakingValidatingFactory.nominationPoolHasApy(pool: selectedPool, locale: locale), + poolStakingValidatingFactory.poolStakingNotViolatingExistentialDeposit( + for: .init( + stakingAmount: params.stakingAmount, + assetBalance: params.assetBalance, + fee: params.fee, + existentialDeposit: params.existentialDeposit, + amountUpdateClosure: params.stakeUpdateClosure + ), + chainAsset: params.chainAsset, + locale: locale + ) + ] + } + + private func createDirectStakingValidations( + for restrictions: RelaychainStakingRestrictions?, + params: RelaychainStakingValidationParams, + locale: Locale + ) -> [DataValidating] { + [ + directStakingValidatingFactory.minRewardableStakeIsNotViolated( + amount: params.stakingAmount, + rewardableStake: restrictions?.minRewardableStake, + assetInfo: params.chainAsset.assetDisplayInfo, + locale: locale + ) + ] + } + + private func createValidationsForRecommendation( + _ recommendation: RelaychainStakingRecommendation, + params: RelaychainStakingValidationParams, + locale: Locale + ) -> [DataValidating] { + switch recommendation.staking { + case let .pool(selectedPool): + return createPoolValidations(for: selectedPool, params: params, locale: locale) + case .direct: + return createDirectStakingValidations( + for: recommendation.restrictions, + params: params, + locale: locale + ) + } + } + + private func createValidationsForManual( + _ manual: RelaychainStakingManual, + params: RelaychainStakingValidationParams, + locale: Locale + ) -> [DataValidating] { + switch manual.staking { + case let .pool(pool): + return createPoolValidations(for: pool, params: params, locale: locale) + case .direct: + return createDirectStakingValidations( + for: manual.restrictions, + params: params, + locale: locale + ) + } + } +} + +extension RelaychainStakingValidatorFacade: RelaychainStakingValidatorFacadeProtocol { + func createValidations( + from stakingMethod: StakingSelectionMethod, + params: RelaychainStakingValidationParams, + locale: Locale + ) -> [DataValidating] { + let commonValidations = createCommonValidations( + params: params, + restrictions: stakingMethod.restrictions, + locale: locale + ) + + switch stakingMethod { + case let .recommendation(optRecommendation): + guard let recommendation = optRecommendation else { + return commonValidations + } + + let specificValidations = createValidationsForRecommendation( + recommendation, + params: params, + locale: locale + ) + + return commonValidations + specificValidations + case let .manual(manual): + let specificValidations = createValidationsForManual( + manual, + params: params, + locale: locale + ) + + return commonValidations + specificValidations + } + } +} diff --git a/novawallet/Modules/Staking/Validation/StakingDataValidatorFactory+Plank.swift b/novawallet/Modules/Staking/Validation/StakingDataValidatorFactory+Plank.swift new file mode 100644 index 0000000000..38fda53d6f --- /dev/null +++ b/novawallet/Modules/Staking/Validation/StakingDataValidatorFactory+Plank.swift @@ -0,0 +1,22 @@ +import Foundation +import BigInt + +extension StakingDataValidatingFactoryProtocol { + func canNominateInPlank( + amount: Decimal?, + minimalBalance: BigUInt?, + minNominatorBond: BigUInt?, + precision: UInt16, + locale: Locale + ) -> DataValidating { + let minimalBalanceDecimal = minimalBalance?.decimal(precision: precision) + let minNominatorBondDecimal = minNominatorBond?.decimal(precision: precision) + + return canNominate( + amount: amount, + minimalBalance: minimalBalanceDecimal, + minNominatorBond: minNominatorBondDecimal, + locale: locale + ) + } +} diff --git a/novawallet/Modules/Staking/Validation/StakingDataValidatorFactory.swift b/novawallet/Modules/Staking/Validation/StakingDataValidatorFactory.swift index eaca84c255..d44a745b38 100644 --- a/novawallet/Modules/Staking/Validation/StakingDataValidatorFactory.swift +++ b/novawallet/Modules/Staking/Validation/StakingDataValidatorFactory.swift @@ -62,12 +62,21 @@ protocol StakingDataValidatingFactoryProtocol: BaseDataValidatingFactoryProtocol locale: Locale ) -> DataValidating - func minStakeIsNotViolated( + func minRewardableStakeIsNotViolated( amount: Decimal?, params: MinStakeIsNotViolatedParams, assetInfo: AssetBalanceDisplayInfo, locale: Locale ) -> DataValidating + + func minRewardableStakeIsNotViolated( + amount: Decimal?, + rewardableStake: BigUInt?, + assetInfo: AssetBalanceDisplayInfo, + locale: Locale + ) -> DataValidating + + func allowsNewNominators(flag: Bool, locale: Locale) -> DataValidating } final class StakingDataValidatingFactory { @@ -325,7 +334,7 @@ extension StakingDataValidatingFactory: StakingDataValidatingFactoryProtocol { }) } - func minStakeIsNotViolated( + func minRewardableStakeIsNotViolated( amount: Decimal?, params: MinStakeIsNotViolatedParams, assetInfo: AssetBalanceDisplayInfo, @@ -351,7 +360,7 @@ extension StakingDataValidatingFactory: StakingDataValidatingFactoryProtocol { roundingMode: .up ).value(for: locale) - self?.presentable.presentMinStakeViolated( + self?.presentable.presentMinRewardableStakeViolated( from: view, action: { delegate.didCompleteWarningHandling() @@ -371,4 +380,57 @@ extension StakingDataValidatingFactory: StakingDataValidatingFactoryProtocol { } }) } + + func minRewardableStakeIsNotViolated( + amount: Decimal?, + rewardableStake: BigUInt?, + assetInfo: AssetBalanceDisplayInfo, + locale: Locale + ) -> DataValidating { + let optMinStakeDecimal = rewardableStake.flatMap { + Decimal.fromSubstrateAmount($0, precision: assetInfo.assetPrecision) + } + + return WarningConditionViolation(onWarning: { [weak self] delegate in + guard + let view = self?.view, + let minStakeDecimal = optMinStakeDecimal else { + return + } + + let amountString = self?.balanceFactory?.amountFromValue( + minStakeDecimal, + roundingMode: .up + ).value(for: locale) + + self?.presentable.presentMinRewardableStakeViolated( + from: view, + action: { + delegate.didCompleteWarningHandling() + }, + minStake: amountString ?? "", + locale: locale + ) + }, preservesCondition: { + if + let amount = amount, + let minStakeDecimal = optMinStakeDecimal { + return amount >= minStakeDecimal + } else { + return true + } + }) + } + + func allowsNewNominators(flag: Bool, locale: Locale) -> DataValidating { + ErrorConditionViolation(onError: { [weak self] in + guard let view = self?.view else { + return + } + + self?.presentable.presentMaxNumberOfNominatorsReached(from: view, locale: locale) + }, preservesCondition: { + flag + }) + } } diff --git a/novawallet/Modules/Staking/ViewModel/NominationPoolsIconFactory.swift b/novawallet/Modules/Staking/ViewModel/NominationPoolsIconFactory.swift new file mode 100644 index 0000000000..2631504325 --- /dev/null +++ b/novawallet/Modules/Staking/ViewModel/NominationPoolsIconFactory.swift @@ -0,0 +1,39 @@ +import Foundation +import SubstrateSdk + +protocol NominationPoolsIconFactoryProtocol { + func createIconViewModel( + for chainAsset: ChainAsset, + poolId: NominationPools.PoolId, + bondedAccountId: AccountId + ) -> ImageViewModelProtocol? +} + +final class NominationPoolsIconFactory { + private lazy var iconGenerator = PolkadotIconGenerator() + + private func getKnownPoolIcon(for chainAsset: ChainAsset, poolId: NominationPools.PoolId) -> UIImage? { + let chainId = chainAsset.chain.chainId + let isKnownPool = StakingConstants.recommendedPoolIds[chainId] == poolId + + return isKnownPool ? R.image.iconNova() : nil + } +} + +extension NominationPoolsIconFactory: NominationPoolsIconFactoryProtocol { + func createIconViewModel( + for chainAsset: ChainAsset, + poolId: NominationPools.PoolId, + bondedAccountId: AccountId + ) -> ImageViewModelProtocol? { + if let knownPoolIcon = getKnownPoolIcon(for: chainAsset, poolId: poolId) { + return StaticImageViewModel(image: knownPoolIcon) + } + + guard let accountIcon = try? iconGenerator.generateFromAccountId(bondedAccountId) else { + return nil + } + + return DrawableIconViewModel(icon: accountIcon) + } +} diff --git a/novawallet/Modules/Staking/StakingAmount/ViewModel/RewardDestinationViewModel.swift b/novawallet/Modules/Staking/ViewModel/RewardDestinationViewModel.swift similarity index 100% rename from novawallet/Modules/Staking/StakingAmount/ViewModel/RewardDestinationViewModel.swift rename to novawallet/Modules/Staking/ViewModel/RewardDestinationViewModel.swift diff --git a/novawallet/Modules/Staking/StakingAmount/ViewModel/RewardDestinationViewModelFactory.swift b/novawallet/Modules/Staking/ViewModel/RewardDestinationViewModelFactory.swift similarity index 100% rename from novawallet/Modules/Staking/StakingAmount/ViewModel/RewardDestinationViewModelFactory.swift rename to novawallet/Modules/Staking/ViewModel/RewardDestinationViewModelFactory.swift diff --git a/novawallet/Modules/TransactionHistory/LocalFilter/TransactionHistoryAccountPrefixFilter.swift b/novawallet/Modules/TransactionHistory/LocalFilter/TransactionHistoryAccountPrefixFilter.swift new file mode 100644 index 0000000000..e58569c22b --- /dev/null +++ b/novawallet/Modules/TransactionHistory/LocalFilter/TransactionHistoryAccountPrefixFilter.swift @@ -0,0 +1,38 @@ +import Foundation + +final class TransactionHistoryAccountPrefixFilter { + let accountPrefix: Data + let chainAsset: ChainAsset + + init(accountPrefix: Data, chainAsset: ChainAsset) { + self.accountPrefix = accountPrefix + self.chainAsset = chainAsset + } + + var chainFormat: ChainFormat { + chainAsset.chain.chainFormat + } +} + +extension TransactionHistoryAccountPrefixFilter: TransactionHistoryLocalFilterProtocol { + func shouldDisplayOperation(model: TransactionHistoryItem) -> Bool { + guard model.callPath.isBalancesTransfer else { + return true + } + + if + let sender = try? model.sender.toAccountId(using: chainFormat), + sender.starts(with: accountPrefix) { + return false + } + + if + let receiverAddress = model.receiver, + let recepient = try? receiverAddress.toAccountId(using: chainFormat), + recepient.starts(with: accountPrefix) { + return false + } + + return true + } +} diff --git a/novawallet/Modules/TransactionHistory/LocalFilter/TransactionHistoryLocalFilter.swift b/novawallet/Modules/TransactionHistory/LocalFilter/TransactionHistoryLocalFilter.swift new file mode 100644 index 0000000000..154714e44b --- /dev/null +++ b/novawallet/Modules/TransactionHistory/LocalFilter/TransactionHistoryLocalFilter.swift @@ -0,0 +1,19 @@ +import Foundation + +protocol TransactionHistoryLocalFilterProtocol { + func shouldDisplayOperation(model: TransactionHistoryItem) -> Bool +} + +final class TransactionHistoryAndPredicate { + let innerFilters: [TransactionHistoryLocalFilterProtocol] + + init(innerFilters: [TransactionHistoryLocalFilterProtocol]) { + self.innerFilters = innerFilters + } +} + +extension TransactionHistoryAndPredicate: TransactionHistoryLocalFilterProtocol { + func shouldDisplayOperation(model: TransactionHistoryItem) -> Bool { + innerFilters.allSatisfy { $0.shouldDisplayOperation(model: model) } + } +} diff --git a/novawallet/Modules/TransactionHistory/LocalFilter/TransactionHistoryLocalFilterFactory.swift b/novawallet/Modules/TransactionHistory/LocalFilter/TransactionHistoryLocalFilterFactory.swift new file mode 100644 index 0000000000..36f0e10885 --- /dev/null +++ b/novawallet/Modules/TransactionHistory/LocalFilter/TransactionHistoryLocalFilterFactory.swift @@ -0,0 +1,82 @@ +import Foundation +import RobinHood +import SubstrateSdk + +protocol TransactionHistoryLocalFilterFactoryProtocol { + func createWrapper() -> CompoundOperationWrapper +} + +final class TransactionHistoryLocalFilterFactory { + let runtimeProvider: RuntimeProviderProtocol? + let chainAsset: ChainAsset + let logger: LoggerProtocol + + init(runtimeProvider: RuntimeProviderProtocol?, chainAsset: ChainAsset, logger: LoggerProtocol) { + self.runtimeProvider = runtimeProvider + self.chainAsset = chainAsset + self.logger = logger + } + + private func createPoolAccountPrefixWrapper( + for runtimeProvider: RuntimeProviderProtocol, + chainAsset: ChainAsset + ) -> CompoundOperationWrapper { + let codingFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() + + let constantOperation = StorageConstantOperation(path: NominationPools.palletIdPath) + constantOperation.configurationBlock = { + do { + constantOperation.codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + } catch { + constantOperation.result = .failure(error) + } + } + + constantOperation.addDependency(codingFactoryOperation) + + let mergeOperation = ClosureOperation { + let palletId = try constantOperation.extractNoCancellableResultData().wrappedValue + + let accountPrefix = try NominationPools.derivedAccountPrefix(for: palletId) + + return TransactionHistoryAccountPrefixFilter(accountPrefix: accountPrefix, chainAsset: chainAsset) + } + + mergeOperation.addDependency(constantOperation) + + let dependencies = [codingFactoryOperation, constantOperation] + + return CompoundOperationWrapper(targetOperation: mergeOperation, dependencies: dependencies) + } +} + +extension TransactionHistoryLocalFilterFactory: TransactionHistoryLocalFilterFactoryProtocol { + func createWrapper() -> CompoundOperationWrapper { + let phishingFilter = TransactionHistoryPhishingFilter() + + guard chainAsset.asset.hasPoolStaking, let runtimeProvider = runtimeProvider else { + return CompoundOperationWrapper.createWithResult(phishingFilter) + } + + let poolTransferFilterWrapper = createPoolAccountPrefixWrapper(for: runtimeProvider, chainAsset: chainAsset) + + let mergeOperation = ClosureOperation { [weak self] in + do { + let poolTransferFilter = try poolTransferFilterWrapper.targetOperation.extractNoCancellableResultData() + + return TransactionHistoryAndPredicate(innerFilters: [poolTransferFilter, phishingFilter]) + } catch { + // don't block if something wrong with the filter + self?.logger.warning("Couldn't fetch pools transfer filter: \(error)") + return phishingFilter + } + } + + mergeOperation.addDependency(poolTransferFilterWrapper.targetOperation) + + return CompoundOperationWrapper( + targetOperation: mergeOperation, + dependencies: poolTransferFilterWrapper.allOperations + ) + } +} diff --git a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryPhishingFilter.swift b/novawallet/Modules/TransactionHistory/LocalFilter/TransactionHistoryPhishingFilter.swift similarity index 51% rename from novawallet/Modules/TransactionHistory/Model/TransactionHistoryPhishingFilter.swift rename to novawallet/Modules/TransactionHistory/LocalFilter/TransactionHistoryPhishingFilter.swift index c5c22878fb..f0168a21e9 100644 --- a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryPhishingFilter.swift +++ b/novawallet/Modules/TransactionHistory/LocalFilter/TransactionHistoryPhishingFilter.swift @@ -1,12 +1,12 @@ import Foundation -protocol TransactionHistoryPhishingFilterProtocol { - func isPhishing(transaction: TransactionHistoryItem) -> Bool -} - -final class TransactionHistoryPhishingFilter: TransactionHistoryPhishingFilterProtocol { - func isPhishing(transaction: TransactionHistoryItem) -> Bool { +final class TransactionHistoryPhishingFilter: TransactionHistoryLocalFilterProtocol { + private func isPhishing(transaction: TransactionHistoryItem) -> Bool { transaction.source == .evmAsset && transaction.callPath.isERC20Transfer && transaction.amountInPlankIntOrZero == 0 } + + func shouldDisplayOperation(model: TransactionHistoryItem) -> Bool { + !isPhishing(transaction: model) + } } diff --git a/novawallet/Modules/TransactionHistory/Model/HistoryPoolRewardContext.swift b/novawallet/Modules/TransactionHistory/Model/HistoryPoolRewardContext.swift new file mode 100644 index 0000000000..d892b27c7d --- /dev/null +++ b/novawallet/Modules/TransactionHistory/Model/HistoryPoolRewardContext.swift @@ -0,0 +1,6 @@ +import Foundation + +struct HistoryPoolRewardContext: Codable { + let poolId: NominationPools.PoolId? + let eventId: String +} diff --git a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift index cbc615ba4e..5d61372815 100644 --- a/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift +++ b/novawallet/Modules/TransactionHistory/Model/TransactionHistoryViewModelFactory.swift @@ -151,6 +151,50 @@ final class TransactionHistoryViewModelFactory { priceCalculator: TokenPriceCalculatorProtocol?, locale: Locale, txType: TransactionType + ) -> TransactionItemViewModel { + let title = txType == .reward ? + R.string.localizable.stakingReward(preferredLanguages: locale.rLanguages) : + R.string.localizable.stakingSlash(preferredLanguages: locale.rLanguages) + let subtitle = R.string.localizable.stakingTitle(preferredLanguages: locale.rLanguages) + + return createCommonRewardItemFromData( + data, + title: title, + subtitle: subtitle, + priceCalculator: priceCalculator, + locale: locale, + txType: txType + ) + } + + private func createPoolRewardOrSlashFromData( + _ data: TransactionHistoryItem, + priceCalculator: TokenPriceCalculatorProtocol?, + locale: Locale, + txType: TransactionType + ) -> TransactionItemViewModel { + let title = txType == .poolReward ? + R.string.localizable.stakingReward(preferredLanguages: locale.rLanguages) : + R.string.localizable.stakingSlash(preferredLanguages: locale.rLanguages) + let subtitle = R.string.localizable.stakingTypeNominationPool(preferredLanguages: locale.rLanguages) + + return createCommonRewardItemFromData( + data, + title: title, + subtitle: subtitle, + priceCalculator: priceCalculator, + locale: locale, + txType: txType + ) + } + + private func createCommonRewardItemFromData( + _ data: TransactionHistoryItem, + title: String, + subtitle: String, + priceCalculator: TokenPriceCalculatorProtocol?, + locale: Locale, + txType: TransactionType ) -> TransactionItemViewModel { let amountInPlank = data.amountInPlank.map { BigUInt($0) ?? 0 } ?? 0 let amount = Decimal.fromSubstrateAmount( @@ -169,10 +213,6 @@ final class TransactionHistoryViewModelFactory { let icon = R.image.iconRewardOperation() let imageViewModel = icon.map { StaticImageViewModel(image: $0) } - let title = txType == .reward ? - R.string.localizable.stakingReward(preferredLanguages: locale.rLanguages) : - R.string.localizable.stakingSlash(preferredLanguages: locale.rLanguages) - let subtitle = R.string.localizable.stakingTitle(preferredLanguages: locale.rLanguages) let amountDetails = amountDetails(price: balance.price, time: time, locale: locale) return TransactionItemViewModel( @@ -297,6 +337,13 @@ extension TransactionHistoryViewModelFactory: TransactionHistoryViewModelFactory locale: locale, txType: transactionType ) + case .poolReward, .poolSlash: + return createPoolRewardOrSlashFromData( + data, + priceCalculator: priceCalculator, + locale: locale, + txType: transactionType + ) case .extrinsic: return createExtrinsicItemFromData( data, @@ -315,6 +362,10 @@ extension TransactionHistoryItem { return .slash case .reward: return .reward + case .poolReward: + return .poolReward + case .poolSlash: + return .poolSlash default: if callPath.isSubstrateOrEvmTransfer { return sender == address ? .outgoing : .incoming diff --git a/novawallet/Modules/TransactionHistory/Model/WalletHistoryFilter+CallCodingPath.swift b/novawallet/Modules/TransactionHistory/Model/WalletHistoryFilter+CallCodingPath.swift deleted file mode 100644 index 2a38be3c9a..0000000000 --- a/novawallet/Modules/TransactionHistory/Model/WalletHistoryFilter+CallCodingPath.swift +++ /dev/null @@ -1,19 +0,0 @@ -extension WalletHistoryFilter { - func isFit(moduleName: String?, callName: String?) -> Bool { - guard let moduleName = moduleName, let callName = callName else { - return false - } - let callPath = CallCodingPath(moduleName: moduleName, callName: callName) - return isFit(callPath: callPath) - } - - func isFit(callPath: CallCodingPath) -> Bool { - if callPath.isSubstrateOrEvmTransfer { - return contains(.transfers) - } else if callPath.isRewardOrSlashTransfer { - return contains(.rewardsAndSlashes) - } else { - return contains(.extrinsics) - } - } -} diff --git a/novawallet/Modules/TransactionHistory/Service/AssetHistoryFactoryFacade.swift b/novawallet/Modules/TransactionHistory/Service/AssetHistoryFactoryFacade.swift index ea48b5b36c..933e1afb11 100644 --- a/novawallet/Modules/TransactionHistory/Service/AssetHistoryFactoryFacade.swift +++ b/novawallet/Modules/TransactionHistory/Service/AssetHistoryFactoryFacade.swift @@ -36,7 +36,8 @@ final class AssetHistoryFacade { return SubqueryHistoryOperationFactory( url: url, filter: mappedFilter, - assetId: historyAssetId + assetId: historyAssetId, + hasPoolStaking: asset.hasPoolStaking ) } catch { return nil diff --git a/novawallet/Modules/TransactionHistory/Service/WalletRemoteHistoryProtocols.swift b/novawallet/Modules/TransactionHistory/Service/WalletRemoteHistoryProtocols.swift index 9ce2105641..459bbd804b 100644 --- a/novawallet/Modules/TransactionHistory/Service/WalletRemoteHistoryProtocols.swift +++ b/novawallet/Modules/TransactionHistory/Service/WalletRemoteHistoryProtocols.swift @@ -6,6 +6,7 @@ enum WalletRemoteHistorySourceLabel: Int, CaseIterable { case transfers case rewards case extrinsics + case poolRewards } protocol WalletRemoteHistoryItemProtocol { diff --git a/novawallet/Modules/TransactionHistory/TransactionHistoryInteractor.swift b/novawallet/Modules/TransactionHistory/TransactionHistoryInteractor.swift index 4b1991e320..2bb7d4fd43 100644 --- a/novawallet/Modules/TransactionHistory/TransactionHistoryInteractor.swift +++ b/novawallet/Modules/TransactionHistory/TransactionHistoryInteractor.swift @@ -2,34 +2,46 @@ import UIKit import SoraUI import RobinHood -final class TransactionHistoryInteractor { +final class TransactionHistoryInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning { weak var presenter: TransactionHistoryInteractorOutputProtocol? let fetcherFactory: TransactionHistoryFetcherFactoryProtocol let chainAsset: ChainAsset let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol + let localFilterFactory: TransactionHistoryLocalFilterFactoryProtocol let accountId: AccountId let pageSize: Int + let operationQueue: OperationQueue private var fetcher: TransactionHistoryFetching? private var priceProvider: AnySingleValueProvider? + private var localFilterCancellable: CancellableCall? + init( accountId: AccountId, chainAsset: ChainAsset, fetcherFactory: TransactionHistoryFetcherFactoryProtocol, + localFilterFactory: TransactionHistoryLocalFilterFactoryProtocol, priceLocalSubscriptionFactory: PriceProviderFactoryProtocol, currencyManager: CurrencyManagerProtocol, + operationQueue: OperationQueue, pageSize: Int ) { self.accountId = accountId self.chainAsset = chainAsset self.fetcherFactory = fetcherFactory + self.localFilterFactory = localFilterFactory self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory self.pageSize = pageSize + self.operationQueue = operationQueue self.currencyManager = currencyManager } + deinit { + clear(cancellable: &localFilterCancellable) + } + private func setupFetcher(for filter: WalletHistoryFilter) { do { fetcher = try fetcherFactory.createFetcher( @@ -54,8 +66,37 @@ final class TransactionHistoryInteractor { return } + clear(singleValueProvider: &priceProvider) + priceProvider = subscribeToPriceHistory(for: priceId, currency: selectedCurrency) } + + private func provideLocalFilter() { + clear(cancellable: &localFilterCancellable) + + let wrapper = localFilterFactory.createWrapper() + + wrapper.targetOperation.completionBlock = { [weak self] in + DispatchQueue.main.async { + guard self?.localFilterCancellable === wrapper else { + return + } + + self?.localFilterCancellable = nil + + do { + let localFilter = try wrapper.targetOperation.extractNoCancellableResultData() + self?.presenter?.didReceive(localFilter: localFilter) + } catch { + self?.presenter?.didReceive(error: .localFilter(error)) + } + } + } + + localFilterCancellable = wrapper + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) + } } extension TransactionHistoryInteractor: TransactionHistoryInteractorInputProtocol { @@ -69,10 +110,19 @@ extension TransactionHistoryInteractor: TransactionHistoryInteractorInputProtoco } func setup() { + provideLocalFilter() setupPriceHistorySubscription() setupFetcher(for: .all) } + func remakeSubscriptions() { + setupPriceHistorySubscription() + } + + func retryLocalFilter() { + provideLocalFilter() + } + func set(filter: WalletHistoryFilter) { setupFetcher(for: filter) } diff --git a/novawallet/Modules/TransactionHistory/TransactionHistoryPresenter.swift b/novawallet/Modules/TransactionHistory/TransactionHistoryPresenter.swift index 886c885e78..cbd3a3847c 100644 --- a/novawallet/Modules/TransactionHistory/TransactionHistoryPresenter.swift +++ b/novawallet/Modules/TransactionHistory/TransactionHistoryPresenter.swift @@ -8,27 +8,25 @@ final class TransactionHistoryPresenter { let wireframe: TransactionHistoryWireframeProtocol let interactor: TransactionHistoryInteractorInputProtocol let viewModelFactory: TransactionHistoryViewModelFactoryProtocol - let phishingFilter: TransactionHistoryPhishingFilterProtocol let logger: LoggerProtocol? let address: AccountAddress private var items: [String: TransactionHistoryItem] = [:] private var filter: WalletHistoryFilter = .all private var priceCalculator: TokenPriceCalculatorProtocol? + private var localFilter: TransactionHistoryLocalFilterProtocol? init( address: AccountAddress, interactor: TransactionHistoryInteractorInputProtocol, wireframe: TransactionHistoryWireframeProtocol, viewModelFactory: TransactionHistoryViewModelFactoryProtocol, - phishingFilter: TransactionHistoryPhishingFilterProtocol, localizationManager: LocalizationManagerProtocol, logger: LoggerProtocol? ) { self.address = address self.interactor = interactor self.wireframe = wireframe - self.phishingFilter = phishingFilter self.viewModelFactory = viewModelFactory self.logger = logger self.localizationManager = localizationManager @@ -39,7 +37,12 @@ final class TransactionHistoryPresenter { return } - let models = Array(items.values).filter { !phishingFilter.isPhishing(transaction: $0) } + guard let localFilter = localFilter else { + view.didReceive(viewModel: []) + return + } + + let models = Array(items.values).filter { localFilter.shouldDisplayOperation(model: $0) } let viewModel = viewModelFactory.createGroupModel( models, @@ -105,10 +108,17 @@ extension TransactionHistoryPresenter: TransactionHistoryInteractorOutputProtoco switch error { case .fetchFailed: view?.stopLoading() + case .setupFailed: break case .priceFailed: - break + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.remakeSubscriptions() + } + case .localFilter: + wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in + self?.interactor.retryLocalFilter() + } } } @@ -125,6 +135,12 @@ extension TransactionHistoryPresenter: TransactionHistoryInteractorOutputProtoco reloadView() } + func didReceive(localFilter: TransactionHistoryLocalFilterProtocol) { + self.localFilter = localFilter + + reloadView() + } + func didReceiveFetchingState(isComplete: Bool) { if isComplete { view?.stopLoading() diff --git a/novawallet/Modules/TransactionHistory/TransactionHistoryProtocols.swift b/novawallet/Modules/TransactionHistory/TransactionHistoryProtocols.swift index 10170a0661..aae8ee27ac 100644 --- a/novawallet/Modules/TransactionHistory/TransactionHistoryProtocols.swift +++ b/novawallet/Modules/TransactionHistory/TransactionHistoryProtocols.swift @@ -16,6 +16,8 @@ protocol TransactionHistoryPresenterProtocol: AnyObject { protocol TransactionHistoryInteractorInputProtocol: AnyObject { func setup() func set(filter: WalletHistoryFilter) + func retryLocalFilter() + func remakeSubscriptions() func loadNext() } @@ -23,10 +25,11 @@ protocol TransactionHistoryInteractorOutputProtocol: AnyObject { func didReceive(error: TransactionHistoryError) func didReceive(changes: [DataProviderChange]) func didReceive(priceCalculator: TokenPriceCalculatorProtocol) + func didReceive(localFilter: TransactionHistoryLocalFilterProtocol) func didReceiveFetchingState(isComplete: Bool) } -protocol TransactionHistoryWireframeProtocol: AnyObject { +protocol TransactionHistoryWireframeProtocol: ErrorPresentable, AlertPresentable, CommonRetryable { func showFilter( from view: TransactionHistoryViewProtocol, filter: WalletHistoryFilter, @@ -43,4 +46,5 @@ enum TransactionHistoryError: Error { case fetchFailed(Error) case setupFailed(Error) case priceFailed(Error) + case localFilter(Error) } diff --git a/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift b/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift index 497878a6e3..278c2d4bd3 100644 --- a/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift +++ b/novawallet/Modules/TransactionHistory/TransactionHistoryViewFactory.swift @@ -38,7 +38,6 @@ struct TransactionHistoryViewFactory { interactor: interactor, wireframe: wireframe, viewModelFactory: viewModelFactory, - phishingFilter: TransactionHistoryPhishingFilter(), localizationManager: LocalizationManager.shared, logger: Logger.shared ) @@ -60,6 +59,9 @@ struct TransactionHistoryViewFactory { chainAsset: ChainAsset, currencyManager: CurrencyManagerProtocol ) -> TransactionHistoryInteractor { + let chainRegistry = ChainRegistryFacade.sharedRegistry + let runtimeProvider = chainRegistry.getRuntimeProvider(for: chainAsset.chain.chainId) + let operationQueue = OperationManagerFacade.sharedDefaultQueue let repositoryFactory = SubstrateRepositoryFactory(storageFacade: SubstrateDataStorageFacade.shared) @@ -75,12 +77,20 @@ struct TransactionHistoryViewFactory { operationQueue: operationQueue ) + let localFilterFactory = TransactionHistoryLocalFilterFactory( + runtimeProvider: runtimeProvider, + chainAsset: chainAsset, + logger: Logger.shared + ) + return .init( accountId: accountId, chainAsset: chainAsset, fetcherFactory: fetcherFactory, + localFilterFactory: localFilterFactory, priceLocalSubscriptionFactory: PriceProviderFactory.shared, currencyManager: currencyManager, + operationQueue: OperationManagerFacade.sharedDefaultQueue, pageSize: 100 ) } diff --git a/novawallet/Modules/TransactionHistory/View/HistoryItemTableViewCell.swift b/novawallet/Modules/TransactionHistory/View/HistoryItemTableViewCell.swift index 0306c70ab7..ce1190be0b 100644 --- a/novawallet/Modules/TransactionHistory/View/HistoryItemTableViewCell.swift +++ b/novawallet/Modules/TransactionHistory/View/HistoryItemTableViewCell.swift @@ -169,10 +169,10 @@ extension HistoryItemTableViewCell { amountDetailsLabel.text = transactionModel.amountDetails switch transactionModel.type { - case .incoming, .reward: + case .incoming, .reward, .poolReward: amountLabel.text = "+ \(transactionModel.amount)" amountLabel.textColor = R.color.colorTextPositive()! - case .outgoing, .slash, .extrinsic: + case .outgoing, .slash, .poolSlash, .extrinsic: amountLabel.text = "- \(transactionModel.amount)" amountLabel.textColor = R.color.colorTextPrimary()! } @@ -186,7 +186,7 @@ extension HistoryItemTableViewCell { .offset(-Constants.titleSpacingForTransfer) } - case .slash, .reward: + case .slash, .reward, .poolReward, .poolSlash: subtitleLabel.lineBreakMode = .byTruncatingTail subtitleLabel.snp.updateConstraints { make in diff --git a/novawallet/Modules/Vote/Governance/ReferendumDetails/View/BindableView.swift b/novawallet/Modules/Vote/Governance/ReferendumDetails/View/BindableView.swift index 6da0810523..17b36b9a74 100644 --- a/novawallet/Modules/Vote/Governance/ReferendumDetails/View/BindableView.swift +++ b/novawallet/Modules/Vote/Governance/ReferendumDetails/View/BindableView.swift @@ -16,10 +16,8 @@ extension BindableView { } } -extension RowView: BindableView where T: BindableView { - typealias TModel = T.TModel - - func bind(viewModel: TModel) { +extension StackTitleValueDiffCell: BindableView { + func bind(viewModel: ReferendumLockTransitionViewModel) { rowContentView.bind(viewModel: viewModel) } } diff --git a/novawallet/Modules/Vote/Governance/Subscription/Gov2SubscriptionFactory/Gov2SubscriptionFactory+Votes.swift b/novawallet/Modules/Vote/Governance/Subscription/Gov2SubscriptionFactory/Gov2SubscriptionFactory+Votes.swift index a7c9f51207..f9f586c549 100644 --- a/novawallet/Modules/Vote/Governance/Subscription/Gov2SubscriptionFactory/Gov2SubscriptionFactory+Votes.swift +++ b/novawallet/Modules/Vote/Governance/Subscription/Gov2SubscriptionFactory/Gov2SubscriptionFactory+Votes.swift @@ -73,7 +73,7 @@ extension Gov2SubscriptionFactory { } let subscription = CallbackBatchStorageSubscription( - requests: requests, + requests: requests.map { BatchStorageSubscriptionRequest(innerRequest: $0, mappingKey: nil) }, connection: connection, runtimeService: runtimeProvider, repository: nil, diff --git a/novawallet/Modules/Vote/Governance/View/ReferendumInfoView.swift b/novawallet/Modules/Vote/Governance/View/ReferendumInfoView.swift index 870656976b..727e618053 100644 --- a/novawallet/Modules/Vote/Governance/View/ReferendumInfoView.swift +++ b/novawallet/Modules/Vote/Governance/View/ReferendumInfoView.swift @@ -177,6 +177,11 @@ extension IconDetailsView.Style { tintColor: R.color.colorTextWarning()!, font: .caption1 ) + + static let chips = IconDetailsView.Style( + tintColor: R.color.colorIconChip()!, + font: .semiBoldCaps2 + ) } private extension UILabel.Style { diff --git a/novawallet/Modules/Vote/Parent/View/SkeletonableView.swift b/novawallet/Modules/Vote/Parent/View/SkeletonableView.swift index 620955d305..7df3895120 100644 --- a/novawallet/Modules/Vote/Parent/View/SkeletonableView.swift +++ b/novawallet/Modules/Vote/Parent/View/SkeletonableView.swift @@ -14,6 +14,8 @@ protocol SkeletonableView: UIView { var hidingViews: [UIView] { get } func startLoadingIfNeeded() func stopLoadingIfNeeded() + func didStartSkeleton() + func didStopSkeleton() func createSkeletons(for spaceSize: CGSize) -> [Skeletonable] func createDecorations(for spaceSize: CGSize) -> [Decorable] func updateLoadingState() @@ -27,6 +29,8 @@ extension SkeletonableView { func startLoadingIfNeeded() { hidingViews.forEach { $0.alpha = 0 } + didStartSkeleton() + guard skeletonView == nil else { return } @@ -41,6 +45,8 @@ extension SkeletonableView { func stopLoadingIfNeeded() { hidingViews.forEach { $0.alpha = 1 } + didStopSkeleton() + guard skeletonView != nil else { return } @@ -97,6 +103,9 @@ extension SkeletonableView { } func createDecorations(for _: CGSize) -> [Decorable] { [] } + + func didStartSkeleton() {} + func didStopSkeleton() {} } extension BlurredTableViewCell: SkeletonableViewCell where TContentView: SkeletonableView { diff --git a/novawallet/Modules/WalletsList/Common/CrowdloanContributionId.swift b/novawallet/Modules/WalletsList/Common/CrowdloanContributionId.swift deleted file mode 100644 index c3003357ad..0000000000 --- a/novawallet/Modules/WalletsList/Common/CrowdloanContributionId.swift +++ /dev/null @@ -1,7 +0,0 @@ -import BigInt - -struct CrowdloanContributionId { - let chainId: ChainModel.Id - let accountId: AccountId - let amount: BigUInt -} diff --git a/novawallet/Modules/WalletsList/Common/ExternalBalanceContribution.swift b/novawallet/Modules/WalletsList/Common/ExternalBalanceContribution.swift new file mode 100644 index 0000000000..cd5c63374b --- /dev/null +++ b/novawallet/Modules/WalletsList/Common/ExternalBalanceContribution.swift @@ -0,0 +1,7 @@ +import BigInt + +struct ExternalBalanceContribution { + let chainAssetId: ChainAssetId + let accountId: AccountId + let amount: BigUInt +} diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 9d182bb243..f98493b479 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -625,7 +625,7 @@ "staking.set.separate.account.controller_v2_2_0" = "Select another account as a controller to improve staking security by delegating staking management operations to it"; "staking.controller.can.hint_v2_2_0" = "Controller is used to: unstake, redeem, return to stake, change validators and set rewards destination"; "staking.stash.can.hint_v2_2_0" = "Stash is used to: stake more and set the controller"; -"staking.network.info.title" = "About staking"; +"staking.network.info.title" = "Staking info"; "staking.network.info.staking.period.value" = "Unlimited"; "staking.network.info.staking.period.title" = "Staking period"; "crowdloan.learn_v2_2_0" = "%@'s crowdloan website"; @@ -655,7 +655,6 @@ "dapp.sign.operation.details.subtitle" = "Make sure the operation is correct"; "common.all" = "All"; "dapp.list.search" = "Search by name or enter URL"; -"dapp.browser.close.confirmation" = "Are you sure you want to close this screen?\nYour changes will not be applied."; "common.wallet" = "Wallet"; "common.reload" = "Reload"; "dapp.confirmation.address.mismatch" = "Address in transaction (%@) doesn't match the address in the wallet (%@)"; @@ -834,6 +833,7 @@ "welcome.import.subtitle" = "Nova is compatible with all apps"; "welcome.watch.only.subtitle" = "Track any wallet by its address"; "common.ok.back" = "Okay, back"; +"common.back" = "Back"; "no.key.title" = "Oops! Key is missing"; "no.key.message" = "Your wallet is watch-only, meaning that you cannot do any operations with it"; "welcome.watch.only.title" = "Add watch-only wallet"; @@ -1280,6 +1280,35 @@ "multistaking.inactive.header" = "Stake and earn rewards"; "multistaking.more.options" = "More staking options"; "staking.on.network" = "%@ staking"; +"staking.start.stake" = "Stake anytime with as little as %@. Your stake will actively earn rewards %@"; +"staking.start.stake.without.minimum.stake" = "Stake anytime. Your stake will actively earn rewards %@"; +"staking.start.unstake" = "Unstake anytime, and redeem your funds %@. No rewards are earned while unstaking"; +"staking.start.rewards.manual.claim" = "Rewards accrue %@. You need to claim rewards manually"; +"staking.start.rewards.restake" = "Rewards accrue %@ and added back to the stake"; +"staking.start.rewards.balance" = "Rewards accrue %@ and added to the transferable balance"; +"staking.start.rewards.direct.staking" = "Rewards accrue %@. Stake over %@ for automatic rewards payout, otherwise you need to claim rewards manually"; +"staking.start.gov.nomination.pool" = "%@ with your staked tokens"; +"staking.start.gov.direct.staking" = "Stake over %@ and %@ with your staked tokens"; +"staking.start.gov.nomination.pool.action" = "Participate in governance"; +"staking.start.gov.nomination.direct.staking.action" = "participate in governance"; +"staking.start.changes.action" = "Monitor your stake"; +"staking.start.changes" = "Rewards and staking status vary over time. %@ from time to time"; +"staking.start.wiki" = "Find out more information about\n%@ staking over at the %@"; +"staking.start.wiki.link" = "Nova Wiki"; +"staking.start.terms" = "See %@"; +"staking.start.terms.link" = "Terms of Use"; +"staking.start.test.network" = "%@ is a %@ with %@"; +"staking.start.test.network.description" = "test network"; +"staking.start.test.network.token.value" = "no token value"; +"staking.start.earn.up.title" = "%@\non your %@ tokens\nper year"; +"staking.start.earn.up" = "Earn up to %@"; +"staking.start.balance.with.fiat" = "Available balance: %@(%@)"; +"staking.start.balance" = "Available balance: %@"; +"staking.start.no.account" = "No %@ account"; +"common.time.in" = "in"; +"common.and" = " and "; +"common.time.period.after" = "after"; +"common.time.period.every" = "every"; "wallet.history.transfer.incoming.details" = "From: %@"; "wallet.history.transfer.outgoing.details" = "To: %@"; "wallet.history.amount.details" = "%@ at %@"; @@ -1288,6 +1317,35 @@ "asset.operation.send.title" = "Send"; "asset.operation.receive.title" = "Receive"; "asset.operation.buy.title" = "Buy"; +"staking.direct" = "Direct"; +"staking.pool" = "Pool"; +"staking.setup.amount.title" = "Stake %@"; +"staking.type.direct" = "Direct staking"; +"staking.type.nomination.pool" = "Pool staking"; +"staking.type.minimum.stake" = "Minimum stake: %@"; +"staking.type.auto.rewards" = "Rewards: Paid automatically"; +"staking.type.manual.rewards" = "Rewards: Claim manually"; +"staking.type.gov.reuse.tokens" = "Reuse tokens in Governance"; +"staking.type.staking.managment" = "Advanced staking management"; +"staking.type.validators.title" = "Validators"; +"staking.type.recommended.validators.subtitle" = "Recommended"; +"staking.type.recommended.pool" = "Recommended"; +"staking.type.direct.staking.alert.title" = "Your stake is less than the minimum to earn rewards."; +"staking.type.direct.staking.alert.message" = "You have specified less than the minimum stake of %@ required to earn rewards with Direct staking. You should consider using Pool staking to earn rewards."; +"common.recommended" = "Recommended"; +"staking.direct.staking" = "Direct staking"; +"staking.pool.staking" = "Pool staking"; +"staking.min.required.stake.error" = "You can't stake less than the minimal value (%@)"; +"staking.locked.pool.violation.error" = "You have locked tokens on your balance due to %@. In order to continue you should enter up to %@ or %@ and more. To stake another amount you should remove your locks."; +"staking.type.title" = "Staking Type"; +"staking.validators" = "Validators"; +"staking.select.pool.count" = "active pools: %@"; +"staking.select.pool.members" = "members"; +"staking.select.pool.title" = "Select pool"; +"staking.search.pool.empty" = "No pool with entered name or pool\nID were found. Make sure\nyou entered correct data"; +"staking.search.pool.placeholder" = "Search by name or pool ID"; +"common.every.day" = "everyday"; +"staking.pool.network.info" = "Pool staking info"; "token.add.remote.exist.message" = "The entered contract address is present in Nova as a %@ token. Are you sure you want to modify it?"; "send.system.account.message" = "Recipient is a system account. It is not controlled by any company or individual.\nAre you sure you still want to perform this transfer?"; "send.system.account.title" = "Tokens will be lost"; @@ -1296,3 +1354,32 @@ "evm.transaction.fee.too.high.title" = "Network fee is too high"; "update.token.completion.message" = "%@ token updated"; "staking.reward.filters.period.custom.month.short" = "%@D"; +"staking.locked.pool.violation.title" = "You can’t stake the specified amount"; +"staking.setup.amount.direct.type.subtitle" = "Selected: %li of %li"; +"staking.pool.has.no.apy.title" = "You will not receive rewards"; +"staking.pool.has.no.apy.message" = "The pool you have selected is inactive due to no validators selected or its stake is less than the minimum. +Are you sure you want to proceed with the selected Pool?"; +"staking.claim.rewards" = "Claim rewards"; +"staking.your.pool.title" = "Your pool"; +"staking.your.pool.format" = "Your pool (#%@)"; +"staking.pool.rewards.claim.hint" = "Your rewards (%@) will also be claimed and added to your free balance"; +"staking.pool.rewards.bond.more.pool.is.destroing" = "Cannot perform specified operation since pool is in destroying state. It will be closed soon."; +"staking.pool.rewards.bond.more.pool.unbonding.error.title" = "Unable to stake more"; +"staking.pool.rewards.bond.more.pool.unbonding.error.message" = "You are unstaking all of your tokens and can't stake more."; +"staking.unstake.too.high.message" = "Amount you want to unstake is greater than staked balance."; +"staking.unstake.no.space.title" = "Too many people are unstaking from your pool"; +"staking.unstake.no.space.message" = "There are currently no free spot in unstaking queue for your pool. Please try again in %@"; +"staking.unstake.crossed.min.title" = "Too small amount remains in stake"; +"staking.unstake.crossed.min.message" = "When unstaking partially, you should leave at least %@ in stake. Do you want to perform full unstake by unstaking remaining %@ as well?"; +"staking.unstake.all" = "Unstake all"; +"staking.restake.message" = "Your tokens will be added back to the stake"; +"staking.pool.ed.error.message" = "Your available balance is %@, you need to leave %@ as minimal balance and pay network fee %@. You can stake not more than %@."; +"staking.maximum.action" = "Stake max"; +"staking.pool.is.full.title" = "Selected pool is full"; +"staking.pool.is.full.message" = "You cannot join selected pool since it reached maximum number of members"; +"staking.pool.is.not.open.title" = "Pool is not open"; +"staking.pool.is.not.open.message" = "You cannot join pool that is not open. Please, contact the pool owner."; +"common.close.when.changes.confirmation" = "Are you sure you want to close this screen?\nYour changes will not be applied."; +"staking.start.already.staking.title" = "Staking type cannot be changed"; +"staking.start.already.staking.pool" = "You are already staking in a pool"; +"staking.start.already.staking.direct" = "You are already have Direct staking"; diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 002843d895..f243049159 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -625,7 +625,7 @@ "staking.set.separate.account.controller_v2_2_0" = "Установите отдельный аккаунт как контроллер, чтобы увеличить безопасность стейкинга делегируя ему управление стейком"; "staking.controller.can.hint_v2_2_0" = "Контроллер аккаунт может: вывести, забрать, вернуть в стейк, сменить валидаторов и установить назначение вознаграждений"; "staking.stash.can.hint_v2_2_0" = "Стэш аккаунт может: застейкать больше и установить контроллер аккаунт"; -"staking.network.info.title" = "О стейкинге"; +"staking.network.info.title" = "Информация о стейкинге"; "staking.network.info.staking.period.value" = "Неограниченно"; "staking.network.info.staking.period.title" = "Время стейкинга"; "crowdloan.learn_v2_2_0" = "Cайт краудлоана %@"; @@ -655,7 +655,6 @@ "dapp.sign.operation.details.subtitle" = "Убедитесь, что операция правильная"; "common.all" = "Все"; "dapp.list.search" = "Введите имя или URL"; -"dapp.browser.close.confirmation" = "Вы уверены, что хотите закрыть этот экран?\nВсе изменения не будут применены."; "common.wallet" = "Кошелек"; "common.reload" = "Перезагрузить"; "dapp.confirmation.address.mismatch" = "Адрес в транзакции (%@) не совпадает с адресом в кошельке (%@)"; @@ -834,6 +833,7 @@ "welcome.import.subtitle" = "Nova совместима со всеми приложениями"; "welcome.watch.only.subtitle" = "Отследить любой кошелек по его адресу"; "common.ok.back" = "Хорошо, назад"; +"common.back" = "Назад"; "no.key.title" = "Упс! Ключ отсутствует"; "no.key.message" = "Ваш кошелек доступен только для просмотра, то есть вы не можете выполнять с ним никаких операций"; "welcome.watch.only.title" = "Добавить watch-only кошелёк"; @@ -1280,6 +1280,35 @@ "multistaking.inactive.header" = "Стейкайте и получайте награды"; "multistaking.more.options" = "Больше вариантов стейкинга"; "staking.on.network" = "%@ стейкинг"; +"staking.start.stake" = "Стейкайте в любое время всего c %@. Ваш стейк активно будет зарабатывать вознаграждения %@"; +"staking.start.stake.without.minimum.stake" = "Стейкайте в любое время. Ваш стейк активно будет зарабатывать вознаграждения %@"; +"staking.start.unstake" = "Отмените стейк в любое время, и верните свои средства %@. Вознаграждения не начисляются пока производится отмена"; +"staking.start.rewards.manual.claim" = "Награды начисляются %@. Вам нужно забрать награды вручную"; +"staking.start.rewards.restake" = "Награды начисляются %@ и добавляются в стейк"; +"staking.start.rewards.balance" = "Награды начисляются %@ и добавляются в доступный баланс"; +"staking.start.rewards.direct.staking" = "Награды начисляются %@. Застейкайте более %@ для автоматической выплаты вознаграждения, иначе необходимо забирать награды вручную"; +"staking.start.gov.nomination.pool" = "%@ с вашими застейканными токенами"; +"staking.start.gov.direct.staking" = "Застейкайте более %@ и %@ с вашими застейканными токенами"; +"staking.start.gov.nomination.pool.action" = "Участвуйте в голосовании"; +"staking.start.gov.nomination.direct.staking.action" = "участвуйте в голосовании"; +"staking.start.changes.action" = "Проверяйте свой стейк"; +"staking.start.changes" = "Награды и статус стейкинга меняются со временем. %@ время от времени"; +"staking.start.wiki" = "Узнайте больше информации о\n%@ стейкинге на %@"; +"staking.start.wiki.link" = "Nova Wiki"; +"staking.start.terms" = "Смотреть %@"; +"staking.start.terms.link" = "Условия использования"; +"staking.start.test.network" = "%@ является %@ без %@"; +"staking.start.test.network.description" = "тестовой сетью"; +"staking.start.test.network.token.value" = "ценности токена"; +"staking.start.earn.up.title" = "%@\nс вашими %@ токенами\nв год"; +"staking.start.earn.up" = "Зарабатывайте до %@"; +"staking.start.balance.with.fiat" = "Доступный баланс: %@(%@)"; +"staking.start.balance" = "Доступный баланс: %@"; +"staking.start.no.account" = "Нет %@ аккаунта"; +"common.time.in" = "через"; +"common.and" = " и "; +"common.time.period.after" = "через"; +"common.time.period.every" = "с переодичностью"; "wallet.history.transfer.incoming.details" = "От: %@"; "wallet.history.transfer.outgoing.details" = "Кому: %@"; "wallet.history.amount.details" = "%@ в %@"; @@ -1288,6 +1317,35 @@ "asset.operation.send.title" = "Отправить"; "asset.operation.receive.title" = "Получить"; "asset.operation.buy.title" = "Купить"; +"staking.direct" = "Прямой"; +"staking.pool" = "Пул"; +"staking.setup.amount.title" = "Стейк %@"; +"staking.type.direct" = "Прямой стейкинг"; +"staking.type.nomination.pool" = "Пул стейкинг "; +"staking.type.minimum.stake" = "Минимальный стейк: %@"; +"staking.type.auto.rewards" = "Вознаграждения: Выплачиваются автоматически"; +"staking.type.manual.rewards" = "Вознаграждения: Нужно забирать вручную"; +"staking.type.gov.reuse.tokens" = "Переиспользование токенов в Голосовании"; +"staking.type.staking.managment" = "Расширенное управление стейкингом"; +"staking.type.validators.title" = "Валидаторы"; +"staking.type.recommended.validators.subtitle" = "Рекомендованные"; +"staking.type.recommended.pool" = "Рекомендованный"; +"staking.type.direct.staking.alert.title" = "Ваш стейк меньше, чем минимальный для начисления наград."; +"staking.type.direct.staking.alert.message" = "Ваш стейк меньше минимального в %@, необходимого для получения вознаграждения с помощью Прямого стейкинга. Вам следует рассмотреть стейкинг в Пуле для получения вознаграждения."; +"common.recommended" = "Рекомендованный"; +"staking.direct.staking" = "Прямой стейкинг"; +"staking.pool.staking" = "Пул стейкинг"; +"staking.min.required.stake.error" = "Вы не можете стейкать меньше минимального значения (%@)"; +"staking.locked.pool.violation.error" = "Ваши токены заблокированы из - за %@. Для продолжения необходим ввести не больше чем %@ или %@ и больше. Для стейкинга другой суммы разблокируйте токены."; +"staking.type.title" = "Тип стейкинга"; +"staking.validators" = "Валидаторы"; +"staking.select.pool.count" = "активные пулы: %@"; +"staking.select.pool.members" = "участники"; +"staking.select.pool.title" = "Выбор пула"; +"staking.search.pool.empty" = "Пулы с таким именем или \nидентификатором не найдены. Убедитесь, что\nввели корректные данные"; +"common.every.day" = "каждый день"; +"staking.search.pool.placeholder" = "Поиск по имени или ID"; +"staking.pool.network.info" = "Информация о пул стейкинге"; "token.add.remote.exist.message" = "Введенный адрес контракта присутствует в Nova как токен %@. Вы уверены, что хотите изменить его?"; "send.system.account.message" = "Получатель является системным аккаунтом. Этот аккаунт не контролируется какой-либо компанией или частным лицом. \nВы уверены, что все еще хотите выполнить данный перевод?"; "send.system.account.title" = "Токены будут потеряны"; @@ -1296,3 +1354,32 @@ "evm.transaction.fee.too.high.title" = "Комиссия сети слишком высокая"; "update.token.completion.message" = "%@ токен обновлён"; "staking.reward.filters.period.custom.month.short" = "%@Д"; +"staking.locked.pool.violation.title" = "Вы не можете застейкать указанную сумму"; +"staking.setup.amount.direct.type.subtitle" = "Выбраны: %li из %li"; +"staking.pool.has.no.apy.title" = "Вы не получите награды"; +"staking.pool.has.no.apy.message" = "Выбранный вами пул неактивен, поскольку не выбраны валидаторы или его стейк меньше минимального. +Вы уверены, что хотите продолжить работу с выбранным пулом?"; +"staking.claim.rewards" = "Забрать награды"; +"staking.your.pool.title" = "Ваш пул"; +"staking.your.pool.format" = "Ваш пул (#%@)"; +"staking.pool.rewards.claim.hint" = "Ваши награды (%@) также будут добавлены к свободному балансу"; +"staking.pool.rewards.bond.more.pool.is.destroing" = "Невозможно выполнить операцию, поскольку пул находится в состоянии уничтожения. Скоро он будет закрыт."; +"staking.pool.rewards.bond.more.pool.unbonding.error.title" = "Невозможно увеличить стейк"; +"staking.pool.rewards.bond.more.pool.unbonding.error.message" = "Вы забираете все токены из стейка и поэтому не можете увеличивать стейк."; +"staking.unstake.too.high.message" = "Сумма анстейка больше чем ваш стейк."; +"staking.unstake.no.space.title" = "Слишком много людей забирают стейк в вашем пуле"; +"staking.unstake.no.space.message" = "В очереде не осталось свободных мест для того, чтобы забрать токены. Пожалуйста, попробуйте через %@"; +"staking.unstake.crossed.min.title" = "Слишком маленькая сумма остаётся в стейке"; +"staking.unstake.crossed.min.message" = "Когда стейк забирается частично, необходимо, чтобы в стейке оставалось не менее %@. Вы хотите забрать из стейка остаток %@ тоже?"; +"staking.unstake.all" = "Забрать всё"; +"staking.restake.message" = "Ваши награды будут добавлены к текущему стейку"; +"staking.pool.ed.error.message" = "Ваш доступный баланс %@, Вам необходимо оставить минимальный баланс %@ и заплатить комиссию сети %@. Вы можете застейкать не более чем %@."; +"staking.maximum.action" = "Застейкать макс."; +"staking.pool.is.full.title" = "Выбранный пул заполнен"; +"staking.pool.is.full.message" = "Вы не можете присоединиться к выбранному пулу так как в нём не осталось места"; +"staking.pool.is.not.open.title" = "Пул не открыт"; +"staking.pool.is.not.open.message" = "Вы не можете присоединиться к не открытому пулу. Пожалуйста, свяжитесь с владельцем пула."; +"common.close.when.changes.confirmation" = "Вы уверены, что хотите закрыть этот экран?\nИзменения не будут применены."; +"staking.start.already.staking.title" = "Тип стейкинга не может быть изменён"; +"staking.start.already.staking.pool" = "Вы уже стейкаете в пуле"; +"staking.start.already.staking.direct" = "У вас уже есть прямой стейкинг"; diff --git a/novawalletIntegrationTests/ActiveNominationPoolsTests.swift b/novawalletIntegrationTests/ActiveNominationPoolsTests.swift new file mode 100644 index 0000000000..0d5fa30866 --- /dev/null +++ b/novawalletIntegrationTests/ActiveNominationPoolsTests.swift @@ -0,0 +1,126 @@ +import XCTest +@testable import novawallet +import RobinHood + +final class ActiveNominationPoolsTests: XCTestCase { + + func testPolkadotPools() throws { + try performActivePoolsTest(for: KnowChainId.polkadot) + } + + private func performActivePoolsTest(for chainId: ChainModel.Id) throws { + // given + + let storageFacade = SubstrateStorageTestFacade() + + let chainRegistry = ChainRegistryFacade.setupForIntegrationTest(with: storageFacade) + + guard + let chain = chainRegistry.getChain(for: chainId), + let asset = chain.utilityAsset(), + let connection = chainRegistry.getConnection(for: chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + throw ChainRegistryError.noChain(chainId) + } + + let chainAsset = ChainAsset(chain: chain, asset: asset) + let operationQueue = OperationQueue() + + let substrateRepositoryFactory = SubstrateRepositoryFactory(storageFacade: storageFacade) + let substrateDataProviderFactory = SubstrateDataProviderFactory( + facade: storageFacade, + operationManager: OperationManager(operationQueue: operationQueue) + ) + + let eraValidatorService = EraValidatorService( + chainId: chainId, + storageFacade: storageFacade, + runtimeCodingService: runtimeService, + connection: connection, + providerFactory: substrateDataProviderFactory, + operationManager: OperationManager(operationQueue: operationQueue), + eventCenter: EventCenter.shared, + logger: Logger.shared + ) + + let npRemoteSubscriptionService = NominationPoolsRemoteSubscriptionService( + chainRegistry: chainRegistry, + repository: substrateRepositoryFactory.createChainStorageItemRepository(), + syncOperationManager: OperationManager(operationQueue: operationQueue), + repositoryOperationManager: OperationManager(operationQueue: operationQueue), + logger: Logger.shared + ) + + let relaychainSubscriptionService = StakingRemoteSubscriptionService( + chainRegistry: chainRegistry, + repository: substrateRepositoryFactory.createChainStorageItemRepository(), + syncOperationManager: OperationManager(operationQueue: operationQueue), + repositoryOperationManager: OperationManager(operationQueue: operationQueue), + logger: Logger.shared + ) + + let npOperationFactory = NominationPoolsOperationFactory(operationQueue: operationQueue) + let npDataProviderFactory = NPoolsLocalSubscriptionFactory( + chainRegistry: chainRegistry, + storageFacade: storageFacade, + operationManager: OperationManagerFacade.sharedManager, + logger: Logger.shared + ) + + let nominationPoolsService = EraNominationPoolsService( + chainAsset: chainAsset, + runtimeCodingService: runtimeService, + operationFactory: npOperationFactory, + npoolsLocalSubscriptionFactory: npDataProviderFactory, + eraValidatorService: eraValidatorService, + operationQueue: operationQueue + ) + + let npSubscriptionId = npRemoteSubscriptionService.attachToGlobalData( + for: chainId, + queue: nil, + closure: nil + ) + + let relaychainSubscriptionId = relaychainSubscriptionService.attachToGlobalData( + for: chainId, + queue: nil, + closure: nil + ) + + eraValidatorService.setup() + nominationPoolsService.setup() + + // when + + let operation = nominationPoolsService.fetchInfoOperation() + + operationQueue.addOperations([operation], waitUntilFinished: true) + + // then + + do { + let activePools = try operation.extractNoCancellableResultData() + Logger.shared.info("Active pools: \(activePools)") + } catch { + XCTFail("Can't get active pools: \(error)") + } + + npRemoteSubscriptionService.detachFromGlobalData( + for: npSubscriptionId!, + chainId: chainId, + queue: nil, + closure: nil + ) + + relaychainSubscriptionService.detachFromGlobalData( + for: relaychainSubscriptionId!, + chainId: chainId, + queue: nil, + closure: nil + ) + + eraValidatorService.throttle() + } + +} diff --git a/novawalletIntegrationTests/ExternalAssetBalanceIntegrationTests.swift b/novawalletIntegrationTests/ExternalAssetBalanceIntegrationTests.swift new file mode 100644 index 0000000000..58ade0bfd0 --- /dev/null +++ b/novawalletIntegrationTests/ExternalAssetBalanceIntegrationTests.swift @@ -0,0 +1,74 @@ +import XCTest +@testable import novawallet + +final class ExternalAssetBalanceIntegrationTests: XCTestCase { + + func testPolkadot() throws { + let accountId = try "1ChFWeNRLarAPRCTM3bfJmncJbSAbSS9yqjueWz7jX7iTVZ".toAccountId() + let chainAssetId = ChainAssetId( + chainId: KnowChainId.polkadot, + assetId: AssetModel.utilityAssetId + ) + + let balances = try performFetch( + for: chainAssetId, + accountId: accountId, + expectingTypes: [.nominationPools, .crowdloan] + ) + + Logger.shared.info("Balances: \(balances)") + } + + private func performFetch( + for chainAssetId: ChainAssetId, + accountId: AccountId, + expectingTypes: Set + ) throws -> [ExternalAssetBalance] { + let storageFacade = SubstrateStorageTestFacade() + let chainRegistry = ChainRegistryFacade.setupForIntegrationTest(with: storageFacade) + + guard + let chain = chainRegistry.getChain(for: chainAssetId.chainId), + let asset = chain.asset(for: chainAssetId.assetId) else { + throw ChainRegistryError.noChain(chainAssetId.chainId) + } + + let chainAsset = ChainAsset(chain: chain, asset: asset) + + let subscriptionFactory = ExternalBalanceLocalSubscriptionFacade.createDefaultFactory( + for: storageFacade, + chainRegistry: chainRegistry + ) + + guard let provider = subscriptionFactory.getExternalAssetBalanceProvider(for: accountId, chainAsset: chainAsset) else { + throw CommonError.dataCorruption + } + + var balances = [ExternalAssetBalance]() + + let expectation = XCTestExpectation() + + provider.addObserver( + self, + deliverOn: .main, + executing: { changes in + balances = balances.applying(changes: changes) + + let types = Set(balances.map({ $0.type })) + + if types.intersection(expectingTypes) == expectingTypes { + expectation.fulfill() + } + }, failing: { error in + Logger.shared.error("Unexpected error: \(error)") + }, + options: .init() + ) + + wait(for: [expectation], timeout: 600) + + provider.removeObserver(self) + + return balances + } +} diff --git a/novawalletIntegrationTests/MultistakingSyncTests.swift b/novawalletIntegrationTests/MultistakingSyncTests.swift index a7b2ad1fd0..84d4841452 100644 --- a/novawalletIntegrationTests/MultistakingSyncTests.swift +++ b/novawalletIntegrationTests/MultistakingSyncTests.swift @@ -12,6 +12,15 @@ final class MultistakingSyncTests: XCTestCase { Logger.shared.info("Result: \(result)") } + + func testAllStakableChainsForPoolSync() throws { + let result = try performAllStakableOptionsSync( + for: "1SohJrC8gHwHeJT1nkSonEbMd6yrkJgw8PwGsXUrKw3YrEK", + ethereumAddress: "0x7aa98aeb3afacf10021539d5412c7ac6afe0fb00" + ) + + Logger.shared.info("Result: \(result)") + } private func performAllStakableOptionsSync( for substrateAddress: AccountAddress, @@ -40,9 +49,10 @@ final class MultistakingSyncTests: XCTestCase { type: .watchOnly ) - let repositoryFactory = MultistakingRepositoryFactory(storageFacade: storageFacade) + let multistakingRepositoryFactory = MultistakingRepositoryFactory(storageFacade: storageFacade) + let substrateRepositoryFactory = SubstrateRepositoryFactory(storageFacade: storageFacade) let providerFactory = MultistakingProviderFactory( - repositoryFactory: repositoryFactory, + repositoryFactory: multistakingRepositoryFactory, operationQueue: operationQueue ) @@ -50,7 +60,8 @@ final class MultistakingSyncTests: XCTestCase { wallet: wallet, chainRegistry: chainRegistry, providerFactory: providerFactory, - repositoryFactory: repositoryFactory, + multistakingRepositoryFactory: multistakingRepositoryFactory, + substrateRepositoryFactory: substrateRepositoryFactory, offchainOperationFactory: SubqueryMultistakingOperationFactory(url: ApplicationConfig.shared.multistakingURL) ) @@ -81,7 +92,7 @@ final class MultistakingSyncTests: XCTestCase { // then - let dashboardRepository = repositoryFactory.createDashboardRepository(for: wallet.metaId) + let dashboardRepository = multistakingRepositoryFactory.createDashboardRepository(for: wallet.metaId) let fetchOperation = dashboardRepository.fetchAllOperation(with: RepositoryFetchOptions()) diff --git a/novawalletIntegrationTests/NominationPoolsApyTests.swift b/novawalletIntegrationTests/NominationPoolsApyTests.swift new file mode 100644 index 0000000000..e124f2ac6b --- /dev/null +++ b/novawalletIntegrationTests/NominationPoolsApyTests.swift @@ -0,0 +1,169 @@ +import XCTest +@testable import novawallet +import RobinHood + +final class NominationPoolsApyTests: XCTestCase { + + func testMaxApyCalculation() throws { + let calculator = try fetchRewardEngine(for: KnowChainId.polkadot) + + let maxApy = try calculator.calculateMaxReturn(isCompound: true, period: .year) + + Logger.shared.info("Polkadot max apy: \(maxApy.maxApy.stringWithPointSeparator)") + } + + func testApyForNovaPool() throws { + let calculator = try fetchRewardEngine(for: KnowChainId.polkadot) + + let maxApy = try calculator.calculateMaxReturn(poolId: 54, isCompound: true, period: .year) + + Logger.shared.info("Nova pool apy: \(maxApy.maxApy.stringWithPointSeparator)") + } + + func testApyForMaxPool() throws { + let calculator = try fetchRewardEngine(for: KnowChainId.polkadot) + + let maxApy = try calculator.calculateMaxReturn(isCompound: true, period: .year) + + Logger.shared.info("Pool apy: \(maxApy.maxApy.stringWithPointSeparator)") + } + + private func fetchRewardEngine(for chainId: ChainModel.Id) throws -> NominationPoolsRewardEngineProtocol { + // given + + let storageFacade = SubstrateStorageTestFacade() + + let chainRegistry = ChainRegistryFacade.setupForIntegrationTest(with: storageFacade) + + guard + let chain = chainRegistry.getChain(for: chainId), + let asset = chain.utilityAsset(), + let connection = chainRegistry.getConnection(for: chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { + throw ChainRegistryError.noChain(chainId) + } + + let chainAsset = ChainAsset(chain: chain, asset: asset) + let operationQueue = OperationQueue() + + let substrateRepositoryFactory = SubstrateRepositoryFactory(storageFacade: storageFacade) + let substrateDataProviderFactory = SubstrateDataProviderFactory( + facade: storageFacade, + operationManager: OperationManager(operationQueue: operationQueue) + ) + + let eraValidatorService = EraValidatorService( + chainId: chainId, + storageFacade: storageFacade, + runtimeCodingService: runtimeService, + connection: connection, + providerFactory: substrateDataProviderFactory, + operationManager: OperationManager(operationQueue: operationQueue), + eventCenter: EventCenter.shared, + logger: Logger.shared + ) + + let npRemoteSubscriptionService = NominationPoolsRemoteSubscriptionService( + chainRegistry: chainRegistry, + repository: substrateRepositoryFactory.createChainStorageItemRepository(), + syncOperationManager: OperationManager(operationQueue: operationQueue), + repositoryOperationManager: OperationManager(operationQueue: operationQueue), + logger: Logger.shared + ) + + let relaychainSubscriptionService = StakingRemoteSubscriptionService( + chainRegistry: chainRegistry, + repository: substrateRepositoryFactory.createChainStorageItemRepository(), + syncOperationManager: OperationManager(operationQueue: operationQueue), + repositoryOperationManager: OperationManager(operationQueue: operationQueue), + logger: Logger.shared + ) + + let npOperationFactory = NominationPoolsOperationFactory(operationQueue: operationQueue) + let npDataProviderFactory = NPoolsLocalSubscriptionFactory( + chainRegistry: chainRegistry, + storageFacade: storageFacade, + operationManager: OperationManagerFacade.sharedManager, + logger: Logger.shared + ) + + let nominationPoolsService = EraNominationPoolsService( + chainAsset: chainAsset, + runtimeCodingService: runtimeService, + operationFactory: npOperationFactory, + npoolsLocalSubscriptionFactory: npDataProviderFactory, + eraValidatorService: eraValidatorService, + operationQueue: operationQueue + ) + + let validatorRewardCalculatorService = try StakingServiceFactory( + chainRegisty: chainRegistry, + storageFacade: storageFacade, + eventCenter: EventCenter.shared, + operationQueue: operationQueue, + logger: Logger.shared + ).createRewardCalculatorService( + for: chainAsset, + stakingType: .relaychain, + stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactory( + chainRegistry: chainRegistry, + storageFacade: storageFacade, + operationManager: OperationManager(operationQueue: operationQueue), + logger: Logger.shared + ), + stakingDurationFactory: BabeStakingDurationFactory(), + validatorService: eraValidatorService + ) + + let rewardEngineFactory = NPoolsRewardEngineFactory(operationFactory: npOperationFactory) + + let npSubscriptionId = npRemoteSubscriptionService.attachToGlobalData( + for: chainId, + queue: nil, + closure: nil + ) + + let relaychainSubscriptionId = relaychainSubscriptionService.attachToGlobalData( + for: chainId, + queue: nil, + closure: nil + ) + + eraValidatorService.setup() + nominationPoolsService.setup() + validatorRewardCalculatorService.setup() + + // when + + let wrapper = rewardEngineFactory.createEngineWrapper( + for: nominationPoolsService, + validatorRewardService: validatorRewardCalculatorService, + connection: connection, + runtimeService: runtimeService + ) + + operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: true) + + // then + + npRemoteSubscriptionService.detachFromGlobalData( + for: npSubscriptionId!, + chainId: chainId, + queue: nil, + closure: nil + ) + + relaychainSubscriptionService.detachFromGlobalData( + for: relaychainSubscriptionId!, + chainId: chainId, + queue: nil, + closure: nil + ) + + eraValidatorService.throttle() + + validatorRewardCalculatorService.throttle() + + return try wrapper.targetOperation.extractNoCancellableResultData() + } +} diff --git a/novawalletIntegrationTests/NominationPoolsSyncTests.swift b/novawalletIntegrationTests/NominationPoolsSyncTests.swift new file mode 100644 index 0000000000..bd6cedc2b3 --- /dev/null +++ b/novawalletIntegrationTests/NominationPoolsSyncTests.swift @@ -0,0 +1,361 @@ +import XCTest +@testable import novawallet +import BigInt + +final class NominationPoolsSyncTests: XCTestCase { + + func testWalletWithPoolStaking() throws { + try performTestForAddress("1SohJrC8gHwHeJT1nkSonEbMd6yrkJgw8PwGsXUrKw3YrEK", chainId: KnowChainId.polkadot) + } + + private func performTestForAddress(_ address: AccountAddress, chainId: ChainModel.Id) throws { + let accountId = try address.toAccountId() + let chainAssetId = ChainAssetId(chainId: chainId, assetId: 0) + + let wallet = MetaAccountModel( + metaId: UUID().uuidString, + name: "Test", + substrateAccountId: accountId, + substrateCryptoType: 0, + substratePublicKey: Data(), + ethereumAddress: nil, + ethereumPublicKey: nil, + chainAccounts: [], + type: .watchOnly + ) + + try performSyncTest(for: wallet, chainAssetId: chainAssetId) + } + + private func performSyncTest(for wallet: MetaAccountModel, chainAssetId: ChainAssetId) throws { + // given + + let storageFacade = SubstrateStorageTestFacade() + + let chainRegistry = ChainRegistryFacade.setupForIntegrationTest(with: storageFacade) + + guard + let chain = chainRegistry.getChain(for: chainAssetId.chainId), + let asset = chain.asset(for: chainAssetId.assetId), + let accountResponse = wallet.fetch(for: chain.accountRequest()), + let connection = chainRegistry.getConnection(for: chain.chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chain.chainId) else { + throw ChainRegistryError.noChain(chainAssetId.chainId) + } + + let chainAsset = ChainAsset(chain: chain, asset: asset) + + let multistakingRepositoryFactory = MultistakingRepositoryFactory(storageFacade: storageFacade) + let substrateRepository = SubstrateRepositoryFactory(storageFacade: storageFacade) + + let nominationPoolsMultistaking = PoolsMultistakingUpdateService( + walletId: wallet.metaId, + accountId: accountResponse.accountId, + chainAsset: chainAsset, + stakingType: .nominationPools, + dashboardRepository: multistakingRepositoryFactory.createNominationPoolsRepository(), + accountRepository: multistakingRepositoryFactory.createResolvedAccountRepository(), + cacheRepository: substrateRepository.createChainStorageItemRepository(), + connection: connection, + runtimeService: runtimeService, + operationQueue: OperationQueue(), + workingQueue: .global(qos: .background), + logger: Logger.shared + ) + + nominationPoolsMultistaking.setup() + + let npoolsRemoteSubscriptionService = NominationPoolsRemoteSubscriptionService( + chainRegistry: chainRegistry, + repository: substrateRepository.createChainStorageItemRepository(), + syncOperationManager: OperationManagerFacade.sharedManager, + repositoryOperationManager: OperationManagerFacade.sharedManager, + logger: Logger.shared + ) + + let globalSubscriptionId = npoolsRemoteSubscriptionService.attachToGlobalData( + for: chain.chainId, + queue: nil, + closure: nil + ) + + let npoolsPoolSubscriptionService = NominationPoolsPoolSubscriptionService( + chainRegistry: chainRegistry, + repository: substrateRepository.createChainStorageItemRepository(), + syncOperationManager: OperationManagerFacade.sharedManager, + repositoryOperationManager: OperationManagerFacade.sharedManager, + logger: Logger.shared + ) + + let npoolsProviderFactory = NPoolsLocalSubscriptionFactory( + chainRegistry: chainRegistry, + storageFacade: storageFacade, + operationManager: OperationManagerFacade.sharedManager, + logger: Logger.shared + ) + + let npoolsAccountUpdatingService = NominationPoolsAccountUpdatingService( + accountId: accountResponse.accountId, + chainAsset: chainAsset, + connection: connection, + runtimeService: runtimeService, + cacheRepository: substrateRepository.createChainStorageItemRepository(), + npoolsLocalSubscriptionFactory: npoolsProviderFactory, + remoteSubscriptionService: npoolsPoolSubscriptionService, + operationQueue: OperationQueue(), + logger: Logger.shared + ) + + npoolsAccountUpdatingService.setup() + + // when + + let logger = Logger.shared + + let poolMemberExpectation = XCTestExpectation() + + var optPoolMember: NominationPools.PoolMember? + let poolMemberProvider = try npoolsProviderFactory.getPoolMemberProvider( + for: accountResponse.accountId, + chainId: chain.chainId + ) + + poolMemberProvider.addObserver( + self, + deliverOn: nil, + executing: { changes in + let optValue: NominationPools.PoolMember? = changes.reduceToLastChange()?.item + + if let value = optValue { + logger.info("Pool member: \(value)") + optPoolMember = value + poolMemberExpectation.fulfill() + } + }, failing: { error in + logger.error("Error: \(error)") + }, + options: .init() + ) + + wait(for: [poolMemberExpectation], timeout: 600) + + guard let poolMember = optPoolMember else { + throw CommonError.dataCorruption + } + + let lastPoolIdExpectation = XCTestExpectation() + let lastPoolIdProvider = try npoolsProviderFactory.getLastPoolIdProvider(for: chain.chainId) + + lastPoolIdProvider.addObserver( + self, + deliverOn: nil, + executing: { changes in + let optPoolId: NominationPools.PoolId? = changes.reduceToLastChange()?.item?.value + + if let poolId = optPoolId { + logger.info("Last pool id: \(poolId)") + lastPoolIdExpectation.fulfill() + } + }, failing: { error in + logger.error("Error: \(error)") + }, + options: .init() + ) + + let minJoinBondExpectation = XCTestExpectation() + let minJoinBondProvider = try npoolsProviderFactory.getMinJoinBondProvider(for: chain.chainId) + + minJoinBondProvider.addObserver( + self, + deliverOn: nil, + executing: { changes in + let optMinJoinBond: BigUInt? = changes.reduceToLastChange()?.item?.value + + if let minJoinBond = optMinJoinBond { + logger.info("Min join bond: \(minJoinBond)") + minJoinBondExpectation.fulfill() + } + }, failing: { error in + logger.error("Error: \(error)") + }, + options: .init() + ) + + let maxPoolMembersExpectation = XCTestExpectation() + let maxPoolMembersProvider = try npoolsProviderFactory.getMaxPoolMembers( + for: chain.chainId, + missingEntryStrategy: .emitError + ) + + maxPoolMembersProvider.addObserver( + self, + deliverOn: nil, + executing: { changes in + let optMaxPoolMembers: UInt32? = changes.reduceToLastChange()?.item?.value + + if let maxPoolMembers = optMaxPoolMembers { + logger.info("Max pool members: \(maxPoolMembers)") + maxPoolMembersExpectation.fulfill() + } + }, failing: { error in + logger.error("Error: \(error)") + }, + options: .init() + ) + + let maxPoolMembersPerPoolExpectation = XCTestExpectation() + let maxPoolMembersPerPoolProvider = try npoolsProviderFactory.getMaxMembersPerPool( + for: chain.chainId, + missingEntryStrategy: .emitError + ) + + maxPoolMembersPerPoolProvider.addObserver( + self, + deliverOn: nil, + executing: { changes in + let optMaxPoolMembers: UInt32? = changes.reduceToLastChange()?.item?.value + + logger.info("Max pool members per pool: \(optMaxPoolMembers)") + maxPoolMembersPerPoolExpectation.fulfill() + }, failing: { error in + logger.error("Error: \(error)") + }, + options: .init() + ) + + let counterForPoolMembersExpectation = XCTestExpectation() + let counterForPoolMembersProvider = try npoolsProviderFactory.getCounterForPoolMembers( + for: chain.chainId, + missingEntryStrategy: .emitError + ) + + counterForPoolMembersProvider.addObserver( + self, + deliverOn: nil, + executing: { changes in + let optCounterForMembers: UInt32? = changes.reduceToLastChange()?.item?.value + + if let counterForMembers = optCounterForMembers { + logger.info("Counter for pool members: \(counterForMembers)") + counterForPoolMembersExpectation.fulfill() + } + }, failing: { error in + logger.error("Error: \(error)") + }, + options: .init() + ) + + let bondedPoolExpectation = XCTestExpectation() + + let bondedPoolProvider = try npoolsProviderFactory.getBondedPoolProvider( + for: poolMember.poolId, + chainId: chain.chainId + ) + + bondedPoolProvider.addObserver( + self, + deliverOn: nil, + executing: { changes in + let optValue: NominationPools.BondedPool? = changes.reduceToLastChange()?.item + + if let value = optValue { + logger.info("Bonded pool: \(value)") + bondedPoolExpectation.fulfill() + } + }, failing: { error in + logger.error("Error: \(error)") + }, + options: .init() + ) + + let rewardPoolExpectation = XCTestExpectation() + let rewardPoolProvider = try npoolsProviderFactory.getRewardPoolProvider( + for: poolMember.poolId, + chainId: chain.chainId + ) + + rewardPoolProvider.addObserver( + self, + deliverOn: nil, + executing: { changes in + let optValue: NominationPools.RewardPool? = changes.reduceToLastChange()?.item + + if let value = optValue { + logger.info("Reward pool: \(value)") + rewardPoolExpectation.fulfill() + } + }, failing: { error in + logger.error("Error: \(error)") + }, + options: .init() + ) + + let subpoolsExpectation = XCTestExpectation() + let subpoolsProvider = try npoolsProviderFactory.getSubPoolsProvider( + for: poolMember.poolId, + chainId: chain.chainId + ) + + subpoolsProvider.addObserver( + self, + deliverOn: nil, + executing: { changes in + let optValue: NominationPools.SubPools? = changes.reduceToLastChange()?.item + + if let value = optValue { + logger.info("Subpools: \(value)") + subpoolsExpectation.fulfill() + } + }, failing: { error in + logger.error("Error: \(error)") + }, + options: .init() + ) + + let metadataExpectation = XCTestExpectation() + + let metadataProvider = try npoolsProviderFactory.getMetadataProvider( + for: poolMember.poolId, + chainId: chain.chainId + ) + + metadataProvider.addObserver( + self, + deliverOn: nil, + executing: { changes in + let optValue: Data? = changes.reduceToLastChange()?.item?.wrappedValue + + if let value = optValue { + logger.info("Metadata: \(String(describing: String(data: value, encoding: .utf8)))") + metadataExpectation.fulfill() + } + }, failing: { error in + logger.error("Error: \(error)") + }, + options: .init() + ) + + let expectations = [lastPoolIdExpectation, minJoinBondExpectation, bondedPoolExpectation, rewardPoolExpectation, subpoolsExpectation, metadataExpectation, maxPoolMembersExpectation, maxPoolMembersPerPoolExpectation, + counterForPoolMembersExpectation] + + wait(for: expectations, timeout: 6000) + + npoolsRemoteSubscriptionService.detachFromGlobalData( + for: globalSubscriptionId!, + chainId: chain.chainId, + queue: nil, + closure: nil + ) + + bondedPoolProvider.removeObserver(self) + metadataProvider.removeObserver(self) + rewardPoolProvider.removeObserver(self) + subpoolsProvider.removeObserver(self) + minJoinBondProvider.removeObserver(self) + lastPoolIdProvider.removeObserver(self) + poolMemberProvider.removeObserver(self) + maxPoolMembersProvider.removeObserver(self) + counterForPoolMembersProvider.removeObserver(self) + maxPoolMembersPerPoolProvider.removeObserver(self) + } +} diff --git a/novawalletIntegrationTests/SubqueryMultistakingTests.swift b/novawalletIntegrationTests/SubqueryMultistakingTests.swift index cfab064b05..fe21d43d42 100644 --- a/novawalletIntegrationTests/SubqueryMultistakingTests.swift +++ b/novawalletIntegrationTests/SubqueryMultistakingTests.swift @@ -54,7 +54,8 @@ final class SubqueryMultistakingTests: XCTestCase { let wrapper = operationFactory.createWrapper( from: wallet, - resolvedAccounts: [:], + bondedAccounts: [:], + rewardAccounts: [:], chainAssets: chainAssets ) diff --git a/novawalletTests/Common/DataProvider/RewardDataSourceTests.swift b/novawalletTests/Common/DataProvider/RewardDataSourceTests.swift index 1a8d881997..a793a76012 100644 --- a/novawalletTests/Common/DataProvider/RewardDataSourceTests.swift +++ b/novawalletTests/Common/DataProvider/RewardDataSourceTests.swift @@ -108,7 +108,8 @@ class RewardDataSourceTests: NetworkBaseTests { startTimestamp: nil, endTimestamp: nil, assetPrecision: assetPrecision, - operationFactory: operationFactory + operationFactory: operationFactory, + stakingType: .direct ) let provider = SingleValueProvider( diff --git a/novawalletTests/Mocks/CommonMocks.swift b/novawalletTests/Mocks/CommonMocks.swift index 9aae1c0935..f09b450cbb 100644 --- a/novawalletTests/Mocks/CommonMocks.swift +++ b/novawalletTests/Mocks/CommonMocks.swift @@ -6888,16 +6888,16 @@ import RobinHood - func createBlockTimeService(for chainId: ChainModel.Id, consensus: ConsensusType) throws -> BlockTimeEstimationServiceProtocol? { + func createTimeModel(for chainId: ChainModel.Id, consensus: ConsensusType) throws -> StakingTimeModel { - return try cuckoo_manager.callThrows("createBlockTimeService(for: ChainModel.Id, consensus: ConsensusType) throws -> BlockTimeEstimationServiceProtocol?", + return try cuckoo_manager.callThrows("createTimeModel(for: ChainModel.Id, consensus: ConsensusType) throws -> StakingTimeModel", parameters: (chainId, consensus), escapingParameters: (chainId, consensus), superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall() , - defaultCall: __defaultImplStub!.createBlockTimeService(for: chainId, consensus: consensus)) + defaultCall: __defaultImplStub!.createTimeModel(for: chainId, consensus: consensus)) } @@ -6920,9 +6920,9 @@ import RobinHood return .init(stub: cuckoo_manager.createStub(for: MockStakingServiceFactoryProtocol.self, method: "createRewardCalculatorService(for: ChainAsset, stakingType: StakingType, stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol, stakingDurationFactory: StakingDurationOperationFactoryProtocol, validatorService: EraValidatorServiceProtocol) throws -> RewardCalculatorServiceProtocol", parameterMatchers: matchers)) } - func createBlockTimeService(for chainId: M1, consensus: M2) -> Cuckoo.ProtocolStubThrowingFunction<(ChainModel.Id, ConsensusType), BlockTimeEstimationServiceProtocol?> where M1.MatchedType == ChainModel.Id, M2.MatchedType == ConsensusType { + func createTimeModel(for chainId: M1, consensus: M2) -> Cuckoo.ProtocolStubThrowingFunction<(ChainModel.Id, ConsensusType), StakingTimeModel> where M1.MatchedType == ChainModel.Id, M2.MatchedType == ConsensusType { let matchers: [Cuckoo.ParameterMatcher<(ChainModel.Id, ConsensusType)>] = [wrap(matchable: chainId) { $0.0 }, wrap(matchable: consensus) { $0.1 }] - return .init(stub: cuckoo_manager.createStub(for: MockStakingServiceFactoryProtocol.self, method: "createBlockTimeService(for: ChainModel.Id, consensus: ConsensusType) throws -> BlockTimeEstimationServiceProtocol?", parameterMatchers: matchers)) + return .init(stub: cuckoo_manager.createStub(for: MockStakingServiceFactoryProtocol.self, method: "createTimeModel(for: ChainModel.Id, consensus: ConsensusType) throws -> StakingTimeModel", parameterMatchers: matchers)) } } @@ -6954,9 +6954,9 @@ import RobinHood } @discardableResult - func createBlockTimeService(for chainId: M1, consensus: M2) -> Cuckoo.__DoNotUse<(ChainModel.Id, ConsensusType), BlockTimeEstimationServiceProtocol?> where M1.MatchedType == ChainModel.Id, M2.MatchedType == ConsensusType { + func createTimeModel(for chainId: M1, consensus: M2) -> Cuckoo.__DoNotUse<(ChainModel.Id, ConsensusType), StakingTimeModel> where M1.MatchedType == ChainModel.Id, M2.MatchedType == ConsensusType { let matchers: [Cuckoo.ParameterMatcher<(ChainModel.Id, ConsensusType)>] = [wrap(matchable: chainId) { $0.0 }, wrap(matchable: consensus) { $0.1 }] - return cuckoo_manager.verify("createBlockTimeService(for: ChainModel.Id, consensus: ConsensusType) throws -> BlockTimeEstimationServiceProtocol?", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + return cuckoo_manager.verify("createTimeModel(for: ChainModel.Id, consensus: ConsensusType) throws -> StakingTimeModel", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } } @@ -6982,8 +6982,8 @@ import RobinHood - func createBlockTimeService(for chainId: ChainModel.Id, consensus: ConsensusType) throws -> BlockTimeEstimationServiceProtocol? { - return DefaultValueRegistry.defaultValue(for: (BlockTimeEstimationServiceProtocol?).self) + func createTimeModel(for chainId: ChainModel.Id, consensus: ConsensusType) throws -> StakingTimeModel { + return DefaultValueRegistry.defaultValue(for: (StakingTimeModel).self) } } diff --git a/novawalletTests/Mocks/DataProviders/SlashesOperationFactoryStub.swift b/novawalletTests/Mocks/DataProviders/SlashesOperationFactoryStub.swift index e5046cab3f..f41a9e83eb 100644 --- a/novawalletTests/Mocks/DataProviders/SlashesOperationFactoryStub.swift +++ b/novawalletTests/Mocks/DataProviders/SlashesOperationFactoryStub.swift @@ -11,7 +11,7 @@ final class SlashesOperationFactoryStub: SlashesOperationFactoryProtocol { } func createSlashingSpansOperationForStash( - _ stashAddress: AccountAddress, + _ stashAccount: @escaping () throws -> AccountId, engine: JSONRPCEngine, runtimeService: RuntimeCodingServiceProtocol) -> CompoundOperationWrapper { return CompoundOperationWrapper.createWithResult(slashingSpans) diff --git a/novawalletTests/Mocks/DataProviders/StakingLocalSubscriptionFactoryStub.swift b/novawalletTests/Mocks/DataProviders/StakingLocalSubscriptionFactoryStub.swift index 42f683ca96..ec4f05b6d5 100644 --- a/novawalletTests/Mocks/DataProviders/StakingLocalSubscriptionFactoryStub.swift +++ b/novawalletTests/Mocks/DataProviders/StakingLocalSubscriptionFactoryStub.swift @@ -314,14 +314,18 @@ final class StakingLocalSubscriptionFactoryStub: StakingLocalSubscriptionFactory AnySingleValueProvider(SingleValueProviderStub(item: totalReward)) } - func getStashItemProvider(for address: AccountAddress) -> StreamableProvider { + func getStashItemProvider(for address: AccountAddress, chainId: ChainModel.Id) -> StreamableProvider { let provider = SubstrateDataProviderFactory( facade: storageFacade, operationManager: OperationManager() - ).createStashItemProvider(for: address) + ).createStashItemProvider(for: address, chainId: chainId) if let stashItem = stashItem { - let repository: CoreDataRepository = storageFacade.createRepository() + let repository = SubstrateRepositoryFactory(storageFacade: storageFacade).createStashItemRepository( + for: address, + chainId: chainId + ) + let saveOperation = repository.saveOperation({ [stashItem] }, { [] }) OperationQueue().addOperations([saveOperation], waitUntilFinished: true) } diff --git a/novawalletTests/Mocks/DataProviders/ValidatorOperationFactoryStub.swift b/novawalletTests/Mocks/DataProviders/ValidatorOperationFactoryStub.swift index bab23a67f8..6659a0eed7 100644 --- a/novawalletTests/Mocks/DataProviders/ValidatorOperationFactoryStub.swift +++ b/novawalletTests/Mocks/DataProviders/ValidatorOperationFactoryStub.swift @@ -32,4 +32,13 @@ class ValidatorOperationFactoryStub: ValidatorOperationFactoryProtocol { func wannabeValidatorsOperation(for accountIdList: [AccountId]) -> CompoundOperationWrapper<[SelectedValidatorInfo]> { CompoundOperationWrapper.createWithResult(selectedValidatorList) } + + func allPreferred(for preferredAccountIds: [AccountId]) -> CompoundOperationWrapper { + let electedAndPrefValidators = ElectedAndPrefValidators( + electedValidators: electedValidatorList, + preferredValidators: selectedValidatorList + ) + + return .createWithResult(electedAndPrefValidators) + } } diff --git a/novawalletTests/Mocks/ModuleMocks.swift b/novawalletTests/Mocks/ModuleMocks.swift index b20e887d3d..5883da179f 100644 --- a/novawalletTests/Mocks/ModuleMocks.swift +++ b/novawalletTests/Mocks/ModuleMocks.swift @@ -22898,6 +22898,21 @@ import RobinHood } + + + func allPreferred(for preferredAccountIds: [AccountId]) -> CompoundOperationWrapper { + + return cuckoo_manager.call("allPreferred(for: [AccountId]) -> CompoundOperationWrapper", + parameters: (preferredAccountIds), + escapingParameters: (preferredAccountIds), + superclassCall: + + Cuckoo.MockManager.crashOnProtocolSuperclassCall() + , + defaultCall: __defaultImplStub!.allPreferred(for: preferredAccountIds)) + + } + struct __StubbingProxy_ValidatorOperationFactoryProtocol: Cuckoo.StubbingProxy { private let cuckoo_manager: Cuckoo.MockManager @@ -22932,6 +22947,11 @@ import RobinHood return .init(stub: cuckoo_manager.createStub(for: MockValidatorOperationFactoryProtocol.self, method: "wannabeValidatorsOperation(for: [AccountId]) -> CompoundOperationWrapper<[SelectedValidatorInfo]>", parameterMatchers: matchers)) } + func allPreferred(for preferredAccountIds: M1) -> Cuckoo.ProtocolStubFunction<([AccountId]), CompoundOperationWrapper> where M1.MatchedType == [AccountId] { + let matchers: [Cuckoo.ParameterMatcher<([AccountId])>] = [wrap(matchable: preferredAccountIds) { $0 }] + return .init(stub: cuckoo_manager.createStub(for: MockValidatorOperationFactoryProtocol.self, method: "allPreferred(for: [AccountId]) -> CompoundOperationWrapper", parameterMatchers: matchers)) + } + } struct __VerificationProxy_ValidatorOperationFactoryProtocol: Cuckoo.VerificationProxy { @@ -22978,6 +22998,12 @@ import RobinHood return cuckoo_manager.verify("wannabeValidatorsOperation(for: [AccountId]) -> CompoundOperationWrapper<[SelectedValidatorInfo]>", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } + @discardableResult + func allPreferred(for preferredAccountIds: M1) -> Cuckoo.__DoNotUse<([AccountId]), CompoundOperationWrapper> where M1.MatchedType == [AccountId] { + let matchers: [Cuckoo.ParameterMatcher<([AccountId])>] = [wrap(matchable: preferredAccountIds) { $0 }] + return cuckoo_manager.verify("allPreferred(for: [AccountId]) -> CompoundOperationWrapper", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + } + } } @@ -23017,6 +23043,12 @@ import RobinHood return DefaultValueRegistry.defaultValue(for: (CompoundOperationWrapper<[SelectedValidatorInfo]>).self) } + + + func allPreferred(for preferredAccountIds: [AccountId]) -> CompoundOperationWrapper { + return DefaultValueRegistry.defaultValue(for: (CompoundOperationWrapper).self) + } + } @@ -26728,9 +26760,9 @@ import SoraFoundation - func didReceiveValidators(result: Result<[ElectedValidatorInfo], Error>) { + func didReceiveValidators(result: Result) { - return cuckoo_manager.call("didReceiveValidators(result: Result<[ElectedValidatorInfo], Error>)", + return cuckoo_manager.call("didReceiveValidators(result: Result)", parameters: (result), escapingParameters: (result), superclassCall: @@ -26765,9 +26797,9 @@ import SoraFoundation } - func didReceiveValidators(result: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(Result<[ElectedValidatorInfo], Error>)> where M1.MatchedType == Result<[ElectedValidatorInfo], Error> { - let matchers: [Cuckoo.ParameterMatcher<(Result<[ElectedValidatorInfo], Error>)>] = [wrap(matchable: result) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockSelectValidatorsStartInteractorOutputProtocol.self, method: "didReceiveValidators(result: Result<[ElectedValidatorInfo], Error>)", parameterMatchers: matchers)) + func didReceiveValidators(result: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(Result)> where M1.MatchedType == Result { + let matchers: [Cuckoo.ParameterMatcher<(Result)>] = [wrap(matchable: result) { $0 }] + return .init(stub: cuckoo_manager.createStub(for: MockSelectValidatorsStartInteractorOutputProtocol.self, method: "didReceiveValidators(result: Result)", parameterMatchers: matchers)) } func didReceiveMaxNominations(result: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(Result)> where M1.MatchedType == Result { @@ -26792,9 +26824,9 @@ import SoraFoundation @discardableResult - func didReceiveValidators(result: M1) -> Cuckoo.__DoNotUse<(Result<[ElectedValidatorInfo], Error>), Void> where M1.MatchedType == Result<[ElectedValidatorInfo], Error> { - let matchers: [Cuckoo.ParameterMatcher<(Result<[ElectedValidatorInfo], Error>)>] = [wrap(matchable: result) { $0 }] - return cuckoo_manager.verify("didReceiveValidators(result: Result<[ElectedValidatorInfo], Error>)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func didReceiveValidators(result: M1) -> Cuckoo.__DoNotUse<(Result), Void> where M1.MatchedType == Result { + let matchers: [Cuckoo.ParameterMatcher<(Result)>] = [wrap(matchable: result) { $0 }] + return cuckoo_manager.verify("didReceiveValidators(result: Result)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -26814,7 +26846,7 @@ import SoraFoundation - func didReceiveValidators(result: Result<[ElectedValidatorInfo], Error>) { + func didReceiveValidators(result: Result) { return DefaultValueRegistry.defaultValue(for: (Void).self) } @@ -35798,21 +35830,6 @@ import Foundation - func showSetupAmount(from view: StakingMainViewProtocol?) { - - return cuckoo_manager.call("showSetupAmount(from: StakingMainViewProtocol?)", - parameters: (view), - escapingParameters: (view), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.showSetupAmount(from: view)) - - } - - - func proceedToSelectValidatorsStart(from view: StakingMainViewProtocol?, existingBonding: ExistingBonding) { return cuckoo_manager.call("proceedToSelectValidatorsStart(from: StakingMainViewProtocol?, existingBonding: ExistingBonding)", @@ -35828,21 +35845,6 @@ import Foundation - func showRewardDetails(from view: ControllerBackedProtocol?, maxReward: Decimal, avgReward: Decimal) { - - return cuckoo_manager.call("showRewardDetails(from: ControllerBackedProtocol?, maxReward: Decimal, avgReward: Decimal)", - parameters: (view, maxReward, avgReward), - escapingParameters: (view, maxReward, avgReward), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.showRewardDetails(from: view, maxReward: maxReward, avgReward: avgReward)) - - } - - - func showRewardPayoutsForNominator(from view: ControllerBackedProtocol?, stashAddress: AccountAddress) { return cuckoo_manager.call("showRewardPayoutsForNominator(from: ControllerBackedProtocol?, stashAddress: AccountAddress)", @@ -36045,21 +36047,11 @@ import Foundation } - func showSetupAmount(from view: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(StakingMainViewProtocol?)> where M1.OptionalMatchedType == StakingMainViewProtocol { - let matchers: [Cuckoo.ParameterMatcher<(StakingMainViewProtocol?)>] = [wrap(matchable: view) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockStakingRelaychainWireframeProtocol.self, method: "showSetupAmount(from: StakingMainViewProtocol?)", parameterMatchers: matchers)) - } - func proceedToSelectValidatorsStart(from view: M1, existingBonding: M2) -> Cuckoo.ProtocolStubNoReturnFunction<(StakingMainViewProtocol?, ExistingBonding)> where M1.OptionalMatchedType == StakingMainViewProtocol, M2.MatchedType == ExistingBonding { let matchers: [Cuckoo.ParameterMatcher<(StakingMainViewProtocol?, ExistingBonding)>] = [wrap(matchable: view) { $0.0 }, wrap(matchable: existingBonding) { $0.1 }] return .init(stub: cuckoo_manager.createStub(for: MockStakingRelaychainWireframeProtocol.self, method: "proceedToSelectValidatorsStart(from: StakingMainViewProtocol?, existingBonding: ExistingBonding)", parameterMatchers: matchers)) } - func showRewardDetails(from view: M1, maxReward: M2, avgReward: M3) -> Cuckoo.ProtocolStubNoReturnFunction<(ControllerBackedProtocol?, Decimal, Decimal)> where M1.OptionalMatchedType == ControllerBackedProtocol, M2.MatchedType == Decimal, M3.MatchedType == Decimal { - let matchers: [Cuckoo.ParameterMatcher<(ControllerBackedProtocol?, Decimal, Decimal)>] = [wrap(matchable: view) { $0.0 }, wrap(matchable: maxReward) { $0.1 }, wrap(matchable: avgReward) { $0.2 }] - return .init(stub: cuckoo_manager.createStub(for: MockStakingRelaychainWireframeProtocol.self, method: "showRewardDetails(from: ControllerBackedProtocol?, maxReward: Decimal, avgReward: Decimal)", parameterMatchers: matchers)) - } - func showRewardPayoutsForNominator(from view: M1, stashAddress: M2) -> Cuckoo.ProtocolStubNoReturnFunction<(ControllerBackedProtocol?, AccountAddress)> where M1.OptionalMatchedType == ControllerBackedProtocol, M2.MatchedType == AccountAddress { let matchers: [Cuckoo.ParameterMatcher<(ControllerBackedProtocol?, AccountAddress)>] = [wrap(matchable: view) { $0.0 }, wrap(matchable: stashAddress) { $0.1 }] return .init(stub: cuckoo_manager.createStub(for: MockStakingRelaychainWireframeProtocol.self, method: "showRewardPayoutsForNominator(from: ControllerBackedProtocol?, stashAddress: AccountAddress)", parameterMatchers: matchers)) @@ -36141,24 +36133,12 @@ import Foundation - @discardableResult - func showSetupAmount(from view: M1) -> Cuckoo.__DoNotUse<(StakingMainViewProtocol?), Void> where M1.OptionalMatchedType == StakingMainViewProtocol { - let matchers: [Cuckoo.ParameterMatcher<(StakingMainViewProtocol?)>] = [wrap(matchable: view) { $0 }] - return cuckoo_manager.verify("showSetupAmount(from: StakingMainViewProtocol?)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - @discardableResult func proceedToSelectValidatorsStart(from view: M1, existingBonding: M2) -> Cuckoo.__DoNotUse<(StakingMainViewProtocol?, ExistingBonding), Void> where M1.OptionalMatchedType == StakingMainViewProtocol, M2.MatchedType == ExistingBonding { let matchers: [Cuckoo.ParameterMatcher<(StakingMainViewProtocol?, ExistingBonding)>] = [wrap(matchable: view) { $0.0 }, wrap(matchable: existingBonding) { $0.1 }] return cuckoo_manager.verify("proceedToSelectValidatorsStart(from: StakingMainViewProtocol?, existingBonding: ExistingBonding)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } - @discardableResult - func showRewardDetails(from view: M1, maxReward: M2, avgReward: M3) -> Cuckoo.__DoNotUse<(ControllerBackedProtocol?, Decimal, Decimal), Void> where M1.OptionalMatchedType == ControllerBackedProtocol, M2.MatchedType == Decimal, M3.MatchedType == Decimal { - let matchers: [Cuckoo.ParameterMatcher<(ControllerBackedProtocol?, Decimal, Decimal)>] = [wrap(matchable: view) { $0.0 }, wrap(matchable: maxReward) { $0.1 }, wrap(matchable: avgReward) { $0.2 }] - return cuckoo_manager.verify("showRewardDetails(from: ControllerBackedProtocol?, maxReward: Decimal, avgReward: Decimal)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - @discardableResult func showRewardPayoutsForNominator(from view: M1, stashAddress: M2) -> Cuckoo.__DoNotUse<(ControllerBackedProtocol?, AccountAddress), Void> where M1.OptionalMatchedType == ControllerBackedProtocol, M2.MatchedType == AccountAddress { let matchers: [Cuckoo.ParameterMatcher<(ControllerBackedProtocol?, AccountAddress)>] = [wrap(matchable: view) { $0.0 }, wrap(matchable: stashAddress) { $0.1 }] @@ -36248,24 +36228,12 @@ import Foundation - func showSetupAmount(from view: StakingMainViewProtocol?) { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - func proceedToSelectValidatorsStart(from view: StakingMainViewProtocol?, existingBonding: ExistingBonding) { return DefaultValueRegistry.defaultValue(for: (Void).self) } - func showRewardDetails(from view: ControllerBackedProtocol?, maxReward: Decimal, avgReward: Decimal) { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - func showRewardPayoutsForNominator(from view: ControllerBackedProtocol?, stashAddress: AccountAddress) { return DefaultValueRegistry.defaultValue(for: (Void).self) } @@ -36445,9 +36413,9 @@ import SoraFoundation - func didRecieveNetworkStakingInfo(viewModel: LocalizableResource?) { + func didRecieveNetworkStakingInfo(viewModel: NetworkStakingInfoViewModel) { - return cuckoo_manager.call("didRecieveNetworkStakingInfo(viewModel: LocalizableResource?)", + return cuckoo_manager.call("didRecieveNetworkStakingInfo(viewModel: NetworkStakingInfoViewModel)", parameters: (viewModel), escapingParameters: (viewModel), superclassCall: @@ -36505,6 +36473,21 @@ import SoraFoundation + func didReceiveSelectedEntity(_ entity: StakingSelectedEntityViewModel) { + + return cuckoo_manager.call("didReceiveSelectedEntity(_: StakingSelectedEntityViewModel)", + parameters: (entity), + escapingParameters: (entity), + superclassCall: + + Cuckoo.MockManager.crashOnProtocolSuperclassCall() + , + defaultCall: __defaultImplStub!.didReceiveSelectedEntity(entity)) + + } + + + func didEditRewardFilters() { return cuckoo_manager.call("didEditRewardFilters()", @@ -36562,9 +36545,9 @@ import SoraFoundation return .init(stub: cuckoo_manager.createStub(for: MockStakingMainViewProtocol.self, method: "didReceive(viewModel: StakingMainViewModel)", parameterMatchers: matchers)) } - func didRecieveNetworkStakingInfo(viewModel: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(LocalizableResource?)> where M1.OptionalMatchedType == LocalizableResource { - let matchers: [Cuckoo.ParameterMatcher<(LocalizableResource?)>] = [wrap(matchable: viewModel) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockStakingMainViewProtocol.self, method: "didRecieveNetworkStakingInfo(viewModel: LocalizableResource?)", parameterMatchers: matchers)) + func didRecieveNetworkStakingInfo(viewModel: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(NetworkStakingInfoViewModel)> where M1.MatchedType == NetworkStakingInfoViewModel { + let matchers: [Cuckoo.ParameterMatcher<(NetworkStakingInfoViewModel)>] = [wrap(matchable: viewModel) { $0 }] + return .init(stub: cuckoo_manager.createStub(for: MockStakingMainViewProtocol.self, method: "didRecieveNetworkStakingInfo(viewModel: NetworkStakingInfoViewModel)", parameterMatchers: matchers)) } func didReceiveStakingState(viewModel: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(StakingViewState)> where M1.MatchedType == StakingViewState { @@ -36582,6 +36565,11 @@ import SoraFoundation return .init(stub: cuckoo_manager.createStub(for: MockStakingMainViewProtocol.self, method: "didReceiveStatics(viewModel: StakingMainStaticViewModelProtocol)", parameterMatchers: matchers)) } + func didReceiveSelectedEntity(_ entity: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(StakingSelectedEntityViewModel)> where M1.MatchedType == StakingSelectedEntityViewModel { + let matchers: [Cuckoo.ParameterMatcher<(StakingSelectedEntityViewModel)>] = [wrap(matchable: entity) { $0 }] + return .init(stub: cuckoo_manager.createStub(for: MockStakingMainViewProtocol.self, method: "didReceiveSelectedEntity(_: StakingSelectedEntityViewModel)", parameterMatchers: matchers)) + } + func didEditRewardFilters() -> Cuckoo.ProtocolStubNoReturnFunction<()> { let matchers: [Cuckoo.ParameterMatcher] = [] return .init(stub: cuckoo_manager.createStub(for: MockStakingMainViewProtocol.self, method: "didEditRewardFilters()", parameterMatchers: matchers)) @@ -36630,9 +36618,9 @@ import SoraFoundation } @discardableResult - func didRecieveNetworkStakingInfo(viewModel: M1) -> Cuckoo.__DoNotUse<(LocalizableResource?), Void> where M1.OptionalMatchedType == LocalizableResource { - let matchers: [Cuckoo.ParameterMatcher<(LocalizableResource?)>] = [wrap(matchable: viewModel) { $0 }] - return cuckoo_manager.verify("didRecieveNetworkStakingInfo(viewModel: LocalizableResource?)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func didRecieveNetworkStakingInfo(viewModel: M1) -> Cuckoo.__DoNotUse<(NetworkStakingInfoViewModel), Void> where M1.MatchedType == NetworkStakingInfoViewModel { + let matchers: [Cuckoo.ParameterMatcher<(NetworkStakingInfoViewModel)>] = [wrap(matchable: viewModel) { $0 }] + return cuckoo_manager.verify("didRecieveNetworkStakingInfo(viewModel: NetworkStakingInfoViewModel)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -36653,6 +36641,12 @@ import SoraFoundation return cuckoo_manager.verify("didReceiveStatics(viewModel: StakingMainStaticViewModelProtocol)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } + @discardableResult + func didReceiveSelectedEntity(_ entity: M1) -> Cuckoo.__DoNotUse<(StakingSelectedEntityViewModel), Void> where M1.MatchedType == StakingSelectedEntityViewModel { + let matchers: [Cuckoo.ParameterMatcher<(StakingSelectedEntityViewModel)>] = [wrap(matchable: entity) { $0 }] + return cuckoo_manager.verify("didReceiveSelectedEntity(_: StakingSelectedEntityViewModel)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + } + @discardableResult func didEditRewardFilters() -> Cuckoo.__DoNotUse<(), Void> { let matchers: [Cuckoo.ParameterMatcher] = [] @@ -36711,7 +36705,7 @@ import SoraFoundation - func didRecieveNetworkStakingInfo(viewModel: LocalizableResource?) { + func didRecieveNetworkStakingInfo(viewModel: NetworkStakingInfoViewModel) { return DefaultValueRegistry.defaultValue(for: (Void).self) } @@ -36735,6 +36729,12 @@ import SoraFoundation + func didReceiveSelectedEntity(_ entity: StakingSelectedEntityViewModel) { + return DefaultValueRegistry.defaultValue(for: (Void).self) + } + + + func didEditRewardFilters() { return DefaultValueRegistry.defaultValue(for: (Void).self) } @@ -36789,151 +36789,106 @@ import SoraFoundation - func performMainAction() { - - return cuckoo_manager.call("performMainAction()", - parameters: (), - escapingParameters: (), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.performMainAction()) - - } - - - - func performRewardInfoAction() { + func performRedeemAction() { - return cuckoo_manager.call("performRewardInfoAction()", + return cuckoo_manager.call("performRedeemAction()", parameters: (), escapingParameters: (), superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall() , - defaultCall: __defaultImplStub!.performRewardInfoAction()) + defaultCall: __defaultImplStub!.performRedeemAction()) } - func performChangeValidatorsAction() { + func performRebondAction() { - return cuckoo_manager.call("performChangeValidatorsAction()", + return cuckoo_manager.call("performRebondAction()", parameters: (), escapingParameters: (), superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall() , - defaultCall: __defaultImplStub!.performChangeValidatorsAction()) + defaultCall: __defaultImplStub!.performRebondAction()) } - func performSetupValidatorsForBondedAction() { + func performClaimRewards() { - return cuckoo_manager.call("performSetupValidatorsForBondedAction()", + return cuckoo_manager.call("performClaimRewards()", parameters: (), escapingParameters: (), superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall() , - defaultCall: __defaultImplStub!.performSetupValidatorsForBondedAction()) + defaultCall: __defaultImplStub!.performClaimRewards()) } - func performStakeMoreAction() { + func networkInfoViewDidChangeExpansion(isExpanded: Bool) { - return cuckoo_manager.call("performStakeMoreAction()", - parameters: (), - escapingParameters: (), + return cuckoo_manager.call("networkInfoViewDidChangeExpansion(isExpanded: Bool)", + parameters: (isExpanded), + escapingParameters: (isExpanded), superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall() , - defaultCall: __defaultImplStub!.performStakeMoreAction()) + defaultCall: __defaultImplStub!.networkInfoViewDidChangeExpansion(isExpanded: isExpanded)) } - func performRedeemAction() { + func performManageAction(_ action: StakingManageOption) { - return cuckoo_manager.call("performRedeemAction()", - parameters: (), - escapingParameters: (), + return cuckoo_manager.call("performManageAction(_: StakingManageOption)", + parameters: (action), + escapingParameters: (action), superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall() , - defaultCall: __defaultImplStub!.performRedeemAction()) + defaultCall: __defaultImplStub!.performManageAction(action)) } - func performRebondAction() { + func performAlertAction(_ alert: StakingAlert) { - return cuckoo_manager.call("performRebondAction()", - parameters: (), - escapingParameters: (), + return cuckoo_manager.call("performAlertAction(_: StakingAlert)", + parameters: (alert), + escapingParameters: (alert), superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall() , - defaultCall: __defaultImplStub!.performRebondAction()) + defaultCall: __defaultImplStub!.performAlertAction(alert)) } - func performRebag() { + func performSelectedEntityAction() { - return cuckoo_manager.call("performRebag()", + return cuckoo_manager.call("performSelectedEntityAction()", parameters: (), escapingParameters: (), superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall() , - defaultCall: __defaultImplStub!.performRebag()) - - } - - - - func networkInfoViewDidChangeExpansion(isExpanded: Bool) { - - return cuckoo_manager.call("networkInfoViewDidChangeExpansion(isExpanded: Bool)", - parameters: (isExpanded), - escapingParameters: (isExpanded), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.networkInfoViewDidChangeExpansion(isExpanded: isExpanded)) - - } - - - - func performManageAction(_ action: StakingManageOption) { - - return cuckoo_manager.call("performManageAction(_: StakingManageOption)", - parameters: (action), - escapingParameters: (action), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.performManageAction(action)) + defaultCall: __defaultImplStub!.performSelectedEntityAction()) } @@ -36966,31 +36921,6 @@ import SoraFoundation return .init(stub: cuckoo_manager.createStub(for: MockStakingMainPresenterProtocol.self, method: "setup()", parameterMatchers: matchers)) } - func performMainAction() -> Cuckoo.ProtocolStubNoReturnFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockStakingMainPresenterProtocol.self, method: "performMainAction()", parameterMatchers: matchers)) - } - - func performRewardInfoAction() -> Cuckoo.ProtocolStubNoReturnFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockStakingMainPresenterProtocol.self, method: "performRewardInfoAction()", parameterMatchers: matchers)) - } - - func performChangeValidatorsAction() -> Cuckoo.ProtocolStubNoReturnFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockStakingMainPresenterProtocol.self, method: "performChangeValidatorsAction()", parameterMatchers: matchers)) - } - - func performSetupValidatorsForBondedAction() -> Cuckoo.ProtocolStubNoReturnFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockStakingMainPresenterProtocol.self, method: "performSetupValidatorsForBondedAction()", parameterMatchers: matchers)) - } - - func performStakeMoreAction() -> Cuckoo.ProtocolStubNoReturnFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockStakingMainPresenterProtocol.self, method: "performStakeMoreAction()", parameterMatchers: matchers)) - } - func performRedeemAction() -> Cuckoo.ProtocolStubNoReturnFunction<()> { let matchers: [Cuckoo.ParameterMatcher] = [] return .init(stub: cuckoo_manager.createStub(for: MockStakingMainPresenterProtocol.self, method: "performRedeemAction()", parameterMatchers: matchers)) @@ -37001,9 +36931,9 @@ import SoraFoundation return .init(stub: cuckoo_manager.createStub(for: MockStakingMainPresenterProtocol.self, method: "performRebondAction()", parameterMatchers: matchers)) } - func performRebag() -> Cuckoo.ProtocolStubNoReturnFunction<()> { + func performClaimRewards() -> Cuckoo.ProtocolStubNoReturnFunction<()> { let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockStakingMainPresenterProtocol.self, method: "performRebag()", parameterMatchers: matchers)) + return .init(stub: cuckoo_manager.createStub(for: MockStakingMainPresenterProtocol.self, method: "performClaimRewards()", parameterMatchers: matchers)) } func networkInfoViewDidChangeExpansion(isExpanded: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(Bool)> where M1.MatchedType == Bool { @@ -37016,6 +36946,16 @@ import SoraFoundation return .init(stub: cuckoo_manager.createStub(for: MockStakingMainPresenterProtocol.self, method: "performManageAction(_: StakingManageOption)", parameterMatchers: matchers)) } + func performAlertAction(_ alert: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(StakingAlert)> where M1.MatchedType == StakingAlert { + let matchers: [Cuckoo.ParameterMatcher<(StakingAlert)>] = [wrap(matchable: alert) { $0 }] + return .init(stub: cuckoo_manager.createStub(for: MockStakingMainPresenterProtocol.self, method: "performAlertAction(_: StakingAlert)", parameterMatchers: matchers)) + } + + func performSelectedEntityAction() -> Cuckoo.ProtocolStubNoReturnFunction<()> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return .init(stub: cuckoo_manager.createStub(for: MockStakingMainPresenterProtocol.self, method: "performSelectedEntityAction()", parameterMatchers: matchers)) + } + func selectPeriod() -> Cuckoo.ProtocolStubNoReturnFunction<()> { let matchers: [Cuckoo.ParameterMatcher] = [] return .init(stub: cuckoo_manager.createStub(for: MockStakingMainPresenterProtocol.self, method: "selectPeriod()", parameterMatchers: matchers)) @@ -37043,36 +36983,6 @@ import SoraFoundation return cuckoo_manager.verify("setup()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } - @discardableResult - func performMainAction() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify("performMainAction()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - @discardableResult - func performRewardInfoAction() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify("performRewardInfoAction()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - @discardableResult - func performChangeValidatorsAction() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify("performChangeValidatorsAction()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - @discardableResult - func performSetupValidatorsForBondedAction() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify("performSetupValidatorsForBondedAction()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - @discardableResult - func performStakeMoreAction() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify("performStakeMoreAction()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - @discardableResult func performRedeemAction() -> Cuckoo.__DoNotUse<(), Void> { let matchers: [Cuckoo.ParameterMatcher] = [] @@ -37086,9 +36996,9 @@ import SoraFoundation } @discardableResult - func performRebag() -> Cuckoo.__DoNotUse<(), Void> { + func performClaimRewards() -> Cuckoo.__DoNotUse<(), Void> { let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify("performRebag()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + return cuckoo_manager.verify("performClaimRewards()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -37103,6 +37013,18 @@ import SoraFoundation return cuckoo_manager.verify("performManageAction(_: StakingManageOption)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } + @discardableResult + func performAlertAction(_ alert: M1) -> Cuckoo.__DoNotUse<(StakingAlert), Void> where M1.MatchedType == StakingAlert { + let matchers: [Cuckoo.ParameterMatcher<(StakingAlert)>] = [wrap(matchable: alert) { $0 }] + return cuckoo_manager.verify("performAlertAction(_: StakingAlert)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + } + + @discardableResult + func performSelectedEntityAction() -> Cuckoo.__DoNotUse<(), Void> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return cuckoo_manager.verify("performSelectedEntityAction()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + } + @discardableResult func selectPeriod() -> Cuckoo.__DoNotUse<(), Void> { let matchers: [Cuckoo.ParameterMatcher] = [] @@ -37126,61 +37048,43 @@ import SoraFoundation - func performMainAction() { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - func performRewardInfoAction() { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - func performChangeValidatorsAction() { + func performRedeemAction() { return DefaultValueRegistry.defaultValue(for: (Void).self) } - func performSetupValidatorsForBondedAction() { + func performRebondAction() { return DefaultValueRegistry.defaultValue(for: (Void).self) } - func performStakeMoreAction() { + func performClaimRewards() { return DefaultValueRegistry.defaultValue(for: (Void).self) } - func performRedeemAction() { + func networkInfoViewDidChangeExpansion(isExpanded: Bool) { return DefaultValueRegistry.defaultValue(for: (Void).self) } - func performRebondAction() { + func performManageAction(_ action: StakingManageOption) { return DefaultValueRegistry.defaultValue(for: (Void).self) } - func performRebag() { + func performAlertAction(_ alert: StakingAlert) { return DefaultValueRegistry.defaultValue(for: (Void).self) } - func networkInfoViewDidChangeExpansion(isExpanded: Bool) { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - func performManageAction(_ action: StakingManageOption) { + func performSelectedEntityAction() { return DefaultValueRegistry.defaultValue(for: (Void).self) } @@ -37705,136 +37609,91 @@ import SoraFoundation - func performMainAction() { - - return cuckoo_manager.call("performMainAction()", - parameters: (), - escapingParameters: (), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.performMainAction()) - - } - - - - func performRewardInfoAction() { - - return cuckoo_manager.call("performRewardInfoAction()", - parameters: (), - escapingParameters: (), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.performRewardInfoAction()) - - } - - - - func performChangeValidatorsAction() { + func performRedeemAction() { - return cuckoo_manager.call("performChangeValidatorsAction()", + return cuckoo_manager.call("performRedeemAction()", parameters: (), escapingParameters: (), superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall() , - defaultCall: __defaultImplStub!.performChangeValidatorsAction()) + defaultCall: __defaultImplStub!.performRedeemAction()) } - func performSetupValidatorsForBondedAction() { + func performRebondAction() { - return cuckoo_manager.call("performSetupValidatorsForBondedAction()", + return cuckoo_manager.call("performRebondAction()", parameters: (), escapingParameters: (), superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall() , - defaultCall: __defaultImplStub!.performSetupValidatorsForBondedAction()) + defaultCall: __defaultImplStub!.performRebondAction()) } - func performStakeMoreAction() { + func performClaimRewards() { - return cuckoo_manager.call("performStakeMoreAction()", + return cuckoo_manager.call("performClaimRewards()", parameters: (), escapingParameters: (), superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall() , - defaultCall: __defaultImplStub!.performStakeMoreAction()) + defaultCall: __defaultImplStub!.performClaimRewards()) } - func performRedeemAction() { + func performManageAction(_ action: StakingManageOption) { - return cuckoo_manager.call("performRedeemAction()", - parameters: (), - escapingParameters: (), + return cuckoo_manager.call("performManageAction(_: StakingManageOption)", + parameters: (action), + escapingParameters: (action), superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall() , - defaultCall: __defaultImplStub!.performRedeemAction()) + defaultCall: __defaultImplStub!.performManageAction(action)) } - func performRebondAction() { + func performAlertAction(_ alert: StakingAlert) { - return cuckoo_manager.call("performRebondAction()", - parameters: (), - escapingParameters: (), + return cuckoo_manager.call("performAlertAction(_: StakingAlert)", + parameters: (alert), + escapingParameters: (alert), superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall() , - defaultCall: __defaultImplStub!.performRebondAction()) + defaultCall: __defaultImplStub!.performAlertAction(alert)) } - func performRebag() { + func performSelectedEntityAction() { - return cuckoo_manager.call("performRebag()", + return cuckoo_manager.call("performSelectedEntityAction()", parameters: (), escapingParameters: (), superclassCall: Cuckoo.MockManager.crashOnProtocolSuperclassCall() , - defaultCall: __defaultImplStub!.performRebag()) - - } - - - - func performManageAction(_ action: StakingManageOption) { - - return cuckoo_manager.call("performManageAction(_: StakingManageOption)", - parameters: (action), - escapingParameters: (action), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.performManageAction(action)) + defaultCall: __defaultImplStub!.performSelectedEntityAction()) } @@ -37867,31 +37726,6 @@ import SoraFoundation return .init(stub: cuckoo_manager.createStub(for: MockStakingMainChildPresenterProtocol.self, method: "setup()", parameterMatchers: matchers)) } - func performMainAction() -> Cuckoo.ProtocolStubNoReturnFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockStakingMainChildPresenterProtocol.self, method: "performMainAction()", parameterMatchers: matchers)) - } - - func performRewardInfoAction() -> Cuckoo.ProtocolStubNoReturnFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockStakingMainChildPresenterProtocol.self, method: "performRewardInfoAction()", parameterMatchers: matchers)) - } - - func performChangeValidatorsAction() -> Cuckoo.ProtocolStubNoReturnFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockStakingMainChildPresenterProtocol.self, method: "performChangeValidatorsAction()", parameterMatchers: matchers)) - } - - func performSetupValidatorsForBondedAction() -> Cuckoo.ProtocolStubNoReturnFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockStakingMainChildPresenterProtocol.self, method: "performSetupValidatorsForBondedAction()", parameterMatchers: matchers)) - } - - func performStakeMoreAction() -> Cuckoo.ProtocolStubNoReturnFunction<()> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockStakingMainChildPresenterProtocol.self, method: "performStakeMoreAction()", parameterMatchers: matchers)) - } - func performRedeemAction() -> Cuckoo.ProtocolStubNoReturnFunction<()> { let matchers: [Cuckoo.ParameterMatcher] = [] return .init(stub: cuckoo_manager.createStub(for: MockStakingMainChildPresenterProtocol.self, method: "performRedeemAction()", parameterMatchers: matchers)) @@ -37902,9 +37736,9 @@ import SoraFoundation return .init(stub: cuckoo_manager.createStub(for: MockStakingMainChildPresenterProtocol.self, method: "performRebondAction()", parameterMatchers: matchers)) } - func performRebag() -> Cuckoo.ProtocolStubNoReturnFunction<()> { + func performClaimRewards() -> Cuckoo.ProtocolStubNoReturnFunction<()> { let matchers: [Cuckoo.ParameterMatcher] = [] - return .init(stub: cuckoo_manager.createStub(for: MockStakingMainChildPresenterProtocol.self, method: "performRebag()", parameterMatchers: matchers)) + return .init(stub: cuckoo_manager.createStub(for: MockStakingMainChildPresenterProtocol.self, method: "performClaimRewards()", parameterMatchers: matchers)) } func performManageAction(_ action: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(StakingManageOption)> where M1.MatchedType == StakingManageOption { @@ -37912,6 +37746,16 @@ import SoraFoundation return .init(stub: cuckoo_manager.createStub(for: MockStakingMainChildPresenterProtocol.self, method: "performManageAction(_: StakingManageOption)", parameterMatchers: matchers)) } + func performAlertAction(_ alert: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(StakingAlert)> where M1.MatchedType == StakingAlert { + let matchers: [Cuckoo.ParameterMatcher<(StakingAlert)>] = [wrap(matchable: alert) { $0 }] + return .init(stub: cuckoo_manager.createStub(for: MockStakingMainChildPresenterProtocol.self, method: "performAlertAction(_: StakingAlert)", parameterMatchers: matchers)) + } + + func performSelectedEntityAction() -> Cuckoo.ProtocolStubNoReturnFunction<()> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return .init(stub: cuckoo_manager.createStub(for: MockStakingMainChildPresenterProtocol.self, method: "performSelectedEntityAction()", parameterMatchers: matchers)) + } + func selectPeriod(_ period: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(StakingRewardFiltersPeriod)> where M1.MatchedType == StakingRewardFiltersPeriod { let matchers: [Cuckoo.ParameterMatcher<(StakingRewardFiltersPeriod)>] = [wrap(matchable: period) { $0 }] return .init(stub: cuckoo_manager.createStub(for: MockStakingMainChildPresenterProtocol.self, method: "selectPeriod(_: StakingRewardFiltersPeriod)", parameterMatchers: matchers)) @@ -37939,36 +37783,6 @@ import SoraFoundation return cuckoo_manager.verify("setup()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } - @discardableResult - func performMainAction() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify("performMainAction()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - @discardableResult - func performRewardInfoAction() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify("performRewardInfoAction()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - @discardableResult - func performChangeValidatorsAction() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify("performChangeValidatorsAction()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - @discardableResult - func performSetupValidatorsForBondedAction() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify("performSetupValidatorsForBondedAction()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - - @discardableResult - func performStakeMoreAction() -> Cuckoo.__DoNotUse<(), Void> { - let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify("performStakeMoreAction()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) - } - @discardableResult func performRedeemAction() -> Cuckoo.__DoNotUse<(), Void> { let matchers: [Cuckoo.ParameterMatcher] = [] @@ -37982,9 +37796,9 @@ import SoraFoundation } @discardableResult - func performRebag() -> Cuckoo.__DoNotUse<(), Void> { + func performClaimRewards() -> Cuckoo.__DoNotUse<(), Void> { let matchers: [Cuckoo.ParameterMatcher] = [] - return cuckoo_manager.verify("performRebag()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + return cuckoo_manager.verify("performClaimRewards()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -37993,6 +37807,18 @@ import SoraFoundation return cuckoo_manager.verify("performManageAction(_: StakingManageOption)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } + @discardableResult + func performAlertAction(_ alert: M1) -> Cuckoo.__DoNotUse<(StakingAlert), Void> where M1.MatchedType == StakingAlert { + let matchers: [Cuckoo.ParameterMatcher<(StakingAlert)>] = [wrap(matchable: alert) { $0 }] + return cuckoo_manager.verify("performAlertAction(_: StakingAlert)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + } + + @discardableResult + func performSelectedEntityAction() -> Cuckoo.__DoNotUse<(), Void> { + let matchers: [Cuckoo.ParameterMatcher] = [] + return cuckoo_manager.verify("performSelectedEntityAction()", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + } + @discardableResult func selectPeriod(_ period: M1) -> Cuckoo.__DoNotUse<(StakingRewardFiltersPeriod), Void> where M1.MatchedType == StakingRewardFiltersPeriod { let matchers: [Cuckoo.ParameterMatcher<(StakingRewardFiltersPeriod)>] = [wrap(matchable: period) { $0 }] @@ -38016,55 +37842,37 @@ import SoraFoundation - func performMainAction() { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - func performRewardInfoAction() { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - func performChangeValidatorsAction() { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - - func performSetupValidatorsForBondedAction() { + func performRedeemAction() { return DefaultValueRegistry.defaultValue(for: (Void).self) } - func performStakeMoreAction() { + func performRebondAction() { return DefaultValueRegistry.defaultValue(for: (Void).self) } - func performRedeemAction() { + func performClaimRewards() { return DefaultValueRegistry.defaultValue(for: (Void).self) } - func performRebondAction() { + func performManageAction(_ action: StakingManageOption) { return DefaultValueRegistry.defaultValue(for: (Void).self) } - func performRebag() { + func performAlertAction(_ alert: StakingAlert) { return DefaultValueRegistry.defaultValue(for: (Void).self) } - func performManageAction(_ action: StakingManageOption) { + func performSelectedEntityAction() { return DefaultValueRegistry.defaultValue(for: (Void).self) } diff --git a/novawalletTests/Modules/ControllerAccount/ControllerAccountTests.swift b/novawalletTests/Modules/ControllerAccount/ControllerAccountTests.swift index 306d87cd53..bf42994c91 100644 --- a/novawalletTests/Modules/ControllerAccount/ControllerAccountTests.swift +++ b/novawalletTests/Modules/ControllerAccount/ControllerAccountTests.swift @@ -73,7 +73,7 @@ class ControllerAccountTests: XCTestCase { let controllerAddress = try Data.random(of: 32)!.toAddress(using: chain.chainFormat) let stashAddress = try Data.random(of: 32)!.toAddress(using: chain.chainFormat) - let stashItem = StashItem(stash: stashAddress, controller: controllerAddress) + let stashItem = StashItem(stash: stashAddress, controller: controllerAddress, chainId: chain.chainId) presenter.didReceiveStashItem(result: .success(stashItem)) let controllerId = try controllerAddress.toAccountId() diff --git a/novawalletTests/Modules/Staking/BondMoreConfirm/BondMoreConfirmTests.swift b/novawalletTests/Modules/Staking/BondMoreConfirm/BondMoreConfirmTests.swift index a0d33fc508..93b8bc020d 100644 --- a/novawalletTests/Modules/Staking/BondMoreConfirm/BondMoreConfirmTests.swift +++ b/novawalletTests/Modules/Staking/BondMoreConfirm/BondMoreConfirmTests.swift @@ -69,7 +69,7 @@ class BondMoreConfirmTests: XCTestCase { let metaAccount = AccountGenerator.generateMetaAccount() let accountResponse = metaAccount.fetch(for: chain.accountRequest())! let selectedAddress = accountResponse.toAddress()! - let stashItem = StashItem(stash: selectedAddress, controller: selectedAddress) + let stashItem = StashItem(stash: selectedAddress, controller: selectedAddress, chainId: chain.chainId) let userDataStorage = UserDataStorageTestFacade() let accountRepositoryFactory = AccountRepositoryFactory(storageFacade: userDataStorage) diff --git a/novawalletTests/Modules/Staking/SelectValidatorsFlow/CustomValidators/CustomValidatorListComposerTests.swift b/novawalletTests/Modules/Staking/SelectValidatorsFlow/CustomValidators/CustomValidatorListComposerTests.swift index aed3c9b63b..15430e9524 100644 --- a/novawalletTests/Modules/Staking/SelectValidatorsFlow/CustomValidators/CustomValidatorListComposerTests.swift +++ b/novawalletTests/Modules/Staking/SelectValidatorsFlow/CustomValidators/CustomValidatorListComposerTests.swift @@ -18,7 +18,7 @@ class CustomValidatorListComposerTests: XCTestCase { // when - let result = composer.compose(from: allValidators) + let result = composer.compose(from: allValidators, preferrences: []) //then @@ -45,7 +45,7 @@ class CustomValidatorListComposerTests: XCTestCase { // when - let result = composer.compose(from: allValidators) + let result = composer.compose(from: allValidators, preferrences: []) //then @@ -66,7 +66,7 @@ class CustomValidatorListComposerTests: XCTestCase { // when - let result = composer.compose(from: allValidators) + let result = composer.compose(from: allValidators, preferrences: []) //then @@ -90,7 +90,7 @@ class CustomValidatorListComposerTests: XCTestCase { // when - let result = composer.compose(from: allValidators) + let result = composer.compose(from: allValidators, preferrences: []) //then @@ -118,7 +118,7 @@ class CustomValidatorListComposerTests: XCTestCase { // when - let result = composer.compose(from: allValidators) + let result = composer.compose(from: allValidators, preferrences: []) //then @@ -151,7 +151,62 @@ class CustomValidatorListComposerTests: XCTestCase { // when - let result = composer.compose(from: allValidators) + let result = composer.compose(from: allValidators, preferrences: []) + + //then + + XCTAssertEqual(result, expectedResult) + } + + func testDefaultFilterWithPreferrences() { + // given + let generator = CustomValidatorListTestDataGenerator.self + let allValidators = generator.createSelectedValidators( + from: generator.goodValidators + + generator.badValidators + ) + + let preferences = generator.createSelectedValidators(from: [generator.clusterValidatorChild1]) + + let expectedResult = (allValidators + preferences).sorted { + $0.stakeReturn >= $1.stakeReturn + } + + let filter = CustomValidatorListFilter.defaultFilter() + let composer = CustomValidatorListComposer(filter: filter) + + // when + + let result = composer.compose(from: allValidators, preferrences: preferences) + + //then + + XCTAssertEqual(result, expectedResult) + } + + func testRecommendedFilterWithPreferrences() { + // given + let generator = CustomValidatorListTestDataGenerator.self + + let allValidators = generator.createSelectedValidators( + from: generator.goodValidators + + generator.badValidators + ) + + let goodValidators = generator.createSelectedValidators(from: generator.goodValidators) + + let preferrences = generator.createSelectedValidators(from: [generator.clusterValidatorChild1]) + + let expectedResult = (goodValidators + preferrences).sorted { + $0.stakeReturn >= $1.stakeReturn + } + + let filter = CustomValidatorListFilter.recommendedFilter(havingIdentity: true) + let composer = CustomValidatorListComposer(filter: filter) + + // when + + let result = composer.compose(from: allValidators, preferrences: preferrences) //then diff --git a/novawalletTests/Modules/Staking/SelectValidatorsFlow/CustomValidators/CustomValidatorListTests.swift b/novawalletTests/Modules/Staking/SelectValidatorsFlow/CustomValidators/CustomValidatorListTests.swift index 40f64f1b4a..20638976f8 100644 --- a/novawalletTests/Modules/Staking/SelectValidatorsFlow/CustomValidators/CustomValidatorListTests.swift +++ b/novawalletTests/Modules/Staking/SelectValidatorsFlow/CustomValidators/CustomValidatorListTests.swift @@ -60,7 +60,7 @@ class CustomValidatorListTests: XCTestCase { wireframe: wireframe, viewModelFactory: viewModelFactory, localizationManager: LocalizationManager.shared, - fullValidatorList: fullValidatorList, + fullValidatorList: .init(allValidators: fullValidatorList, preferredValidators: []), recommendedValidatorList: recommendedValidatorList, selectedValidatorList: SharedList(items: []), validatorsSelectionParams: validatorsSelectionParams diff --git a/novawalletTests/Modules/Staking/SelectValidatorsFlow/RecommendedValidators/RecommendationsComposerTests.swift b/novawalletTests/Modules/Staking/SelectValidatorsFlow/RecommendedValidators/RecommendationsComposerTests.swift index fc9cb8ec30..d760d845bf 100644 --- a/novawalletTests/Modules/Staking/SelectValidatorsFlow/RecommendedValidators/RecommendationsComposerTests.swift +++ b/novawalletTests/Modules/Staking/SelectValidatorsFlow/RecommendedValidators/RecommendationsComposerTests.swift @@ -88,7 +88,7 @@ class RecommendationsComposerTests: XCTestCase { func testClusterRemovalAndFilters() { // given - let expectedValidators: [ElectedValidatorInfo] = [ + let expectedValidators: [SelectedValidatorInfo] = [ ElectedValidatorInfo( address: "5EJQtTE1ZS9cBdqiuUdjQtieNLRVjk7Pyo6Bfv8Ff6e7pnr7", nominators: [], @@ -113,13 +113,13 @@ class RecommendationsComposerTests: XCTestCase { maxNominatorsRewarded: 128, blocked: false ) - ] + ].map({ $0.toSelected(for: nil) }) let composer = RecommendationsComposer(resultSize: 10, clusterSizeLimit: 1) // when - let result = composer.compose(from: allValidators) + let result = composer.compose(from: allValidators.map({ $0.toSelected(for: nil) }), preferrences: []) // then @@ -129,7 +129,7 @@ class RecommendationsComposerTests: XCTestCase { func testMaxSizeApplied() { // given - let expectedValidators: [ElectedValidatorInfo] = [ + let expectedValidators: [SelectedValidatorInfo] = [ ElectedValidatorInfo( address: "5EJQtTE1ZS9cBdqiuUdjQtieNLRVjk7Pyo6Bfv8Ff6e7pnr7", nominators: [], @@ -142,16 +142,85 @@ class RecommendationsComposerTests: XCTestCase { maxNominatorsRewarded: 128, blocked: false ) - ] + ].map({ $0.toSelected(for: nil) }) let composer = RecommendationsComposer(resultSize: 1, clusterSizeLimit: 1) // when - let result = composer.compose(from: allValidators) + let result = composer.compose(from: allValidators.map({ $0.toSelected(for: nil) }), preferrences: []) // then XCTAssertEqual(expectedValidators, result) } + + func testRecommendedAndPreferredValidators() { + // given + + let composer = RecommendationsComposer(resultSize: 2, clusterSizeLimit: 1) + + let generator = CustomValidatorListTestDataGenerator.self + + let preferred = generator.poorGoodValidator.toSelected(for: nil) + let recommended1 = generator.goodValidator.toSelected(for: nil) + let recommended2 = generator.greedyGoodValidator.toSelected(for: nil) + + let recommendations = composer.compose(from: [recommended1, recommended2], preferrences: [preferred]) + + XCTAssertEqual(recommendations.count, composer.resultSize) + XCTAssertTrue(recommendations.contains(where: { $0.address == preferred.address })) + } + + func testPreferredOversubscribedNotIncludedValidators() { + // given + + let composer = RecommendationsComposer(resultSize: 2, clusterSizeLimit: 1) + + let generator = CustomValidatorListTestDataGenerator.self + + let preferred = generator.oversubscribedValidator.toSelected(for: nil) + let recommended1 = generator.goodValidator.toSelected(for: nil) + let recommended2 = generator.greedyGoodValidator.toSelected(for: nil) + + let recommendations = composer.compose(from: [recommended1, recommended2], preferrences: [preferred]) + + let expectedResult = [recommended1, recommended2].sorted { $0.stakeReturn > $1.stakeReturn } + + XCTAssertEqual(recommendations, expectedResult) + } + + func testOnlyPreferredValidators() { + // given + + let composer = RecommendationsComposer(resultSize: 2, clusterSizeLimit: 1) + + let generator = CustomValidatorListTestDataGenerator.self + + let preferred = generator.clusterValidatorChild1.toSelected(for: nil) + + let recommendations = composer.compose(from: [], preferrences: [preferred]) + + XCTAssertEqual(recommendations, [preferred]) + } + + func testOnlyPreferredWithAvailableRecommendedValidators() { + // given + + let composer = RecommendationsComposer(resultSize: 2, clusterSizeLimit: 1) + + let generator = CustomValidatorListTestDataGenerator.self + + let recommended1 = generator.goodValidator.toSelected(for: nil) + let preferred1 = generator.clusterValidatorChild1.toSelected(for: nil) + let preferred2 = generator.clusterValidatorChild1.toSelected(for: nil) + let preferred3 = generator.clusterValidatorChild1.toSelected(for: nil) + + let preferredList = [preferred1, preferred2, preferred3] + let recommendations = composer.compose(from: [recommended1], preferrences: preferredList) + + let expectedList = [preferred1, preferred2] + + XCTAssertEqual(recommendations, expectedList) + } } diff --git a/novawalletTests/Modules/Staking/SelectValidatorsFlow/StartSelectValidators/StartSelectValidatorsTests.swift b/novawalletTests/Modules/Staking/SelectValidatorsFlow/StartSelectValidators/StartSelectValidatorsTests.swift index d69f600a42..095a304402 100644 --- a/novawalletTests/Modules/Staking/SelectValidatorsFlow/StartSelectValidators/StartSelectValidatorsTests.swift +++ b/novawalletTests/Modules/Staking/SelectValidatorsFlow/StartSelectValidators/StartSelectValidatorsTests.swift @@ -54,7 +54,8 @@ class SelectValidatorsStartTests: XCTestCase { let interactor = SelectValidatorsStartInteractor( runtimeService: runtimeService, operationFactory: operationFactory, - operationManager: OperationManager() + operationManager: OperationManager(), + preferredValidators: [] ) let presenter = SelectValidatorsStartPresenter( @@ -71,8 +72,8 @@ class SelectValidatorsStartTests: XCTestCase { // when stub(operationFactory) { stub in - when(stub).allElectedOperation().then { _ in - CompoundOperationWrapper.createWithResult(allValidators) + when(stub).allPreferred(for: any()).then { _ in + CompoundOperationWrapper.createWithResult(.init(electedValidators: allValidators, preferredValidators: [])) } } @@ -100,7 +101,7 @@ class SelectValidatorsStartTests: XCTestCase { expectedCustomValidators.sorted { $0.address.lexicographicallyPrecedes($1.address) }, - selectionValidatorGroups.fullValidatorList.sorted { + selectionValidatorGroups.fullValidatorList.distinctAll().sorted { $0.address.lexicographicallyPrecedes($1.address) }) } diff --git a/novawalletTests/Modules/Staking/SelectValidatorsFlow/YourValidators/YourValidatorListTests.swift b/novawalletTests/Modules/Staking/SelectValidatorsFlow/YourValidators/YourValidatorListTests.swift index 8251f54abb..03ddcc2045 100644 --- a/novawalletTests/Modules/Staking/SelectValidatorsFlow/YourValidators/YourValidatorListTests.swift +++ b/novawalletTests/Modules/Staking/SelectValidatorsFlow/YourValidators/YourValidatorListTests.swift @@ -39,7 +39,7 @@ class YourValidatorListTests: XCTestCase { let saveControllerOperation = accountRepository.saveOperation({ [managedMetaAccount] }, { [] }) operationQueue.addOperations([saveControllerOperation], waitUntilFinished: true) - let stashItem = StashItem(stash: nominatorAddress, controller: nominatorAddress) + let stashItem = StashItem(stash: nominatorAddress, controller: nominatorAddress, chainId: chain.chainId) let stakingLedger = StakingLedger( stash: selectedAccount.accountId, total: BigUInt(16e+12), diff --git a/novawalletTests/Modules/Staking/StakingBondMore/StakingBondMoreTests.swift b/novawalletTests/Modules/Staking/StakingBondMore/StakingBondMoreTests.swift index 8d6157ebd9..9330b66895 100644 --- a/novawalletTests/Modules/Staking/StakingBondMore/StakingBondMoreTests.swift +++ b/novawalletTests/Modules/Staking/StakingBondMore/StakingBondMoreTests.swift @@ -52,7 +52,7 @@ class StakingBondMoreTests: XCTestCase { } // balance & fee is received - let stashItem = StashItem(stash: WestendStub.address, controller: WestendStub.address) + let stashItem = StashItem(stash: WestendStub.address, controller: WestendStub.address, chainId: chain.chainId) let stashAccountId = try stashItem.stash.toAccountId() let assetBalance = AssetBalance( chainAssetId: chain.utilityChainAssetId()!, diff --git a/novawalletTests/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationTests.swift b/novawalletTests/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationTests.swift index 83b0094621..1905393651 100644 --- a/novawalletTests/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationTests.swift +++ b/novawalletTests/Modules/Staking/StakingRebondConfirmation/StakingRebondConfirmationTests.swift @@ -86,7 +86,7 @@ class StakingRebondConfirmationTests: XCTestCase { extrinsicService: ExtrinsicServiceStub.dummy() ) - let stashItem = StashItem(stash: nominatorAddress, controller: nominatorAddress) + let stashItem = StashItem(stash: nominatorAddress, controller: nominatorAddress, chainId: chain.chainId) let stakingLedger = StakingLedger( stash: selectedAccount.accountId, total: BigUInt(3e+12), diff --git a/novawalletTests/Modules/Staking/StakingRebondSetup/StakingRebondSetupTests.swift b/novawalletTests/Modules/Staking/StakingRebondSetup/StakingRebondSetupTests.swift index 1e979b289f..ff881aa33e 100644 --- a/novawalletTests/Modules/Staking/StakingRebondSetup/StakingRebondSetupTests.swift +++ b/novawalletTests/Modules/Staking/StakingRebondSetup/StakingRebondSetupTests.swift @@ -80,7 +80,7 @@ class StakingRebondSetupTests: XCTestCase { extrinsicService: ExtrinsicServiceStub.dummy() ) - let stashItem = StashItem(stash: nominatorAddress, controller: nominatorAddress) + let stashItem = StashItem(stash: nominatorAddress, controller: nominatorAddress, chainId: chain.chainId) let stakingLedger = StakingLedger( stash: selectedAccount.accountId, total: BigUInt(3e+12), diff --git a/novawalletTests/Modules/Staking/StakingRedeem/StakingRedeemTests.swift b/novawalletTests/Modules/Staking/StakingRedeem/StakingRedeemTests.swift index 810d3ec298..f387feb176 100644 --- a/novawalletTests/Modules/Staking/StakingRedeem/StakingRedeemTests.swift +++ b/novawalletTests/Modules/Staking/StakingRedeem/StakingRedeemTests.swift @@ -88,7 +88,7 @@ class StakingRedeemTests: XCTestCase { extrinsicService: ExtrinsicServiceStub.dummy() ) - let stashItem = StashItem(stash: nominatorAddress, controller: nominatorAddress) + let stashItem = StashItem(stash: nominatorAddress, controller: nominatorAddress, chainId: chain.chainId) let stakingLedger = StakingLedger( stash: selectedAccount.accountId, total: BigUInt(3e+12), diff --git a/novawalletTests/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmTests.swift b/novawalletTests/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmTests.swift index 85aeb5e4f5..cc66a21c30 100644 --- a/novawalletTests/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmTests.swift +++ b/novawalletTests/Modules/Staking/StakingRewardDestConfirm/StakingRewardDestConfirmTests.swift @@ -98,7 +98,7 @@ class StakingRewardDestConfirmTests: XCTestCase { let calculatorService = RewardCalculatorServiceStub(engine: WestendStub.rewardCalculator) let address = selectedAccount.toAddress()! - let stashItem = StashItem(stash: address, controller: address) + let stashItem = StashItem(stash: address, controller: address, chainId: chain.chainId) let ledgerInfo = StakingLedger( stash: selectedAccount.accountId, diff --git a/novawalletTests/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestinationSetupTests.swift b/novawalletTests/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestinationSetupTests.swift index 9a6fa2042d..72a2a8aca7 100644 --- a/novawalletTests/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestinationSetupTests.swift +++ b/novawalletTests/Modules/Staking/StakingRewardDestinationSetup/StakingRewardDestinationSetupTests.swift @@ -127,7 +127,7 @@ class StakingRewardDestinationSetupTests: XCTestCase { let calculatorService = RewardCalculatorServiceStub(engine: WestendStub.rewardCalculator) let address = selectedAccount.toAddress()! - let stashItem = StashItem(stash: address, controller: address) + let stashItem = StashItem(stash: address, controller: address, chainId: chain.chainId) let ledgerInfo = StakingLedger( stash: selectedAccount.accountId, total: BigUInt(2e+12), diff --git a/novawalletTests/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmTests.swift b/novawalletTests/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmTests.swift index fea9ae3c42..8e311f8d3c 100644 --- a/novawalletTests/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmTests.swift +++ b/novawalletTests/Modules/Staking/StakingUnbondConfirm/StakingUnbondConfirmTests.swift @@ -92,7 +92,7 @@ class StakingUnbondConfirmTests: XCTestCase { extrinsicService: ExtrinsicServiceStub.dummy() ) - let stashItem = StashItem(stash: nominatorAddress, controller: nominatorAddress) + let stashItem = StashItem(stash: nominatorAddress, controller: nominatorAddress, chainId: chain.chainId) let stakingLedger = StakingLedger( stash: selectedAccount.accountId, total: BigUInt(1e+12), diff --git a/novawalletTests/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupTests.swift b/novawalletTests/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupTests.swift index 4e65db6934..c6a1fe1224 100644 --- a/novawalletTests/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupTests.swift +++ b/novawalletTests/Modules/Staking/StakingUnbondSetup/StakingUnbondSetupTests.swift @@ -90,7 +90,7 @@ class StakingUnbondSetupTests: XCTestCase { extrinsicService: ExtrinsicServiceStub.dummy() ) - let stashItem = StashItem(stash: nominatorAddress, controller: nominatorAddress) + let stashItem = StashItem(stash: nominatorAddress, controller: nominatorAddress, chainId: chain.chainId) let stakingLedger = StakingLedger( stash: selectedAccount.accountId, total: BigUInt(1e+12), diff --git a/novawalletTests/Modules/WalletOperationDetails/ExtrinsicDetailsTests.swift b/novawalletTests/Modules/WalletOperationDetails/ExtrinsicDetailsTests.swift index fc2b85d479..7ea343c2a5 100644 --- a/novawalletTests/Modules/WalletOperationDetails/ExtrinsicDetailsTests.swift +++ b/novawalletTests/Modules/WalletOperationDetails/ExtrinsicDetailsTests.swift @@ -14,6 +14,7 @@ class OperationDetailsTests: XCTestCase { let chain = ChainModelGenerator.generateChain(generatingAssets: 1, addressPrefix: 42) let chainAsset = ChainAsset(chain: chain, asset: chain.utilityAssets().first!) let wallet = AccountGenerator.generateMetaAccount() + let selectedAccount = wallet.fetchMetaChainAccount(for: chain.accountRequest())! let txData = AssetTransactionGenerator.generateExtrinsic( for: wallet, chainAsset: chainAsset @@ -22,11 +23,8 @@ class OperationDetailsTests: XCTestCase { let userDataStorageFacade = UserDataStorageTestFacade() let substrateDataStorageFacade = SubstrateStorageTestFacade() - let walletRepository = AccountRepositoryFactory( + let accountRepositoryFactory = AccountRepositoryFactory( storageFacade: userDataStorageFacade - ).createMetaAccountRepository( - for: nil, - sortDescriptors: [] ) let operationQueue = OperationQueue() @@ -44,16 +42,24 @@ class OperationDetailsTests: XCTestCase { currencyId: Currency.usd.id ) ) + + let operationDetailsProviderFactory = OperationDetailsDataProviderFactory( + selectedAccount: selectedAccount, + chainAsset: chainAsset, + chainRegistry: MockChainRegistryProtocol(), + accountRepositoryFactory: accountRepositoryFactory, + operationQueue: operationQueue + ) + + let operationDataProvider = operationDetailsProviderFactory.createProvider(for: txData)! let interactor = OperationDetailsInteractor( transaction: txData, chainAsset: chainAsset, - wallet: wallet, - walletRepository: walletRepository, transactionLocalSubscriptionFactory: transactionLocalSubscriptionFactory, - operationQueue: operationQueue, currencyManager: CurrencyManagerStub(), - priceLocalSubscriptionFactory: priceLocalSubscriptionFactory + priceLocalSubscriptionFactory: priceLocalSubscriptionFactory, + operationDataProvider: operationDataProvider ) let balanceViewModelFactory = BalanceViewModelFactory(