diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 33f4af36a5..a424fd18cf 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -98,6 +98,14 @@ 0C17BD992A42F1BE004AF9E7 /* MoneyPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */; }; 0C17BD9B2A43025E004AF9E7 /* Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD9A2A43025E004AF9E7 /* Pagination.swift */; }; 0C17BD9D2A43137E004AF9E7 /* TransactionHistoryItem+Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C17BD9C2A43137E004AF9E7 /* TransactionHistoryItem+Subscription.swift */; }; + 0C1998E02C496E4B000EBFB8 /* BalancePallet+Holds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1998DF2C496E4B000EBFB8 /* BalancePallet+Holds.swift */; }; + 0C1998E22C49AD19000EBFB8 /* AssetHoldMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1998E12C49AD19000EBFB8 /* AssetHoldMapper.swift */; }; + 0C1998E42C49ADBB000EBFB8 /* AssetHold.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1998E32C49ADBB000EBFB8 /* AssetHold.swift */; }; + 0C1998E62C49B38C000EBFB8 /* HoldsSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1998E52C49B38C000EBFB8 /* HoldsSubscription.swift */; }; + 0C1998E82C49B5B9000EBFB8 /* BalancesPallet+StoragePath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1998E72C49B5B9000EBFB8 /* BalancesPallet+StoragePath.swift */; }; + 0C1998EA2C4A22FD000EBFB8 /* BalancesRemoteSubscriptionService+Protocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1998E92C4A22FD000EBFB8 /* BalancesRemoteSubscriptionService+Protocol.swift */; }; + 0C1998ED2C4CB24C000EBFB8 /* AssetHold+Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1998EC2C4CB24C000EBFB8 /* AssetHold+Display.swift */; }; + 0C1998EF2C4CC51D000EBFB8 /* SCLoadableControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1998EE2C4CC51D000EBFB8 /* SCLoadableControllerProtocol.swift */; }; 0C1BE19E2A46EDB40010933C /* String+ScientificInt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1BE19D2A46EDB40010933C /* String+ScientificInt.swift */; }; 0C1BE1A02A46F1F00010933C /* ScientificStringParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1BE19F2A46F1F00010933C /* ScientificStringParsing.swift */; }; 0C1BE1A22A46F93B0010933C /* BigUInt+Scientific.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1BE1A12A46F93B0010933C /* BigUInt+Scientific.swift */; }; @@ -322,6 +330,8 @@ 0C7104A22C2D0A6200487E64 /* LedgerTxConfirmationParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7104A12C2D0A6200487E64 /* LedgerTxConfirmationParams.swift */; }; 0C7104A42C2D0FC500487E64 /* BaseLedgerTxConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7104A32C2D0FC500487E64 /* BaseLedgerTxConfirmInteractor.swift */; }; 0C7104A82C2D11EB00487E64 /* GenericLedgerTxConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7104A72C2D11EB00487E64 /* GenericLedgerTxConfirmInteractor.swift */; }; + 0C75E2962C3F9263005A6232 /* DelegatedStakingPallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C75E2952C3F9263005A6232 /* DelegatedStakingPallet.swift */; }; + 0C75E2982C3F92EC005A6232 /* DelegatedStakingPallet+Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C75E2972C3F92EC005A6232 /* DelegatedStakingPallet+Path.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 */; }; @@ -444,6 +454,8 @@ 0CA5BDED2C41792A000A4CDD /* SubstrateAssetsUpdatingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA5BDEC2C41792A000A4CDD /* SubstrateAssetsUpdatingService.swift */; }; 0CA5BDF12C438A45000A4CDD /* TransactionSubscriptionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA5BDF02C438A45000A4CDD /* TransactionSubscriptionFactory.swift */; }; 0CA5BDF32C4476A3000A4CDD /* ConnectionCreationParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA5BDF22C4476A3000A4CDD /* ConnectionCreationParams.swift */; }; + 0CA5BDFB2C455790000A4CDD /* NominationPoolStakingMigrating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA5BDFA2C455790000A4CDD /* NominationPoolStakingMigrating.swift */; }; + 0CA5BDFD2C455A73000A4CDD /* NominationPoolsMigrateCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA5BDFC2C455A73000A4CDD /* NominationPoolsMigrateCall.swift */; }; 0CA719792B768ABC000B086E /* JsonStringify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA719782B768ABC000B086E /* JsonStringify.swift */; }; 0CA7197B2B773172000B086E /* HydraStableswapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7197A2B773172000B086E /* HydraStableswapTests.swift */; }; 0CA7197D2B7737D5000B086E /* HydraQuoteFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7197C2B7737D5000B086E /* HydraQuoteFactory.swift */; }; @@ -463,6 +475,11 @@ 0CA7821C2B03D0A9003F562A /* ExtrinsicProcessor+AssetHubSwapMatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA7821B2B03D0A9003F562A /* ExtrinsicProcessor+AssetHubSwapMatching.swift */; }; 0CA957222B6A507A009AD757 /* HydraSwapParamsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA957212B6A507A009AD757 /* HydraSwapParamsService.swift */; }; 0CA957252B6A566B009AD757 /* HydraDx+Call.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA957242B6A566B009AD757 /* HydraDx+Call.swift */; }; + 0CAB7D9F2C46B05F0070CE4D /* PoolStakingRecommendingValidationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAB7D9E2C46B05F0070CE4D /* PoolStakingRecommendingValidationFactory.swift */; }; + 0CAB7DA12C4714CD0070CE4D /* StakingActivityProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAB7DA02C4714CD0070CE4D /* StakingActivityProviding.swift */; }; + 0CAB7DA32C471AAE0070CE4D /* DirectStkRecommendingValidationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAB7DA22C471AAE0070CE4D /* DirectStkRecommendingValidationFactory.swift */; }; + 0CAB7DA52C471D260070CE4D /* StakingActivityForValidating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAB7DA42C471D260070CE4D /* StakingActivityForValidating.swift */; }; + 0CAB7DA82C47B8860070CE4D /* NominationPool+MigrateWrapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAB7DA72C47B8860070CE4D /* NominationPool+MigrateWrapping.swift */; }; 0CAB7DAE2C47E61F0070CE4D /* ConnectionTLSSupportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAB7DAD2C47E61F0070CE4D /* ConnectionTLSSupportProvider.swift */; }; 0CAC01552A52E0CC0069413E /* AssetListModelHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAC01542A52E0CC0069413E /* AssetListModelHelpers.swift */; }; 0CAC01572A52E1960069413E /* AssetListPresenterHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAC01562A52E1960069413E /* AssetListPresenterHelpers.swift */; }; @@ -5025,6 +5042,15 @@ 0C17BD982A42F1BE004AF9E7 /* MoneyPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoneyPresentable.swift; sourceTree = ""; }; 0C17BD9A2A43025E004AF9E7 /* Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pagination.swift; sourceTree = ""; }; 0C17BD9C2A43137E004AF9E7 /* TransactionHistoryItem+Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TransactionHistoryItem+Subscription.swift"; sourceTree = ""; }; + 0C1998DE2C496B15000EBFB8 /* SubstrateDataModel30.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel30.xcdatamodel; sourceTree = ""; }; + 0C1998DF2C496E4B000EBFB8 /* BalancePallet+Holds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BalancePallet+Holds.swift"; sourceTree = ""; }; + 0C1998E12C49AD19000EBFB8 /* AssetHoldMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHoldMapper.swift; sourceTree = ""; }; + 0C1998E32C49ADBB000EBFB8 /* AssetHold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetHold.swift; sourceTree = ""; }; + 0C1998E52C49B38C000EBFB8 /* HoldsSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoldsSubscription.swift; sourceTree = ""; }; + 0C1998E72C49B5B9000EBFB8 /* BalancesPallet+StoragePath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BalancesPallet+StoragePath.swift"; sourceTree = ""; }; + 0C1998E92C4A22FD000EBFB8 /* BalancesRemoteSubscriptionService+Protocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BalancesRemoteSubscriptionService+Protocol.swift"; sourceTree = ""; }; + 0C1998EC2C4CB24C000EBFB8 /* AssetHold+Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AssetHold+Display.swift"; sourceTree = ""; }; + 0C1998EE2C4CC51D000EBFB8 /* SCLoadableControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SCLoadableControllerProtocol.swift; sourceTree = ""; }; 0C1BE19D2A46EDB40010933C /* String+ScientificInt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+ScientificInt.swift"; sourceTree = ""; }; 0C1BE19F2A46F1F00010933C /* ScientificStringParsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScientificStringParsing.swift; sourceTree = ""; }; 0C1BE1A12A46F93B0010933C /* BigUInt+Scientific.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BigUInt+Scientific.swift"; sourceTree = ""; }; @@ -5258,6 +5284,8 @@ 0C7104A12C2D0A6200487E64 /* LedgerTxConfirmationParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LedgerTxConfirmationParams.swift; sourceTree = ""; }; 0C7104A32C2D0FC500487E64 /* BaseLedgerTxConfirmInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseLedgerTxConfirmInteractor.swift; sourceTree = ""; }; 0C7104A72C2D11EB00487E64 /* GenericLedgerTxConfirmInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericLedgerTxConfirmInteractor.swift; sourceTree = ""; }; + 0C75E2952C3F9263005A6232 /* DelegatedStakingPallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatedStakingPallet.swift; sourceTree = ""; }; + 0C75E2972C3F92EC005A6232 /* DelegatedStakingPallet+Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DelegatedStakingPallet+Path.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 = ""; }; @@ -5382,6 +5410,8 @@ 0CA5BDEC2C41792A000A4CDD /* SubstrateAssetsUpdatingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubstrateAssetsUpdatingService.swift; sourceTree = ""; }; 0CA5BDF02C438A45000A4CDD /* TransactionSubscriptionFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionSubscriptionFactory.swift; sourceTree = ""; }; 0CA5BDF22C4476A3000A4CDD /* ConnectionCreationParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionCreationParams.swift; sourceTree = ""; }; + 0CA5BDFA2C455790000A4CDD /* NominationPoolStakingMigrating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolStakingMigrating.swift; sourceTree = ""; }; + 0CA5BDFC2C455A73000A4CDD /* NominationPoolsMigrateCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NominationPoolsMigrateCall.swift; sourceTree = ""; }; 0CA719782B768ABC000B086E /* JsonStringify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonStringify.swift; sourceTree = ""; }; 0CA7197A2B773172000B086E /* HydraStableswapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraStableswapTests.swift; sourceTree = ""; }; 0CA7197C2B7737D5000B086E /* HydraQuoteFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraQuoteFactory.swift; sourceTree = ""; }; @@ -5401,6 +5431,11 @@ 0CA7821B2B03D0A9003F562A /* ExtrinsicProcessor+AssetHubSwapMatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ExtrinsicProcessor+AssetHubSwapMatching.swift"; sourceTree = ""; }; 0CA957212B6A507A009AD757 /* HydraSwapParamsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HydraSwapParamsService.swift; sourceTree = ""; }; 0CA957242B6A566B009AD757 /* HydraDx+Call.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HydraDx+Call.swift"; sourceTree = ""; }; + 0CAB7D9E2C46B05F0070CE4D /* PoolStakingRecommendingValidationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoolStakingRecommendingValidationFactory.swift; sourceTree = ""; }; + 0CAB7DA02C4714CD0070CE4D /* StakingActivityProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingActivityProviding.swift; sourceTree = ""; }; + 0CAB7DA22C471AAE0070CE4D /* DirectStkRecommendingValidationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectStkRecommendingValidationFactory.swift; sourceTree = ""; }; + 0CAB7DA42C471D260070CE4D /* StakingActivityForValidating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StakingActivityForValidating.swift; sourceTree = ""; }; + 0CAB7DA72C47B8860070CE4D /* NominationPool+MigrateWrapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NominationPool+MigrateWrapping.swift"; sourceTree = ""; }; 0CAB7DAD2C47E61F0070CE4D /* ConnectionTLSSupportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionTLSSupportProvider.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 = ""; }; @@ -10125,6 +10160,8 @@ isa = PBXGroup; children = ( 0C0E0A9F2B3F013800865F10 /* BalancesPallet.swift */, + 0C1998DF2C496E4B000EBFB8 /* BalancePallet+Holds.swift */, + 0C1998E72C49B5B9000EBFB8 /* BalancesPallet+StoragePath.swift */, ); path = BalancesPallet; sourceTree = ""; @@ -10153,6 +10190,8 @@ 0C13D3012A7D53C10054BB6F /* HybridStakingRecommendationMediator.swift */, 0C13D3032A7D56CC0054BB6F /* StakingRecommendationMediatorFactory.swift */, 0C13D3122A80D06B0054BB6F /* StakingRecommendationValidationFactory.swift */, + 0CAB7D9E2C46B05F0070CE4D /* PoolStakingRecommendingValidationFactory.swift */, + 0CAB7DA22C471AAE0070CE4D /* DirectStkRecommendingValidationFactory.swift */, ); path = Recommendation; sourceTree = ""; @@ -10165,6 +10204,7 @@ 0CB261F22A9E182300287305 /* NominationPoolClaimRewards.swift */, 0CB261F42A9E188300287305 /* NominationPoolsBondExtraCall.swift */, 0C7E7FAA2A9F27FB00596628 /* NominationPoolsRedeemCall.swift */, + 0CA5BDFC2C455A73000A4CDD /* NominationPoolsMigrateCall.swift */, ); path = NominationPools; sourceTree = ""; @@ -10188,6 +10228,14 @@ path = ExtrinsicProxy; sourceTree = ""; }; + 0C1998EB2C4CB218000EBFB8 /* Model */ = { + isa = PBXGroup; + children = ( + 0C1998EC2C4CB24C000EBFB8 /* AssetHold+Display.swift */, + ); + path = Model; + sourceTree = ""; + }; 0C1CCC3D2B5F862D00A6EA17 /* HydraDx */ = { isa = PBXGroup; children = ( @@ -10657,6 +10705,15 @@ path = Generic; sourceTree = ""; }; + 0C75E2942C3F923A005A6232 /* DelegatedStaking */ = { + isa = PBXGroup; + children = ( + 0C75E2952C3F9263005A6232 /* DelegatedStakingPallet.swift */, + 0C75E2972C3F92EC005A6232 /* DelegatedStakingPallet+Path.swift */, + ); + path = DelegatedStaking; + sourceTree = ""; + }; 0C77B55D2A83713D00B5AE08 /* StaticValidatorList */ = { isa = PBXGroup; children = ( @@ -10860,6 +10917,14 @@ path = GetTokenOptions; sourceTree = ""; }; + 0CA5BDF92C455727000A4CDD /* Shared */ = { + isa = PBXGroup; + children = ( + 0CAB7DA72C47B8860070CE4D /* NominationPool+MigrateWrapping.swift */, + ); + path = Shared; + sourceTree = ""; + }; 0CA719802B787D9E000B086E /* Omnipool */ = { isa = PBXGroup; children = ( @@ -10916,6 +10981,7 @@ 0CB261D52A97A4D300287305 /* NominationPools */ = { isa = PBXGroup; children = ( + 0CA5BDF92C455727000A4CDD /* Shared */, 139168F2E6530E3946753501 /* Redeem */, F4CFBAC4468FAD5728719A7D /* ClaimRewards */, 0CB261D62A9893BD00287305 /* Unstake */, @@ -13593,6 +13659,7 @@ 83C426015DF863EBA46F1E3E /* Locks */ = { isa = PBXGroup; children = ( + 0C1998EB2C4CB218000EBFB8 /* Model */, 88C7165B28C8D37E0015D1E9 /* View */, F4F9944B0577EFF25A0643FE /* LocksProtocols.swift */, E30E541992BF608923DABE5F /* LocksWireframe.swift */, @@ -14600,6 +14667,7 @@ 8438E1DC24C18F11001BDB13 /* Types */ = { isa = PBXGroup; children = ( + 0C75E2942C3F923A005A6232 /* DelegatedStaking */, 0C38B5012B7A86BA00882A8B /* TokensPallet */, 0C38B4FE2B7A82E000882A8B /* TransactionPaymentPallet */, 0CCCDF7C2B64BE3D00473D42 /* HydraDx */, @@ -15191,6 +15259,7 @@ 77DAFF6C2B297DB500D4220C /* ProxyAccountMapper.swift */, 849A4EF9279ABC8800AB6709 /* AssetBalanceMapper.swift */, 88C017E528C60A65003B2D28 /* AssetLockMapper.swift */, + 0C1998E12C49AD19000EBFB8 /* AssetHoldMapper.swift */, 880855F728D09DA8004255E7 /* CrowdloanContributionDataMapper.swift */, 8499FECB27BF8F4A00712589 /* NftModelMapper.swift */, 84F3B27727F4179A00D64CF5 /* PhishingSiteMapper.swift */, @@ -15927,6 +15996,7 @@ 84757E13299A1E1500616C6C /* BatchSubscriptionHandler.swift */, 8455F1A12A1F5A6C003F072D /* BatchStorageSubscriptionResult.swift */, 0C6941C52B1F51FE00A7CF6D /* RawDataStorageSubscription.swift */, + 0C1998E52C49B38C000EBFB8 /* HoldsSubscription.swift */, ); path = StorageSubscription; sourceTree = ""; @@ -16773,6 +16843,7 @@ 887AFC8628BC95F0002A0422 /* MetaAccountChainResponse.swift */, 8831F0FF28C65B95009F7682 /* AssetLock.swift */, 2AC7BC832731A214001D99B0 /* AssetLocks+Sort.swift */, + 0C1998E32C49ADBB000EBFB8 /* AssetHold.swift */, 849D3222291CC43D00D25839 /* MarkupAttributedText.swift */, 84BAD21F293B574900C55C49 /* MultichainToken.swift */, 77E255682A16148000B644C3 /* StakingRewardsFilter.swift */, @@ -16888,6 +16959,7 @@ 0C259EA72B46C55C00CB86E4 /* ExtrinsicSigningErrorHandling.swift */, 77C35CE42B568ED100308F16 /* YourWalletsPresentable.swift */, 77C35CE62B56D07300308F16 /* ScanAddressPresentable.swift */, + 0C1998EE2C4CC51D000EBFB8 /* SCLoadableControllerProtocol.swift */, ); path = Protocols; sourceTree = ""; @@ -18824,6 +18896,7 @@ 0CA5BDE82C416099000A4CDD /* BalanceRemoteSubscriptionHandlingFactory.swift */, 0CA5BDEA2C4171D9000A4CDD /* BalanceRemoteSubscriptionHandlingProxy.swift */, 0CA5BDEC2C41792A000A4CDD /* SubstrateAssetsUpdatingService.swift */, + 0C1998E92C4A22FD000EBFB8 /* BalancesRemoteSubscriptionService+Protocol.swift */, ); path = Substrate; sourceTree = ""; @@ -19047,6 +19120,8 @@ 84350AD3284580F50031EF24 /* StakingTotalStakePresentable.swift */, 84350AD52845836C0031EF24 /* IdentityPresentable.swift */, 77171CA72A98BBA10032B387 /* NominationPoolErrorPresentable.swift */, + 0CA5BDFA2C455790000A4CDD /* NominationPoolStakingMigrating.swift */, + 0CAB7DA02C4714CD0070CE4D /* StakingActivityProviding.swift */, ); path = Protocols; sourceTree = ""; @@ -19553,6 +19628,7 @@ 0C13D3102A80CA9A0054BB6F /* StakingDataValidatorFactory+Plank.swift */, 0C12A2482AA35AE300C7FA49 /* RelaychainStakingValidatorFacade.swift */, 772AD8E52AC5A87200B0C41A /* MinStakeCrossedParams.swift */, + 0CAB7DA42C471D260070CE4D /* StakingActivityForValidating.swift */, ); path = Validation; sourceTree = ""; @@ -23959,6 +24035,7 @@ 0CE629D92AA9B68C00E250BD /* AssetBalanceViewModel.swift in Sources */, 84452F9325D5EE7300F47EC5 /* DataOperationFactory.swift in Sources */, 841E5538282CF3F400C8438F /* StakingMainViewModelFactory.swift in Sources */, + 0CA5BDFD2C455A73000A4CDD /* NominationPoolsMigrateCall.swift in Sources */, 8442003C28EAA2E400C49C4A /* ReferendumsWireframe.swift in Sources */, 844DBC6F274E702C009F8351 /* AccountImportBaseView.swift in Sources */, 8466781A27ECA021007935D3 /* PersistentExtrinsicService.swift in Sources */, @@ -24256,6 +24333,7 @@ 84644A30256722D2004EAA4B /* TriangularedBlurButton+Inspectable.swift in Sources */, 84CA407E2A037658004BB71E /* WalletConnectSessionsViewModelFactory.swift in Sources */, 844DBC60274D1B3E009F8351 /* IconWithTitleSubtitleViewModel.swift in Sources */, + 0CA5BDFB2C455790000A4CDD /* NominationPoolStakingMigrating.swift in Sources */, 77A0B2F32A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift in Sources */, 849014B924AA87E3008F705E /* PinSetupViewController.swift in Sources */, 84F98D8A25E3DD3F0040418E /* StorageCodingPath.swift in Sources */, @@ -24485,6 +24563,7 @@ 84EDF66929C4B94C002173E6 /* EvmNativeBalanceUpdatingService.swift in Sources */, 84F4A9A42551A8F3000CF0A3 /* AccountExportPasswordError.swift in Sources */, 84B73ADE279D90BD0071AE16 /* AssetsTransfer.swift in Sources */, + 0CAB7D9F2C46B05F0070CE4D /* PoolStakingRecommendingValidationFactory.swift in Sources */, 84CFF1E726526FBC00DB7CF7 /* StakingBondMoreViewLayout.swift in Sources */, 84CA68DB26BEA33F003B9453 /* ChainRegistryFactory.swift in Sources */, AE39839A272BFC2300BC8A85 /* ImportChainAccount.swift in Sources */, @@ -24542,6 +24621,7 @@ 8804AD89295B75F8001C4E09 /* Styles.swift in Sources */, 844CB57826FA702700396E13 /* CrowdloansViewInfo.swift in Sources */, AEACD5F9265E94AB00A09892 /* StatusViewModel.swift in Sources */, + 0C1998EF2C4CC51D000EBFB8 /* SCLoadableControllerProtocol.swift in Sources */, 847A25BF28D7C2A2006AC9F5 /* AccountIdCodingWrapper.swift in Sources */, 84CCBFBC2509709500180F4F /* UIBarButtonItem+Style.swift in Sources */, F4223F102732D445003D8E4E /* AcalaStatementData.swift in Sources */, @@ -24726,6 +24806,7 @@ 84C5ADD32811F6D0006D7388 /* WalletAccountView.swift in Sources */, 8490142F24A935FE008F705E /* ControllerBackedProtocol.swift in Sources */, F41CEBEA273161DE00C06154 /* CrowdloanRewardDestinationView.swift in Sources */, + 0C1998EA2C4A22FD000EBFB8 /* BalancesRemoteSubscriptionService+Protocol.swift in Sources */, 848B2FFE286EDA4700465BA2 /* WalletServiceFacade.swift in Sources */, 849D14CA2994D9BC0048E947 /* StackIconTitleValueCell.swift in Sources */, 84DF21A125347031005454AE /* DetailsDisplayTableViewCell.swift in Sources */, @@ -24850,6 +24931,7 @@ 8488D5DB298167D10019B388 /* GovernanceDelegateTypeView.swift in Sources */, 84CC726228AF8C7A003429E7 /* LedgerApplicationRequest.swift in Sources */, 849AFEBD26DCCE3A00B65924 /* SubqueryResponse.swift in Sources */, + 0C1998E62C49B38C000EBFB8 /* HoldsSubscription.swift in Sources */, 84CFF1E426526FBC00DB7CF7 /* StakingBondMorePresenter.swift in Sources */, 842D1E7924D063FD00C30A7A /* TriangularedView.swift in Sources */, AE2C84E925EF98D500986716 /* ValidatorInfoViewFactory.swift in Sources */, @@ -25191,6 +25273,7 @@ 840B3D70289A575A00DA1DA9 /* ParitySignerScanProtocols.swift in Sources */, 849067C8299BCB0100B2983E /* GovernanceRevokeDelegationConfirmPresenter+Protocol.swift in Sources */, 8452585327ABCA07004F9082 /* HideZeroBalancesChanged.swift in Sources */, + 0CAB7DA12C4714CD0070CE4D /* StakingActivityProviding.swift in Sources */, 843E9B2D27C8AC57009C143A /* NftMediaViewModel.swift in Sources */, 84A5915C292B390800BCCF8F /* EvmTransactionBuilder.swift in Sources */, 7725EB442B2AC9BA007E1A8A /* ChainProxyChangesCalculator.swift in Sources */, @@ -25924,6 +26007,7 @@ 845B821726EF7FED00D25C72 /* SelectedWalletSettings.swift in Sources */, 0CE840E22BFB81F9003C2B7A /* RuntimeFetchOperationFactory.swift in Sources */, 0C38B5092B7CB96800882A8B /* MaxCounter.swift in Sources */, + 0C75E2962C3F9263005A6232 /* DelegatedStakingPallet.swift in Sources */, 84DC3CE12795DEBF0038E2ED /* SubqueryHistoryOperationFactory.swift in Sources */, 848FFE9A25E6E0E400652AA5 /* Staking+Validator.swift in Sources */, 8498430926592E5D006BBB9F /* CrowdloansViewModel.swift in Sources */, @@ -25965,6 +26049,7 @@ F4D551A12643DD240002363F /* AccountSelectionPresentable.swift in Sources */, 0C37AFBF2B56852500009ECA /* PayoutStakersCall.swift in Sources */, 84333BD92856840E00C76A4F /* SelectionValidatorGroups.swift in Sources */, + 0CAB7DA32C471AAE0070CE4D /* DirectStkRecommendingValidationFactory.swift in Sources */, 84002A9E2992444300A80672 /* GovernanceNewDelegation.swift in Sources */, 845B822126EF8F1A00D25C72 /* ManagedMetaAccountMapper.swift in Sources */, 0C9C64302A8D6779004DC078 /* StakingNPoolsPresenter.swift in Sources */, @@ -26253,6 +26338,7 @@ 770ABB8E2B85593700132465 /* Web3TopicSettingsMapper.swift in Sources */, 6F0CFDAB9D0C35075BD74A77 /* WalletHistoryFilterProtocols.swift in Sources */, 849C7BD92A1B21CF00434621 /* GladingRectModel.swift in Sources */, + 0C1998E42C49ADBB000EBFB8 /* AssetHold.swift in Sources */, 60461B20A5DA9E9E3AF0BB84 /* WalletHistoryFilterWireframe.swift in Sources */, 77EAABA52B2726A500CA7305 /* SectionTextHeaderView.swift in Sources */, 8487583327F06AF300495306 /* QRScannerViewLayout.swift in Sources */, @@ -26713,6 +26799,7 @@ DE03CA5AD7F1D0B80DFF13B6 /* DAppBrowserViewController.swift in Sources */, 8473B47A2A2083B1003DE213 /* StakingDashboardItemMapper.swift in Sources */, 0C3205D62A895F0F002EB914 /* EvmGasLimitWithFallbackProvider.swift in Sources */, + 0C1998ED2C4CB24C000EBFB8 /* AssetHold+Display.swift in Sources */, 8442002328E6FE1E00C49C4A /* ReferendumsViewManager.swift in Sources */, 70C0E48EE41B4C7229F5946C /* DAppBrowserViewLayout.swift in Sources */, 8849C5EA29806F1E00DE35CC /* InAppUpdatesStyles.swift in Sources */, @@ -26820,12 +26907,14 @@ 5E6D69D84220119BA5362358 /* OperationDetailsProtocols.swift in Sources */, 81544BD01F6AD0197588D3C5 /* OperationDetailsWireframe.swift in Sources */, 7796C6FF2A177D7300D56094 /* GenericLens.swift in Sources */, + 0C1998E02C496E4B000EBFB8 /* BalancePallet+Holds.swift in Sources */, 7FF2D6FEDD352AC51E1DBB3B /* OperationDetailsPresenter.swift in Sources */, B6DB30A8D1BF84158CAC635D /* OperationDetailsInteractor.swift in Sources */, C9931414951375760E5D1C57 /* OperationDetailsViewController.swift in Sources */, 84FBED0129277CD700FBEB83 /* EvmAssetBalanceUpdatingService.swift in Sources */, 77740BC22AD69E3400E8C06F /* SwapMaxButtonView.swift in Sources */, 84ADA61029B9E2E800EB687E /* MultiExtrinsicRetryable.swift in Sources */, + 0CAB7DA82C47B8860070CE4D /* NominationPool+MigrateWrapping.swift in Sources */, 67684F7576ED0252C1050CA5 /* OperationDetailsViewLayout.swift in Sources */, 77F189442A49974A00E8B933 /* UITextView+bind.swift in Sources */, D840B64C33EF47E723905378 /* OperationDetailsViewFactory.swift in Sources */, @@ -26910,6 +26999,7 @@ 77AAE2282AFC1167006872CC /* ChainModel+historyId.swift in Sources */, D9ECCCCF1449EFAFD0FA886E /* ParaStkStakeSetupViewLayout.swift in Sources */, 84981EEC29D4385F00948306 /* TransactionHistoryHybridFetcher.swift in Sources */, + 0C75E2982C3F92EC005A6232 /* DelegatedStakingPallet+Path.swift in Sources */, 2D6AED5D2C05E908001A0A15 /* CheckboxListPresenterTrait.swift in Sources */, A714CEAF7A86292E8D679056 /* ParaStkStakeSetupViewFactory.swift in Sources */, 84350AD62845836C0031EF24 /* IdentityPresentable.swift in Sources */, @@ -27208,6 +27298,7 @@ E5DC2660D78D3CC9FC48E748 /* LedgerAccountConfirmationViewController.swift in Sources */, 845B823229C8FEC700D187CB /* EtherscanNativeHistoryInfo.swift in Sources */, CDAB179209D12B81430E377C /* LedgerAccountConfirmationViewLayout.swift in Sources */, + 0C1998E82C49B5B9000EBFB8 /* BalancesPallet+StoragePath.swift in Sources */, FA894DFA8EEBB0B4562CD788 /* LedgerAccountConfirmationViewFactory.swift in Sources */, 99A045F3C6403FB48B39971D /* LedgerWalletConfirmProtocols.swift in Sources */, 0CF692892C20E2B1000FC395 /* BackupManualWarningPresentable.swift in Sources */, @@ -27329,6 +27420,7 @@ 2CEFF4C2574F0AABE0E9BF89 /* ReferendumVoteSetupViewController.swift in Sources */, 0C59E8CF2AA5D744001E11F3 /* PooledBalanceUpdatingState.swift in Sources */, 0C85FF342B6D523B00FC0014 /* HydraOmnipoolQuoteFactory.swift in Sources */, + 0CAB7DA52C471D260070CE4D /* StakingActivityForValidating.swift in Sources */, D1C4208A89633395AF2FDB74 /* ReferendumVoteSetupViewLayout.swift in Sources */, 84CE22D929A38AA600A03156 /* GovernanceDelegateInfoPresenter+Update.swift in Sources */, 811096BAAA6BD237DF2769EA /* ReferendumVoteSetupViewFactory.swift in Sources */, @@ -27540,6 +27632,7 @@ 0CCD18CB2BA1F0F00068D73A /* BalanceViewModelFactoryProtocol.swift in Sources */, 061C16B970BE568D7A80F53A /* GovernanceDelegateSetupPresenter.swift in Sources */, 7D690E88EAB7437B9F69C92F /* GovernanceDelegateSetupInteractor.swift in Sources */, + 0C1998E22C49AD19000EBFB8 /* AssetHoldMapper.swift in Sources */, 847C15BF2A0CFE0D003F3FF8 /* WalletConnectErrorPresentable.swift in Sources */, B8DC27C3CAE9846A5E56B4EE /* GovernanceDelegateSetupViewController.swift in Sources */, 4C4142B4CB2DBCA1F06DC046 /* GovernanceDelegateSetupViewLayout.swift in Sources */, @@ -29102,6 +29195,7 @@ 843910CA253F7E6500E3C217 /* SubstrateDataModel.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 0C1998DE2C496B15000EBFB8 /* SubstrateDataModel30.xcdatamodel */, 2DAF539C2C198E690076B4B6 /* SubstrateDataModel29.xcdatamodel */, 2D5A61C52C09D34E006D58E3 /* SubstrateDataModel28.xcdatamodel */, 7761DD252BA10CF2000C600C /* SubstrateDataModel27.xcdatamodel */, @@ -29132,7 +29226,7 @@ 88787F0328DB3A7B00B115AB /* SubstrateDataModel2.xcdatamodel */, 843910CB253F7E6500E3C217 /* SubstrateDataModel.xcdatamodel */, ); - currentVersion = 2DAF539C2C198E690076B4B6 /* SubstrateDataModel29.xcdatamodel */; + currentVersion = 0C1998DE2C496B15000EBFB8 /* SubstrateDataModel30.xcdatamodel */; path = SubstrateDataModel.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/novawallet/Common/Configs/ApplicationConfigs.swift b/novawallet/Common/Configs/ApplicationConfigs.swift index 6e7be19eb7..ca06406385 100644 --- a/novawallet/Common/Configs/ApplicationConfigs.swift +++ b/novawallet/Common/Configs/ApplicationConfigs.swift @@ -131,6 +131,10 @@ extension ApplicationConfig: ApplicationConfigProtocol { URL(string: "https://polkadot.js.org/phishing/all.json")! } + var phishingDAppsTopLevelSet: Set { + ["top"] + } + var chainListURL: URL { #if F_RELEASE URL(string: "https://raw.githubusercontent.com/novasamatech/nova-utils/master/chains/v20/chains.json")! diff --git a/novawallet/Common/DataProvider/NPoolsLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/NPoolsLocalSubscriptionFactory.swift index ccaea08af8..1b469fb92b 100644 --- a/novawallet/Common/DataProvider/NPoolsLocalSubscriptionFactory.swift +++ b/novawallet/Common/DataProvider/NPoolsLocalSubscriptionFactory.swift @@ -16,6 +16,11 @@ protocol NPoolsLocalSubscriptionFactoryProtocol { chainId: ChainModel.Id ) throws -> AnyDataProvider + func getDelegatedStakingDelegatorProvider( + for accountId: AccountId, + chainId: ChainModel.Id + ) throws -> AnyDataProvider + func getBondedPoolProvider( for poolId: NominationPools.PoolId, chainId: ChainModel.Id @@ -91,6 +96,17 @@ extension NPoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol ) } + func getDelegatedStakingDelegatorProvider( + for accountId: AccountId, + chainId: ChainModel.Id + ) throws -> AnyDataProvider { + try getNoFallbackAccountProvider( + for: DelegatedStakingPallet.delegatorsPath, + accountId: accountId, + chainId: chainId + ) + } + func getBondedPoolProvider( for poolId: NominationPools.PoolId, chainId: ChainModel.Id diff --git a/novawallet/Common/DataProvider/Subscription/DecodedProviderTypes.swift b/novawallet/Common/DataProvider/Subscription/DecodedProviderTypes.swift index 325d4a0df1..424378ad7d 100644 --- a/novawallet/Common/DataProvider/Subscription/DecodedProviderTypes.swift +++ b/novawallet/Common/DataProvider/Subscription/DecodedProviderTypes.swift @@ -16,6 +16,7 @@ typealias DecodedAccountInfo = ChainStorageDecodedItem typealias DecodedCrowdloanFunds = ChainStorageDecodedItem typealias DecodedBagListNode = ChainStorageDecodedItem typealias DecodedPoolMember = ChainStorageDecodedItem +typealias DecodedDelegatedStakingDelegator = ChainStorageDecodedItem typealias DecodedBondedPool = ChainStorageDecodedItem typealias DecodedRewardPool = ChainStorageDecodedItem typealias DecodedSubPools = ChainStorageDecodedItem diff --git a/novawallet/Common/DataProvider/Subscription/NPoolsLocalStorageHandler.swift b/novawallet/Common/DataProvider/Subscription/NPoolsLocalStorageHandler.swift index 3e3ad0a972..5deb418de4 100644 --- a/novawallet/Common/DataProvider/Subscription/NPoolsLocalStorageHandler.swift +++ b/novawallet/Common/DataProvider/Subscription/NPoolsLocalStorageHandler.swift @@ -14,6 +14,12 @@ protocol NPoolsLocalSubscriptionHandler { chainId: ChainModel.Id ) + func handleDelegatedStaking( + result: Result, + accountId: AccountId, + chainId: ChainModel.Id + ) + func handlePoolMetadata( result: Result, poolId: NominationPools.PoolId, @@ -71,6 +77,12 @@ extension NPoolsLocalSubscriptionHandler { chainId _: ChainModel.Id ) {} + func handleDelegatedStaking( + result _: Result, + accountId _: AccountId, + chainId _: ChainModel.Id + ) {} + func handlePoolMetadata( result _: Result, poolId _: NominationPools.PoolId, diff --git a/novawallet/Common/DataProvider/Subscription/NPoolsLocalStorageSubscriber.swift b/novawallet/Common/DataProvider/Subscription/NPoolsLocalStorageSubscriber.swift index b6d68dc758..82cdf6f0cc 100644 --- a/novawallet/Common/DataProvider/Subscription/NPoolsLocalStorageSubscriber.swift +++ b/novawallet/Common/DataProvider/Subscription/NPoolsLocalStorageSubscriber.swift @@ -14,6 +14,11 @@ protocol NPoolsLocalStorageSubscriber: LocalStorageProviderObserving where Self: callbackQueue: DispatchQueue ) -> AnyDataProvider? + func subscribeDelegatedStaking( + for accountId: AccountId, + chainId: ChainModel.Id + ) -> AnyDataProvider? + func subscribeBondedPool( for poolId: NominationPools.PoolId, chainId: ChainModel.Id @@ -111,6 +116,39 @@ extension NPoolsLocalStorageSubscriber where Self: NPoolsLocalSubscriptionHandle return provider } + func subscribeDelegatedStaking( + for accountId: AccountId, + chainId: ChainModel.Id + ) -> AnyDataProvider? { + guard + let provider = try? npoolsLocalSubscriptionFactory.getDelegatedStakingDelegatorProvider( + for: accountId, + chainId: chainId + ) else { + return nil + } + + addDataProviderObserver( + for: provider, + updateClosure: { [weak self] value in + self?.npoolsLocalSubscriptionHandler.handleDelegatedStaking( + result: .success(value), + accountId: accountId, + chainId: chainId + ) + }, + failureClosure: { [weak self] error in + self?.npoolsLocalSubscriptionHandler.handleDelegatedStaking( + result: .failure(error), + accountId: accountId, + chainId: chainId + ) + } + ) + + return provider + } + func subscribeBondedPool( for poolId: NominationPools.PoolId, chainId: ChainModel.Id diff --git a/novawallet/Common/DataProvider/Subscription/WalletLocalStorageSubscriber.swift b/novawallet/Common/DataProvider/Subscription/WalletLocalStorageSubscriber.swift index f45c254093..c420780720 100644 --- a/novawallet/Common/DataProvider/Subscription/WalletLocalStorageSubscriber.swift +++ b/novawallet/Common/DataProvider/Subscription/WalletLocalStorageSubscriber.swift @@ -1,7 +1,7 @@ import Foundation import Operation_iOS -protocol WalletLocalStorageSubscriber where Self: AnyObject { +protocol WalletLocalStorageSubscriber: LocalStorageProviderObserving where Self: AnyObject { var walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol { get } var walletLocalSubscriptionHandler: WalletLocalSubscriptionHandler { get } @@ -27,6 +27,16 @@ protocol WalletLocalStorageSubscriber where Self: AnyObject { chainId: ChainModel.Id, assetId: AssetModel.Id ) -> StreamableProvider? + + func subscribeToAllHoldsProvider( + for accountId: AccountId + ) -> StreamableProvider? + + func subscribeToHoldsProvider( + for accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) -> StreamableProvider? } extension WalletLocalStorageSubscriber { @@ -254,6 +264,69 @@ extension WalletLocalStorageSubscriber { return locksProvider } + + func subscribeToAllHoldsProvider( + for accountId: AccountId + ) -> StreamableProvider? { + guard let holdsProvider = try? walletLocalSubscriptionFactory.getHoldsProvider(for: accountId) else { + return nil + } + + addStreamableProviderObserver( + for: holdsProvider, + updateClosure: { [weak self] changes in + self?.walletLocalSubscriptionHandler.handleAccountHolds( + result: .success(changes), + accountId: accountId + ) + }, + failureClosure: { [weak self] error in + self?.walletLocalSubscriptionHandler.handleAccountHolds( + result: .failure(error), + accountId: accountId + ) + } + ) + + return holdsProvider + } + + func subscribeToHoldsProvider( + for accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) -> StreamableProvider? { + guard + let holdsProvider = try? walletLocalSubscriptionFactory.getHoldsProvider( + for: accountId, + chainId: chainId, + assetId: assetId + ) else { + return nil + } + + addStreamableProviderObserver( + for: holdsProvider, + updateClosure: { [weak self] changes in + self?.walletLocalSubscriptionHandler.handleAccountHolds( + result: .success(changes), + accountId: accountId, + chainId: chainId, + assetId: assetId + ) + }, + failureClosure: { [weak self] error in + self?.walletLocalSubscriptionHandler.handleAccountHolds( + result: .failure(error), + accountId: accountId, + chainId: chainId, + assetId: assetId + ) + } + ) + + return holdsProvider + } } extension WalletLocalStorageSubscriber where Self: WalletLocalSubscriptionHandler { diff --git a/novawallet/Common/DataProvider/Subscription/WalletLocalSubscriptionHandler.swift b/novawallet/Common/DataProvider/Subscription/WalletLocalSubscriptionHandler.swift index 96f5788cb9..20af02f9f8 100644 --- a/novawallet/Common/DataProvider/Subscription/WalletLocalSubscriptionHandler.swift +++ b/novawallet/Common/DataProvider/Subscription/WalletLocalSubscriptionHandler.swift @@ -27,6 +27,18 @@ protocol WalletLocalSubscriptionHandler { chainId: ChainModel.Id, assetId: AssetModel.Id ) + + func handleAccountHolds( + result: Result<[DataProviderChange], Error>, + accountId: AccountId + ) + + func handleAccountHolds( + result: Result<[DataProviderChange], Error>, + accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) } extension WalletLocalSubscriptionHandler { @@ -55,4 +67,16 @@ extension WalletLocalSubscriptionHandler { chainId _: ChainModel.Id, assetId _: AssetModel.Id ) {} + + func handleAccountHolds( + result _: Result<[DataProviderChange], Error>, + accountId _: AccountId + ) {} + + func handleAccountHolds( + result _: Result<[DataProviderChange], Error>, + accountId _: AccountId, + chainId _: ChainModel.Id, + assetId _: AssetModel.Id + ) {} } diff --git a/novawallet/Common/DataProvider/WalletLocalSubscriptionFactory.swift b/novawallet/Common/DataProvider/WalletLocalSubscriptionFactory.swift index 55275d7789..7145c1842e 100644 --- a/novawallet/Common/DataProvider/WalletLocalSubscriptionFactory.swift +++ b/novawallet/Common/DataProvider/WalletLocalSubscriptionFactory.swift @@ -19,6 +19,14 @@ protocol WalletLocalSubscriptionFactoryProtocol { chainId: ChainModel.Id, assetId: AssetModel.Id ) throws -> StreamableProvider + + func getHoldsProvider(for accountId: AccountId) throws -> StreamableProvider + + func getHoldsProvider( + for accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) throws -> StreamableProvider } final class WalletLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, @@ -256,4 +264,89 @@ final class WalletLocalSubscriptionFactory: SubstrateLocalSubscriptionFactory, serialQueue: processingQueue ) } + + func getHoldsProvider( + for accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) throws -> StreamableProvider { + let cacheKey = "holds-\(accountId.toHex())-\(chainId)-\(assetId)" + + if let provider = getProvider(for: cacheKey) as? StreamableProvider { + return provider + } + + let filter = NSPredicate.assetHold( + for: accountId, + chainAssetId: ChainAssetId(chainId: chainId, assetId: assetId) + ) + + let provider = createAssetHoldsProvider(for: filter) { entity in + accountId.toHex() == entity.chainAccountId && + chainId == entity.chainId && + assetId == entity.assetId + } + + saveProvider(provider, for: cacheKey) + + return provider + } + + func getHoldsProvider(for accountId: AccountId) throws -> StreamableProvider { + let cacheKey = "holds-\(accountId.toHex())" + + if let provider = getProvider(for: cacheKey) as? StreamableProvider { + return provider + } + + let filter = NSPredicate.assetHold(for: accountId) + + let provider = createAssetHoldsProvider(for: filter) { entity in + accountId.toHex() == entity.chainAccountId + } + + saveProvider(provider, for: cacheKey) + + return provider + } + + private func createAssetHoldsProvider( + for repositoryFilter: NSPredicate, + observingFilter: @escaping (CDAssetHold) -> Bool + ) -> StreamableProvider { + let source = EmptyStreamableSource() + + let mapper = AssetHoldMapper() + + let repository = storageFacade.createRepository( + filter: repositoryFilter, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + + let processingQueue = createStreamableProcessingQueue() + + let observable = CoreDataContextObservable( + service: storageFacade.databaseService, + mapper: AnyCoreDataMapper(mapper), + predicate: { entity in + observingFilter(entity) + }, + processingQueue: processingQueue + ) + + observable.start { [weak self] error in + if let error = error { + self?.logger.error("Did receive error: \(error)") + } + } + + return StreamableProvider( + source: AnyStreamableSource(source), + repository: AnyDataProviderRepository(repository), + observable: AnyDataProviderRepositoryObservable(observable), + operationManager: operationManager, + serialQueue: processingQueue + ) + } } diff --git a/novawallet/Common/Extension/Foundation/BigUInt+Operation.swift b/novawallet/Common/Extension/Foundation/BigUInt+Operation.swift index f91e651e1c..1d75d6a570 100644 --- a/novawallet/Common/Extension/Foundation/BigUInt+Operation.swift +++ b/novawallet/Common/Extension/Foundation/BigUInt+Operation.swift @@ -2,7 +2,7 @@ import Foundation import BigInt extension BigUInt { - func saturatingSub(_ value: BigUInt) -> BigUInt { + func subtractOrZero(_ value: BigUInt) -> BigUInt { self > value ? self - value : 0 } } diff --git a/novawallet/Common/Extension/Foundation/Predicate/NSPredicate+Filter.swift b/novawallet/Common/Extension/Foundation/Predicate/NSPredicate+Filter.swift index 6a99ede71e..98ce9bb4d7 100644 --- a/novawallet/Common/Extension/Foundation/Predicate/NSPredicate+Filter.swift +++ b/novawallet/Common/Extension/Foundation/Predicate/NSPredicate+Filter.swift @@ -210,6 +210,52 @@ extension NSPredicate { ]) } + static func assetHold(chainId: ChainModel.Id, assetId: AssetModel.Id) -> NSPredicate { + let chainIdPredicate = NSPredicate( + format: "%K == %@", + #keyPath(CDAssetHold.chainId), + chainId + ) + + let assetIdPredicate = NSPredicate( + format: "%K == %d", + #keyPath(CDAssetHold.assetId), + assetId + ) + + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + chainIdPredicate, assetIdPredicate + ]) + } + + static func assetHold(for accountId: AccountId) -> NSPredicate { + NSPredicate( + format: "%K == %@", + #keyPath(CDAssetHold.chainAccountId), + accountId.toHex() + ) + } + + static func assetHold(for accountId: AccountId, chainAssetId: ChainAssetId) -> NSPredicate { + let accountPredicate = assetHold(for: accountId) + + let chainIdPredicate = NSPredicate( + format: "%K == %@", + #keyPath(CDAssetHold.chainId), + chainAssetId.chainId + ) + + let assetIdPredicate = NSPredicate( + format: "%K == %d", + #keyPath(CDAssetHold.assetId), + chainAssetId.assetId + ) + + return NSCompoundPredicate(andPredicateWithSubpredicates: [ + accountPredicate, chainIdPredicate, assetIdPredicate + ]) + } + static func nfts(for chainId: ChainModel.Id, ownerId: AccountId) -> NSPredicate { let chainPredicate = NSPredicate(format: "%K == %@", #keyPath(CDNft.chainId), chainId) let ownerPredicate = NSPredicate(format: "%K == %@", #keyPath(CDNft.ownerId), ownerId.toHex()) diff --git a/novawallet/Common/Extension/Foundation/String+Split.swift b/novawallet/Common/Extension/Foundation/String+Split.swift index 4da30e755a..7d78b111d9 100644 --- a/novawallet/Common/Extension/Foundation/String+Split.swift +++ b/novawallet/Common/Extension/Foundation/String+Split.swift @@ -5,6 +5,7 @@ extension String { case hashtag = "#" case space = " " case comma = "," + case dot = "." } enum CompoundSeparator: String { diff --git a/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift b/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift index 5ea5103069..992af00d27 100644 --- a/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift +++ b/novawallet/Common/Helpers/SubstrateRepositoryFactory.swift @@ -26,6 +26,11 @@ protocol SubstrateRepositoryFactoryProtocol { func createAssetLocksRepository(chainAssetIds: Set) -> AnyDataProviderRepository + func createAssetHoldsRepository( + for accountId: AccountId, + chainAssetId: ChainAssetId + ) -> AnyDataProviderRepository + func createChainAddressTxRepository( for address: AccountAddress, chainId: ChainModel.Id @@ -228,6 +233,13 @@ final class SubstrateRepositoryFactory: SubstrateRepositoryFactoryProtocol { createAssetLocksRepository(.assetLock(chainAssetIds: chainAssetIds)) } + func createAssetHoldsRepository( + for accountId: AccountId, + chainAssetId: ChainAssetId + ) -> AnyDataProviderRepository { + createAssetHoldsRepository(.assetHold(for: accountId, chainAssetId: chainAssetId)) + } + private func createAssetLocksRepository(_ filter: NSPredicate) -> AnyDataProviderRepository { let mapper = AssetLockMapper() let repository = storageFacade.createRepository( @@ -238,6 +250,17 @@ final class SubstrateRepositoryFactory: SubstrateRepositoryFactoryProtocol { return AnyDataProviderRepository(repository) } + private func createAssetHoldsRepository(_ filter: NSPredicate) -> AnyDataProviderRepository { + let mapper = AssetHoldMapper() + let repository = storageFacade.createRepository( + filter: filter, + sortDescriptors: [], + mapper: AnyCoreDataMapper(mapper) + ) + + return AnyDataProviderRepository(repository) + } + func createCrowdloanContributionRepository( accountId: AccountId, chainId: ChainModel.Id, diff --git a/novawallet/Common/Migration/SubstrateStorageVersion.swift b/novawallet/Common/Migration/SubstrateStorageVersion.swift index d37ae0fae0..2522775de4 100644 --- a/novawallet/Common/Migration/SubstrateStorageVersion.swift +++ b/novawallet/Common/Migration/SubstrateStorageVersion.swift @@ -28,6 +28,7 @@ enum SubstrateStorageVersion: String, CaseIterable { case version27 = "SubstrateDataModel27" case version28 = "SubstrateDataModel28" case version29 = "SubstrateDataModel29" + case version30 = "SubstrateDataModel30" static var current: SubstrateStorageVersion { allCases.last! @@ -92,6 +93,8 @@ enum SubstrateStorageVersion: String, CaseIterable { case .version28: return .version29 case .version29: + return .version30 + case .version30: return nil } } diff --git a/novawallet/Common/Model/AssetHold.swift b/novawallet/Common/Model/AssetHold.swift new file mode 100644 index 0000000000..329f1e6665 --- /dev/null +++ b/novawallet/Common/Model/AssetHold.swift @@ -0,0 +1,37 @@ +import Foundation +import BigInt +import Operation_iOS + +struct AssetHold: Equatable { + let chainAssetId: ChainAssetId + let accountId: AccountId + let module: String + let reason: String + let amount: BigUInt +} + +extension AssetHold: Identifiable { + static func createIdentifier( + for chainAssetId: ChainAssetId, + accountId: AccountId, + module: String, + reason: String + ) -> String { + let data = [ + chainAssetId.stringValue, + accountId.toHex(), + module, + reason + ].joined(separator: "-").data(using: .utf8)! + return data.sha256().toHex() + } + + var identifier: String { + Self.createIdentifier( + for: chainAssetId, + accountId: accountId, + module: module, + reason: reason + ) + } +} diff --git a/novawallet/Common/Model/KnownChainIds.swift b/novawallet/Common/Model/KnownChainIds.swift index 16f43626b3..70bfd7ff71 100644 --- a/novawallet/Common/Model/KnownChainIds.swift +++ b/novawallet/Common/Model/KnownChainIds.swift @@ -28,7 +28,7 @@ enum KnowChainId { static let westmint = "67f9723393ef76214df0118c34bbbd3dbebc8ed46a10973a8c969d48fe7598c9" static let hydra = "afdc188f45c71dacbaa0b62e16a91f726c7b8699a9748cdf715459de6b7f366d" static let polimec = "7eb9354488318e7549c722669dcbdcdc526f1fef1420e7944667212f3601fdbd" - static let avail = "128ea318539862c0a06b745981300d527c1041c6f3388a8c49565559e3ea3d10" + static let avail = "b91746b45e0346cc2f815a520b9c6cb4d5c0902af848db0a80f85932d2e8276a" static let availTuringTestnet = "d3d2f3a3495dc597434a99d7d449ebad6616db45e4e4f178f31cc6fa14378b70" static let vara = "fe1b4c55fd4d668101126434206571a7838a8b6b93a6d1b95d607e78e6c53763" static let mythos = "f6ee56e9c5277df5b4ce6ae9983ee88f3cbed27d31beeb98f9f84f997a1ab0b9" diff --git a/novawallet/Common/Protocols/SCLoadableControllerProtocol.swift b/novawallet/Common/Protocols/SCLoadableControllerProtocol.swift new file mode 100644 index 0000000000..0cb2b33f3d --- /dev/null +++ b/novawallet/Common/Protocols/SCLoadableControllerProtocol.swift @@ -0,0 +1,17 @@ +import UIKit + +protocol SCLoadableControllerProtocol: ControllerBackedProtocol { + func didStartLoading() + func didStopLoading() +} + +extension SCLoadableControllerProtocol where Self: UIViewController & ViewHolder, + Self.RootViewType: SCLoadableActionLayoutView { + func didStartLoading() { + rootView.genericActionView.startLoading() + } + + func didStopLoading() { + rootView.genericActionView.stopLoading() + } +} diff --git a/novawallet/Common/Services/ExternalBalanceUpdater/PooledBalanceService/PooledBalanceUpdatingService.swift b/novawallet/Common/Services/ExternalBalanceUpdater/PooledBalanceService/PooledBalanceUpdatingService.swift index 697b725d4c..02d662b67e 100644 --- a/novawallet/Common/Services/ExternalBalanceUpdater/PooledBalanceService/PooledBalanceUpdatingService.swift +++ b/novawallet/Common/Services/ExternalBalanceUpdater/PooledBalanceService/PooledBalanceUpdatingService.swift @@ -3,6 +3,11 @@ import SubstrateSdk import Operation_iOS final class PooledBalanceUpdatingService: BaseSyncService, RuntimeConstantFetching { + struct StateParams { + let palletId: Data + let supportsDelegatedStaking: Bool + } + let accountId: AccountId let chainAsset: ChainAsset let connection: JSONRPCEngine @@ -103,10 +108,11 @@ final class PooledBalanceUpdatingService: BaseSyncService, RuntimeConstantFetchi poolMember: poolMember, ledger: nil, bondedPool: nil, - subPools: nil + subPools: nil, + stakingDelegation: nil ) - resolvePalletIdAndSubscribeState(for: poolMember, accountId: accountId) + resolveStateParamsAndSubscribeState(for: poolMember, accountId: accountId) } else { state = nil @@ -117,16 +123,51 @@ final class PooledBalanceUpdatingService: BaseSyncService, RuntimeConstantFetchi } } - private func resolvePalletIdAndSubscribeState(for poolMember: NominationPools.PoolMember, accountId _: AccountId) { + private func createStateParamsWrapper() -> CompoundOperationWrapper { + let codingFactoryOperation = runtimeService.fetchCoderFactoryOperation() + let palletIdOperation = StorageConstantOperation( + path: NominationPools.palletIdPath, + fallbackValue: nil + ) + + palletIdOperation.configurationBlock = { + do { + palletIdOperation.codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + } catch { + palletIdOperation.result = .failure(error) + } + } + + palletIdOperation.addDependency(codingFactoryOperation) + + let mergeOperation = ClosureOperation { + let palletId = try palletIdOperation.extractNoCancellableResultData().wrappedValue + let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + + let supportsDelegatedStaking = codingFactory.hasStorage(for: DelegatedStakingPallet.delegatorsPath) + + return StateParams(palletId: palletId, supportsDelegatedStaking: supportsDelegatedStaking) + } + + mergeOperation.addDependency(palletIdOperation) + mergeOperation.addDependency(codingFactoryOperation) + + return CompoundOperationWrapper( + targetOperation: mergeOperation, + dependencies: [codingFactoryOperation, palletIdOperation] + ) + } + + private func resolveStateParamsAndSubscribeState(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 + let wrapper = createStateParamsWrapper() + + execute( + wrapper: wrapper, + inOperationQueue: operationQueue, + runningCallbackIn: workingQueue + ) { [weak self] result in self?.mutex.lock() defer { @@ -139,30 +180,38 @@ final class PooledBalanceUpdatingService: BaseSyncService, RuntimeConstantFetchi } switch result { - case let .success(palletId): + case let .success(stateParams): if let poolAccountId = try? NominationPools.derivedAccount( for: poolMember.poolId, accountType: .bonded, - palletId: palletId.wrappedValue + palletId: stateParams.palletId ) { self?.logger.debug("Derived pool account id: \(poolAccountId.toHex())") - self?.subscribeState(for: poolAccountId, poolId: poolMember.poolId) + self?.subscribeState( + for: poolAccountId, + poolId: poolMember.poolId, + delegatorAccountId: accountId, + supportsDelegatedStaking: stateParams.supportsDelegatedStaking + ) } 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?.logger.error("Can't get state params \(error)") self?.completeImmediate(error) } } } - private func subscribeState(for poolAccountId: AccountId, poolId: NominationPools.PoolId) { - clearStateSubscription() - + private func prepareStateRequests( + for poolAccountId: AccountId, + poolId: NominationPools.PoolId, + delegatorAccountId: AccountId, + supportsDelegatedStaking: Bool + ) -> [BatchStorageSubscriptionRequest] { let ledgerRequest = BatchStorageSubscriptionRequest( innerRequest: MapSubscriptionRequest( storagePath: Staking.stakingLedger, @@ -196,8 +245,41 @@ final class PooledBalanceUpdatingService: BaseSyncService, RuntimeConstantFetchi mappingKey: PooledBalanceStateChange.Key.bonded.rawValue ) + if supportsDelegatedStaking { + let delegatedStakingRequest = BatchStorageSubscriptionRequest( + innerRequest: MapSubscriptionRequest( + storagePath: DelegatedStakingPallet.delegatorsPath, + localKey: .empty, + keyParamClosure: { + BytesCodable(wrappedValue: delegatorAccountId) + } + ), + mappingKey: PooledBalanceStateChange.Key.stakingDelegation.rawValue + ) + + return [ledgerRequest, bondedPoolRequest, subPoolsRequest, delegatedStakingRequest] + } else { + return [ledgerRequest, bondedPoolRequest, subPoolsRequest] + } + } + + private func subscribeState( + for poolAccountId: AccountId, + poolId: NominationPools.PoolId, + delegatorAccountId: AccountId, + supportsDelegatedStaking: Bool + ) { + clearStateSubscription() + + let requests = prepareStateRequests( + for: poolAccountId, + poolId: poolId, + delegatorAccountId: delegatorAccountId, + supportsDelegatedStaking: supportsDelegatedStaking + ) + stateSubscription = CallbackBatchStorageSubscription( - requests: [ledgerRequest, bondedPoolRequest, subPoolsRequest], + requests: requests, connection: connection, runtimeService: runtimeService, repository: nil, @@ -238,11 +320,15 @@ final class PooledBalanceUpdatingService: BaseSyncService, RuntimeConstantFetchi let optItem: PooledAssetBalance? let removeIdentifier: String? - if let poolId = state?.poolId, let totalStake = state?.totalStake { + if let poolId = state?.poolId { + guard let stake = state?.stakeNotIncludedIntoDelegatedStaking else { + return + } + optItem = PooledAssetBalance( chainAssetId: chainAsset.chainAssetId, accountId: accountId, - amount: totalStake, + amount: stake, poolId: poolId ) diff --git a/novawallet/Common/Services/ExternalBalanceUpdater/PooledBalanceService/PooledBalanceUpdatingState.swift b/novawallet/Common/Services/ExternalBalanceUpdater/PooledBalanceService/PooledBalanceUpdatingState.swift index b852c6e0a9..6735c94fcf 100644 --- a/novawallet/Common/Services/ExternalBalanceUpdater/PooledBalanceService/PooledBalanceUpdatingState.swift +++ b/novawallet/Common/Services/ExternalBalanceUpdater/PooledBalanceService/PooledBalanceUpdatingState.swift @@ -7,11 +7,13 @@ struct PooledBalanceStateChange: BatchStorageSubscriptionResult { case ledger case bonded case subpools + case stakingDelegation } let ledger: UncertainStorage let bondedPool: UncertainStorage let subPools: UncertainStorage + let stakingDelegation: UncertainStorage init( values: [BatchStorageSubscriptionResultValue], @@ -35,6 +37,12 @@ struct PooledBalanceStateChange: BatchStorageSubscriptionResult { mappingKey: Key.subpools.rawValue, context: context ) + + stakingDelegation = try UncertainStorage( + values: values, + mappingKey: Key.stakingDelegation.rawValue, + context: context + ) } } @@ -43,6 +51,7 @@ struct PooledBalanceState { let ledger: StakingLedger? let bondedPool: NominationPools.BondedPool? let subPools: NominationPools.SubPools? + let stakingDelegation: DelegatedStakingPallet.Delegation? var poolId: NominationPools.PoolId { poolMember.poolId @@ -64,16 +73,30 @@ struct PooledBalanceState { return activeStake + unbondingStake } + var stakeNotIncludedIntoDelegatedStaking: BigUInt? { + guard let total = totalStake else { + return nil + } + + guard let stakingDelegation = stakingDelegation else { + return total + } + + return total.subtractOrZero(stakingDelegation.amount) + } + 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) + let newStakingDelegation = change.stakingDelegation.valueWhenDefined(else: stakingDelegation) return .init( poolMember: poolMember, ledger: newLedger, bondedPool: newBondedPool, - subPools: newSubPools + subPools: newSubPools, + stakingDelegation: newStakingDelegation ) } @@ -82,7 +105,8 @@ struct PooledBalanceState { poolMember: newPoolMember, ledger: ledger, bondedPool: bondedPool, - subPools: subPools + subPools: subPools, + stakingDelegation: stakingDelegation ) } } diff --git a/novawallet/Common/Services/GitHubPhishingService/PhishingSiteVerifier+Init.swift b/novawallet/Common/Services/GitHubPhishingService/PhishingSiteVerifier+Init.swift index b087e2123b..eb7efe22c5 100644 --- a/novawallet/Common/Services/GitHubPhishingService/PhishingSiteVerifier+Init.swift +++ b/novawallet/Common/Services/GitHubPhishingService/PhishingSiteVerifier+Init.swift @@ -8,6 +8,10 @@ extension PhishingSiteVerifier { let operationQueue = OperationQueue() operationQueue.maxConcurrentOperationCount = 1 - return PhishingSiteVerifier(repositoryFactory: factory, operationQueue: operationQueue) + return PhishingSiteVerifier( + forbiddenTopLevelDomains: ApplicationConfig.shared.phishingDAppsTopLevelSet, + repositoryFactory: factory, + operationQueue: operationQueue + ) } } diff --git a/novawallet/Common/Services/GitHubPhishingService/PhishingSiteVerifier.swift b/novawallet/Common/Services/GitHubPhishingService/PhishingSiteVerifier.swift index 46bdb2ab13..1e7fb50cb6 100644 --- a/novawallet/Common/Services/GitHubPhishingService/PhishingSiteVerifier.swift +++ b/novawallet/Common/Services/GitHubPhishingService/PhishingSiteVerifier.swift @@ -7,15 +7,29 @@ protocol PhishingSiteVerifing { } final class PhishingSiteVerifier: PhishingSiteVerifing { + let forbiddenTopLevelDomains: Set let repositoryFactory: SubstrateRepositoryFactoryProtocol let operationQueue: OperationQueue - init(repositoryFactory: SubstrateRepositoryFactoryProtocol, operationQueue: OperationQueue) { + init( + forbiddenTopLevelDomains: Set, + repositoryFactory: SubstrateRepositoryFactoryProtocol, + operationQueue: OperationQueue + ) { + self.forbiddenTopLevelDomains = forbiddenTopLevelDomains self.repositoryFactory = repositoryFactory self.operationQueue = operationQueue } func verify(host: String, completion: @escaping (Result) -> Void) { + guard + let topLevel = host.split(by: .dot).last, + !forbiddenTopLevelDomains.contains(topLevel) + else { + completion(.success(false)) + return + } + let filter = NSPredicate.filterPhishingSitesDomain(host) let repository = repositoryFactory.createPhishingSitesRepositoryWithPredicate(filter) let fetchOperation = repository.fetchCountOperation() diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/AccountInfoSubscriptionHandlingFactory.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/AccountInfoSubscriptionHandlingFactory.swift index 6b0946a935..67a444ad5f 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/AccountInfoSubscriptionHandlingFactory.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/AccountInfoSubscriptionHandlingFactory.swift @@ -4,15 +4,18 @@ import Operation_iOS final class AccountInfoSubscriptionHandlingFactory: RemoteSubscriptionHandlingFactoryProtocol { let accountLocalStorageKey: String let locksLocalStorageKey: String + let holdsLocalStorageKey: String let factory: NativeTokenSubscriptionFactoryProtocol init( accountLocalStorageKey: String, locksLocalStorageKey: String, + holdsLocalStorageKey: String, factory: NativeTokenSubscriptionFactoryProtocol ) { self.accountLocalStorageKey = accountLocalStorageKey self.locksLocalStorageKey = locksLocalStorageKey + self.holdsLocalStorageKey = holdsLocalStorageKey self.factory = factory } @@ -29,6 +32,12 @@ final class AccountInfoSubscriptionHandlingFactory: RemoteSubscriptionHandlingFa operationManager: operationManager, logger: logger ) + } else if holdsLocalStorageKey == localStorageKey { + return factory.createBalanceHoldsSubscription( + remoteStorageKey: remoteStorageKey, + operationManager: operationManager, + logger: logger + ) } else { return factory.createAccountInfoSubscription( remoteStorageKey: remoteStorageKey, diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/BalanceRemoteSubscriptionHandlingFactory.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/BalanceRemoteSubscriptionHandlingFactory.swift index 86695e1bb4..be6b6a0d1e 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/BalanceRemoteSubscriptionHandlingFactory.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/BalanceRemoteSubscriptionHandlingFactory.swift @@ -2,7 +2,13 @@ import Foundation import Operation_iOS enum BalanceRemoteSubscriptionHandlingParams { - struct Common { + struct BalancesPallet { + let accountLocalStorageKey: String + let locksLocalStorageKey: String + let holdsLocalStorageKey: String + } + + struct OrmlPallet { let accountLocalStorageKey: String let locksLocalStorageKey: String } @@ -18,14 +24,14 @@ protocol BalanceRemoteSubscriptionHandlingFactoryProtocol { func createNative( for accountId: AccountId, chainAssetId: ChainAssetId, - params: BalanceRemoteSubscriptionHandlingParams.Common, + params: BalanceRemoteSubscriptionHandlingParams.BalancesPallet, transactionSubscription: TransactionSubscribing? ) -> RemoteSubscriptionHandlingFactoryProtocol func createOrml( for accountId: AccountId, chainAssetId: ChainAssetId, - params: BalanceRemoteSubscriptionHandlingParams.Common, + params: BalanceRemoteSubscriptionHandlingParams.OrmlPallet, transactionSubscription: TransactionSubscribing? ) -> RemoteSubscriptionHandlingFactoryProtocol @@ -64,15 +70,12 @@ final class BalanceRemoteSubscriptionHandlingFactory { transactionSubscription: TransactionSubscribing? ) -> TokenSubscriptionFactory { let repositoryFactory = SubstrateRepositoryFactory(storageFacade: substrateStorageFacade) - let assetRepository = repositoryFactory.createAssetBalanceRepository() - let locksRepository = repositoryFactory.createAssetLocksRepository(for: accountId, chainAssetId: chainAssetId) return TokenSubscriptionFactory( chainAssetId: chainAssetId, accountId: accountId, chainRegistry: chainRegistry, - assetRepository: assetRepository, - locksRepository: locksRepository, + repositoryFactory: repositoryFactory, eventCenter: eventCenter, transactionSubscription: transactionSubscription ) @@ -83,7 +86,7 @@ extension BalanceRemoteSubscriptionHandlingFactory: BalanceRemoteSubscriptionHan func createNative( for accountId: AccountId, chainAssetId: ChainAssetId, - params: BalanceRemoteSubscriptionHandlingParams.Common, + params: BalanceRemoteSubscriptionHandlingParams.BalancesPallet, transactionSubscription: TransactionSubscribing? ) -> RemoteSubscriptionHandlingFactoryProtocol { let innerFactory = createTokensSubscriptionFactory( @@ -95,6 +98,7 @@ extension BalanceRemoteSubscriptionHandlingFactory: BalanceRemoteSubscriptionHan return AccountInfoSubscriptionHandlingFactory( accountLocalStorageKey: params.accountLocalStorageKey, locksLocalStorageKey: params.locksLocalStorageKey, + holdsLocalStorageKey: params.holdsLocalStorageKey, factory: innerFactory ) } @@ -102,7 +106,7 @@ extension BalanceRemoteSubscriptionHandlingFactory: BalanceRemoteSubscriptionHan func createOrml( for accountId: AccountId, chainAssetId: ChainAssetId, - params: BalanceRemoteSubscriptionHandlingParams.Common, + params: BalanceRemoteSubscriptionHandlingParams.OrmlPallet, transactionSubscription: TransactionSubscribing? ) -> RemoteSubscriptionHandlingFactoryProtocol { let innerFactory = createTokensSubscriptionFactory( diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/BalanceRemoteSubscriptionService.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/BalanceRemoteSubscriptionService.swift index 7f9d626f35..c8736f4786 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/BalanceRemoteSubscriptionService.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/BalanceRemoteSubscriptionService.swift @@ -65,25 +65,17 @@ final class BalanceRemoteSubscriptionService: RemoteSubscriptionService { ) } - private func createCacheKey(from accountId: AccountId, chainId: ChainModel.Id) -> String { - "balances-\(accountId.toHex())-\(chainId)" - } - - private func createCacheKey(from accountId: AccountId, chainAssetId: ChainAssetId) -> String { - "balances-\(accountId.toHex())-\(chainAssetId.chainId)-\(chainAssetId.assetId)" - } - private func prepareNativeAssetSubscriptionRequests( from accountId: AccountId, chainAsset: ChainAsset, transactionSubscription: TransactionSubscribing? ) throws -> [SubscriptionSettings] { - let storagePath = StorageCodingPath.account let storageKeyFactory = LocalStorageKeyFactory() let chainId = chainAsset.chain.chainId + let accountStoragePath = StorageCodingPath.account let accountLocalKey = try storageKeyFactory.createFromStoragePath( - storagePath, + accountStoragePath, accountId: accountId, chainId: chainId ) @@ -95,26 +87,42 @@ final class BalanceRemoteSubscriptionService: RemoteSubscriptionService { chainId: chainId ) - let accountRequest = MapSubscriptionRequest( - storagePath: storagePath, - localKey: accountLocalKey - ) { BytesCodable(wrappedValue: accountId) } + let holdsStoragePath = BalancesPallet.holdsPath + let holdsLocalKey = try storageKeyFactory.createFromStoragePath( + holdsStoragePath, + encodableElement: accountId, + chainId: chainId + ) + + let accountRequest = MapSubscriptionRequest(storagePath: accountStoragePath, localKey: accountLocalKey) { + BytesCodable(wrappedValue: accountId) + } let locksRequest = MapSubscriptionRequest( - storagePath: .balanceLocks, + storagePath: locksStoragePath, localKey: locksLocalKey ) { BytesCodable(wrappedValue: accountId) } + let holdsRequest = MapSubscriptionRequest( + storagePath: holdsStoragePath, + localKey: holdsLocalKey + ) { BytesCodable(wrappedValue: accountId) } + let handlerFactory = subscriptionHandlingFactory.createNative( for: accountId, chainAssetId: chainAsset.chainAssetId, - params: .init(accountLocalStorageKey: accountLocalKey, locksLocalStorageKey: locksLocalKey), + params: .init( + accountLocalStorageKey: accountLocalKey, + locksLocalStorageKey: locksLocalKey, + holdsLocalStorageKey: holdsLocalKey + ), transactionSubscription: transactionSubscription ) return [ SubscriptionSettings(request: accountRequest, handlingFactory: handlerFactory), - SubscriptionSettings(request: locksRequest, handlingFactory: handlerFactory) + SubscriptionSettings(request: locksRequest, handlingFactory: handlerFactory), + SubscriptionSettings(request: holdsRequest, handlingFactory: handlerFactory) ] } @@ -228,7 +236,7 @@ final class BalanceRemoteSubscriptionService: RemoteSubscriptionService { ] } - private func prepareSubscriptionRequests( + func prepareSubscriptionRequests( from accountId: AccountId, chainAsset: ChainAsset, transactionSubscription: TransactionSubscribing? @@ -269,7 +277,7 @@ final class BalanceRemoteSubscriptionService: RemoteSubscriptionService { } } - private func prepareSubscriptionRequests( + func prepareSubscriptionRequests( from accountId: AccountId, chain: ChainModel, onlyFor assetIds: Set?, @@ -290,112 +298,3 @@ final class BalanceRemoteSubscriptionService: RemoteSubscriptionService { } } } - -extension BalanceRemoteSubscriptionService: BalanceRemoteSubscriptionServiceProtocol { - func attachToBalances( - for accountId: AccountId, - chain: ChainModel, - onlyFor assetIds: Set?, - queue: DispatchQueue?, - closure: RemoteSubscriptionClosure? - ) -> UUID? { - guard - let transactionSubscription = try? transactionSubscriptionFactory.createTransactionSubscription( - for: accountId, - chain: chain - ) else { - logger.error("Can't create transaction subscription") - return nil - } - - let subscriptionSettingsList = prepareSubscriptionRequests( - from: accountId, - chain: chain, - onlyFor: assetIds, - transactionSubscription: transactionSubscription - ) - - guard !subscriptionSettingsList.isEmpty else { - return nil - } - - let cacheKey = createCacheKey(from: accountId, chainId: chain.chainId) - - let requests = subscriptionSettingsList.map(\.request) - let handlersStore = subscriptionSettingsList.reduce( - into: [String: RemoteSubscriptionHandlingFactoryProtocol]() - ) { accum, settings in - accum[settings.request.localKey] = settings.handlingFactory - } - - let handlingFactory = BalanceRemoteSubscriptionHandlingProxy(store: handlersStore) - - return attachToSubscription( - with: requests, - chainId: chain.chainId, - cacheKey: cacheKey, - queue: queue, - closure: closure, - subscriptionHandlingFactory: handlingFactory - ) - } - - func detachFromBalances( - for subscriptionId: UUID, - accountId: AccountId, - chainId: ChainModel.Id, - queue: DispatchQueue?, - closure: RemoteSubscriptionClosure? - ) { - let cacheKey = createCacheKey(from: accountId, chainId: chainId) - detachFromSubscription(cacheKey, subscriptionId: subscriptionId, queue: queue, closure: closure) - } - - func attachToAssetBalance( - for accountId: AccountId, - chainAsset: ChainAsset, - queue: DispatchQueue?, - closure: RemoteSubscriptionClosure? - ) -> UUID? { - let subscriptionSettingsList = prepareSubscriptionRequests( - from: accountId, - chainAsset: chainAsset, - transactionSubscription: nil - ) - - guard !subscriptionSettingsList.isEmpty else { - return nil - } - - let cacheKey = createCacheKey(from: accountId, chainAssetId: chainAsset.chainAssetId) - - let requests = subscriptionSettingsList.map(\.request) - let handlersStore = subscriptionSettingsList.reduce( - into: [String: RemoteSubscriptionHandlingFactoryProtocol]() - ) { accum, settings in - accum[settings.request.localKey] = settings.handlingFactory - } - - let handlingFactory = BalanceRemoteSubscriptionHandlingProxy(store: handlersStore) - - return attachToSubscription( - with: requests, - chainId: chainAsset.chain.chainId, - cacheKey: cacheKey, - queue: queue, - closure: closure, - subscriptionHandlingFactory: handlingFactory - ) - } - - func detachFromAssetBalance( - for subscriptionId: UUID, - accountId: AccountId, - chainAssetId: ChainAssetId, - queue: DispatchQueue?, - closure: RemoteSubscriptionClosure? - ) { - let cacheKey = createCacheKey(from: accountId, chainAssetId: chainAssetId) - detachFromSubscription(cacheKey, subscriptionId: subscriptionId, queue: queue, closure: closure) - } -} diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/BalancesRemoteSubscriptionService+Protocol.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/BalancesRemoteSubscriptionService+Protocol.swift new file mode 100644 index 0000000000..5f680d65ab --- /dev/null +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/BalancesRemoteSubscriptionService+Protocol.swift @@ -0,0 +1,118 @@ +import Foundation + +extension BalanceRemoteSubscriptionService: BalanceRemoteSubscriptionServiceProtocol { + private func createCacheKey(from accountId: AccountId, chainId: ChainModel.Id) -> String { + "balances-\(accountId.toHex())-\(chainId)" + } + + private func createCacheKey(from accountId: AccountId, chainAssetId: ChainAssetId) -> String { + "balances-\(accountId.toHex())-\(chainAssetId.chainId)-\(chainAssetId.assetId)" + } + + func attachToBalances( + for accountId: AccountId, + chain: ChainModel, + onlyFor assetIds: Set?, + queue: DispatchQueue?, + closure: RemoteSubscriptionClosure? + ) -> UUID? { + guard + let transactionSubscription = try? transactionSubscriptionFactory.createTransactionSubscription( + for: accountId, + chain: chain + ) else { + logger.error("Can't create transaction subscription") + return nil + } + + let subscriptionSettingsList = prepareSubscriptionRequests( + from: accountId, + chain: chain, + onlyFor: assetIds, + transactionSubscription: transactionSubscription + ) + + guard !subscriptionSettingsList.isEmpty else { + return nil + } + + let cacheKey = createCacheKey(from: accountId, chainId: chain.chainId) + + let requests = subscriptionSettingsList.map(\.request) + let handlersStore = subscriptionSettingsList.reduce( + into: [String: RemoteSubscriptionHandlingFactoryProtocol]() + ) { accum, settings in + accum[settings.request.localKey] = settings.handlingFactory + } + + let handlingFactory = BalanceRemoteSubscriptionHandlingProxy(store: handlersStore) + + return attachToSubscription( + with: requests, + chainId: chain.chainId, + cacheKey: cacheKey, + queue: queue, + closure: closure, + subscriptionHandlingFactory: handlingFactory + ) + } + + func detachFromBalances( + for subscriptionId: UUID, + accountId: AccountId, + chainId: ChainModel.Id, + queue: DispatchQueue?, + closure: RemoteSubscriptionClosure? + ) { + let cacheKey = createCacheKey(from: accountId, chainId: chainId) + detachFromSubscription(cacheKey, subscriptionId: subscriptionId, queue: queue, closure: closure) + } + + func attachToAssetBalance( + for accountId: AccountId, + chainAsset: ChainAsset, + queue: DispatchQueue?, + closure: RemoteSubscriptionClosure? + ) -> UUID? { + let subscriptionSettingsList = prepareSubscriptionRequests( + from: accountId, + chainAsset: chainAsset, + transactionSubscription: nil + ) + + guard !subscriptionSettingsList.isEmpty else { + return nil + } + + let cacheKey = createCacheKey(from: accountId, chainAssetId: chainAsset.chainAssetId) + + let requests = subscriptionSettingsList.map(\.request) + let handlersStore = subscriptionSettingsList.reduce( + into: [String: RemoteSubscriptionHandlingFactoryProtocol]() + ) { accum, settings in + accum[settings.request.localKey] = settings.handlingFactory + } + + let handlingFactory = BalanceRemoteSubscriptionHandlingProxy(store: handlersStore) + + return attachToSubscription( + with: requests, + chainId: chainAsset.chain.chainId, + cacheKey: cacheKey, + queue: queue, + closure: closure, + subscriptionHandlingFactory: handlingFactory + ) + } + + func detachFromAssetBalance( + for subscriptionId: UUID, + accountId: AccountId, + chainAssetId: ChainAssetId, + queue: DispatchQueue?, + closure: RemoteSubscriptionClosure? + ) { + let cacheKey = createCacheKey(from: accountId, chainAssetId: chainAssetId) + detachFromSubscription(cacheKey, subscriptionId: subscriptionId, queue: queue, closure: closure) + } +} diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsAccountUpdatingService.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsAccountUpdatingService.swift index 5a06f1b402..2a742677dd 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsAccountUpdatingService.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsAccountUpdatingService.swift @@ -73,10 +73,13 @@ final class NominationPoolsAccountUpdatingService: BaseSyncService, NPoolsLocalS private func clearRemoteSubscription() { if let remoteSubscriptionId = remoteSubscriptionId, let poolId = poolId { - remoteSubscriptionService.detachFromPoolData( + remoteSubscriptionService.detachFromAccountPoolData( for: remoteSubscriptionId, - chainId: chainAsset.chain.chainId, - poolId: poolId, + params: NPoolSubscriptionServiceParams( + chainId: chainAsset.chain.chainId, + poolId: poolId, + accountId: accountId + ), queue: nil, closure: nil ) @@ -86,9 +89,12 @@ final class NominationPoolsAccountUpdatingService: BaseSyncService, NPoolsLocalS } private func subscribeRemote(for poolId: NominationPools.PoolId) { - remoteSubscriptionId = remoteSubscriptionService.attachToPoolData( - for: chainAsset.chain.chainId, - poolId: poolId, + remoteSubscriptionId = remoteSubscriptionService.attachToAccountPoolData( + for: NPoolSubscriptionServiceParams( + chainId: chainAsset.chain.chainId, + poolId: poolId, + accountId: accountId + ), queue: .global(qos: .userInitiated) ) { [weak self] result in self?.mutex.lock() diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsPoolSubscriptionService.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsPoolSubscriptionService.swift index ae33b34a1e..018630751f 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsPoolSubscriptionService.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/NominationPoolsPoolSubscriptionService.swift @@ -1,18 +1,26 @@ import Foundation import SubstrateSdk +struct NPoolSubscriptionServiceParams { + let chainId: ChainModel.Id + let poolId: NominationPools.PoolId + let accountId: AccountId + + func encodableSubscriptionElement() -> [UInt8] { + accountId.bytes + poolId.bigEndianBytes + } +} + protocol NominationPoolsPoolSubscriptionServiceProtocol { - func attachToPoolData( - for chainId: ChainModel.Id, - poolId: NominationPools.PoolId, + func attachToAccountPoolData( + for params: NPoolSubscriptionServiceParams, queue: DispatchQueue?, closure: RemoteSubscriptionClosure? ) -> UUID? - func detachFromPoolData( + func detachFromAccountPoolData( for subscriptionId: UUID, - chainId: ChainModel.Id, - poolId: NominationPools.PoolId, + params: NPoolSubscriptionServiceParams, queue: DispatchQueue?, closure: RemoteSubscriptionClosure? ) @@ -24,12 +32,15 @@ final class NominationPoolsPoolSubscriptionService: RemoteSubscriptionService { NominationPools.rewardPoolsPath, NominationPools.subPoolsPath ] + + private static let accountStoragePaths: [StorageCodingPath] = [ + DelegatedStakingPallet.delegatorsPath + ] } extension NominationPoolsPoolSubscriptionService: NominationPoolsPoolSubscriptionServiceProtocol { - func attachToPoolData( - for chainId: ChainModel.Id, - poolId: NominationPools.PoolId, + func attachToAccountPoolData( + for params: NPoolSubscriptionServiceParams, queue: DispatchQueue?, closure: RemoteSubscriptionClosure? ) -> UUID? { @@ -40,28 +51,40 @@ extension NominationPoolsPoolSubscriptionService: NominationPoolsPoolSubscriptio .map { path in let localKey = try localKeyFactory.createFromStoragePath( path, - encodableElement: poolId, - chainId: chainId + encodableElement: params.poolId, + chainId: params.chainId ) return MapSubscriptionRequest(storagePath: path, localKey: localKey) { - StringScaleMapper(value: poolId) + StringScaleMapper(value: params.poolId) } } - let allPaths = Self.poolIdStoragePaths + let accountRequests: [SubscriptionRequestProtocol] = try Self.accountStoragePaths.map { path in + let localKey = try localKeyFactory.createFromStoragePath( + path, + accountId: params.accountId, + chainId: params.chainId + ) + + return MapSubscriptionRequest(storagePath: path, localKey: localKey) { + BytesCodable(wrappedValue: params.accountId) + } + } + + let allPaths = Self.poolIdStoragePaths + Self.accountStoragePaths let cacheKey = try localKeyFactory.createRestorableCacheKey( from: allPaths, - encodableElement: poolId, - chainId: chainId + encodableElement: params.encodableSubscriptionElement(), + chainId: params.chainId ) - let allRequests = poolIdRequests + let allRequests = poolIdRequests + accountRequests return attachToSubscription( with: allRequests, - chainId: chainId, + chainId: params.chainId, cacheKey: cacheKey, queue: queue, closure: closure @@ -73,10 +96,9 @@ extension NominationPoolsPoolSubscriptionService: NominationPoolsPoolSubscriptio } } - func detachFromPoolData( + func detachFromAccountPoolData( for subscriptionId: UUID, - chainId: ChainModel.Id, - poolId: NominationPools.PoolId, + params: NPoolSubscriptionServiceParams, queue: DispatchQueue?, closure: RemoteSubscriptionClosure? ) { @@ -85,8 +107,8 @@ extension NominationPoolsPoolSubscriptionService: NominationPoolsPoolSubscriptio let cacheKey = try LocalStorageKeyFactory().createRestorableCacheKey( from: allPaths, - encodableElement: poolId, - chainId: chainId + encodableElement: params.encodableSubscriptionElement(), + chainId: params.chainId ) detachFromSubscription( diff --git a/novawallet/Common/Services/RemoteSubscription/Substrate/TokenSubscriptionFactory.swift b/novawallet/Common/Services/RemoteSubscription/Substrate/TokenSubscriptionFactory.swift index ccae288a3c..1bd3734c24 100644 --- a/novawallet/Common/Services/RemoteSubscription/Substrate/TokenSubscriptionFactory.swift +++ b/novawallet/Common/Services/RemoteSubscription/Substrate/TokenSubscriptionFactory.swift @@ -32,6 +32,12 @@ protocol NativeTokenSubscriptionFactoryProtocol { operationManager: OperationManagerProtocol, logger: LoggerProtocol ) -> StorageChildSubscribing + + func createBalanceHoldsSubscription( + remoteStorageKey: Data, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing } // MARK: - OrmlTokenSubscriptionFactoryProtocol @@ -40,29 +46,38 @@ final class TokenSubscriptionFactory: OrmlTokenSubscriptionFactoryProtocol { let chainAssetId: ChainAssetId let accountId: AccountId let chainRegistry: ChainRegistryProtocol - let assetRepository: AnyDataProviderRepository let eventCenter: EventCenterProtocol let transactionSubscription: TransactionSubscribing? - let locksRepository: AnyDataProviderRepository + let repositoryFactory: SubstrateRepositoryFactoryProtocol init( chainAssetId: ChainAssetId, accountId: AccountId, chainRegistry: ChainRegistryProtocol, - assetRepository: AnyDataProviderRepository, - locksRepository: AnyDataProviderRepository, + repositoryFactory: SubstrateRepositoryFactoryProtocol, eventCenter: EventCenterProtocol, transactionSubscription: TransactionSubscribing? ) { self.chainAssetId = chainAssetId self.accountId = accountId self.chainRegistry = chainRegistry - self.assetRepository = assetRepository - self.locksRepository = locksRepository + self.repositoryFactory = repositoryFactory self.eventCenter = eventCenter self.transactionSubscription = transactionSubscription } + private func createAssetBalanceRepository() -> AnyDataProviderRepository { + repositoryFactory.createAssetBalanceRepository() + } + + private func createAssetLocksRepository() -> AnyDataProviderRepository { + repositoryFactory.createAssetLocksRepository(for: accountId, chainAssetId: chainAssetId) + } + + private func createAssetHoldsRepository() -> AnyDataProviderRepository { + repositoryFactory.createAssetHoldsRepository(for: accountId, chainAssetId: chainAssetId) + } + func createOrmlAccountSubscription( remoteStorageKey: Data, localStorageKey _: String, @@ -74,7 +89,7 @@ final class TokenSubscriptionFactory: OrmlTokenSubscriptionFactoryProtocol { chainAssetId: chainAssetId, accountId: accountId, chainRegistry: chainRegistry, - assetRepository: assetRepository, + assetRepository: createAssetBalanceRepository(), remoteStorageKey: remoteStorageKey, operationManager: operationManager, logger: logger, @@ -93,7 +108,7 @@ final class TokenSubscriptionFactory: OrmlTokenSubscriptionFactoryProtocol { chainAssetId: chainAssetId, accountId: accountId, chainRegistry: chainRegistry, - repository: locksRepository, + repository: createAssetLocksRepository(), operationManager: operationManager, logger: logger ) @@ -114,7 +129,7 @@ extension TokenSubscriptionFactory: NativeTokenSubscriptionFactoryProtocol { chainAssetId: chainAssetId, accountId: accountId, chainRegistry: chainRegistry, - assetRepository: assetRepository, + assetRepository: createAssetBalanceRepository(), transactionSubscription: transactionSubscription, remoteStorageKey: remoteStorageKey, operationManager: operationManager, @@ -133,7 +148,24 @@ extension TokenSubscriptionFactory: NativeTokenSubscriptionFactoryProtocol { chainAssetId: chainAssetId, accountId: accountId, chainRegistry: chainRegistry, - repository: locksRepository, + repository: createAssetLocksRepository(), + operationManager: operationManager, + logger: logger + ) + } + + func createBalanceHoldsSubscription( + remoteStorageKey: Data, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) -> StorageChildSubscribing { + HoldsSubscription( + storageCodingPath: BalancesPallet.holdsPath, + remoteStorageKey: remoteStorageKey, + chainAssetId: chainAssetId, + accountId: accountId, + chainRegistry: chainRegistry, + repository: createAssetHoldsRepository(), operationManager: operationManager, logger: logger ) diff --git a/novawallet/Common/Services/WebSocketService/StorageSubscription/HoldsSubscription.swift b/novawallet/Common/Services/WebSocketService/StorageSubscription/HoldsSubscription.swift new file mode 100644 index 0000000000..f2b87a47f2 --- /dev/null +++ b/novawallet/Common/Services/WebSocketService/StorageSubscription/HoldsSubscription.swift @@ -0,0 +1,119 @@ +import Foundation +import Operation_iOS + +class HoldsSubscription: StorageChildSubscribing { + var remoteStorageKey: Data + + let chainAssetId: ChainAssetId + let accountId: AccountId + let chainRegistry: ChainRegistryProtocol + let repository: AnyDataProviderRepository + let operationManager: OperationManagerProtocol + let storageCodingPath: StorageCodingPath + let logger: LoggerProtocol + + init( + storageCodingPath: StorageCodingPath, + remoteStorageKey: Data, + chainAssetId: ChainAssetId, + accountId: AccountId, + chainRegistry: ChainRegistryProtocol, + repository: AnyDataProviderRepository, + operationManager: OperationManagerProtocol, + logger: LoggerProtocol + ) { + self.remoteStorageKey = remoteStorageKey + self.chainAssetId = chainAssetId + self.accountId = accountId + self.chainRegistry = chainRegistry + self.repository = repository + self.operationManager = operationManager + self.storageCodingPath = storageCodingPath + self.logger = logger + } + + func processUpdate(_ data: Data?, blockHash _: Data?) { + logger.debug("Did receive holds update") + + let decodingWrapper = createDecodingOperationWrapper( + data: data, + chainAssetId: chainAssetId + ) + + let saveOperation = createSaveOperation( + dependingOn: decodingWrapper.targetOperation, + chainAssetId: chainAssetId, + accountId: accountId, + logger: logger + ) + + saveOperation.addDependency(decodingWrapper.targetOperation) + + let operations = decodingWrapper.allOperations + [saveOperation] + + operationManager.enqueue(operations: operations, in: .transient) + } + + private func createDecodingOperationWrapper( + data: Data?, + chainAssetId: ChainAssetId + ) -> CompoundOperationWrapper<[BalancesPallet.Hold]?> { + guard let data = data else { + return CompoundOperationWrapper.createWithResult(nil) + } + + guard let runtimeProvider = chainRegistry.getRuntimeProvider(for: chainAssetId.chainId) else { + logger.error("Runtime metadata unavailable for chain: \(chainAssetId.chainId)") + return CompoundOperationWrapper.createWithError( + ChainRegistryError.runtimeMetadaUnavailable + ) + } + + let codingFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() + let decodingOperation = StorageFallbackDecodingOperation<[BalancesPallet.Hold]>( + path: storageCodingPath, + data: data + ) + + decodingOperation.configurationBlock = { [weak self] in + do { + decodingOperation.codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + } catch { + self?.logger.error("Error occur while decoding data: \(error.localizedDescription)") + decodingOperation.result = .failure(error) + } + } + + decodingOperation.addDependency(codingFactoryOperation) + + return CompoundOperationWrapper( + targetOperation: decodingOperation, + dependencies: [codingFactoryOperation] + ) + } + + private func createSaveOperation( + dependingOn decodingOperation: BaseOperation<[BalancesPallet.Hold]?>, + chainAssetId: ChainAssetId, + accountId: AccountId, + logger: LoggerProtocol + ) -> BaseOperation { + let replaceOperation = repository.replaceOperation { + let remoteItems = try decodingOperation.extractNoCancellableResultData() ?? [] + + logger.debug("Saving holds: \(remoteItems)") + + return remoteItems.map { remoteItem in + AssetHold( + chainAssetId: chainAssetId, + accountId: accountId, + module: remoteItem.holdId.module, + reason: remoteItem.holdId.reason, + amount: remoteItem.amount + ) + } + } + + return replaceOperation + } +} diff --git a/novawallet/Common/Storage/EntityToModel/AssetHoldMapper.swift b/novawallet/Common/Storage/EntityToModel/AssetHoldMapper.swift new file mode 100644 index 0000000000..7a3eb89163 --- /dev/null +++ b/novawallet/Common/Storage/EntityToModel/AssetHoldMapper.swift @@ -0,0 +1,43 @@ +import Foundation +import Operation_iOS +import CoreData +import BigInt + +final class AssetHoldMapper { + var entityIdentifierFieldName: String { #keyPath(CDAssetLock.identifier) } + + typealias DataProviderModel = AssetHold + typealias CoreDataEntity = CDAssetHold +} + +extension AssetHoldMapper: CoreDataMapperProtocol { + func populate( + entity: CoreDataEntity, + from model: DataProviderModel, + using _: NSManagedObjectContext + ) throws { + entity.identifier = model.identifier + entity.chainAccountId = model.accountId.toHex() + entity.chainId = model.chainAssetId.chainId + entity.assetId = Int32(bitPattern: model.chainAssetId.assetId) + entity.module = model.module + entity.reason = model.reason + 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 chainAssetId = ChainAssetId( + chainId: entity.chainId!, + assetId: UInt32(bitPattern: entity.assetId) + ) + return AssetHold( + chainAssetId: chainAssetId, + accountId: accountId, + module: entity.module!, + reason: entity.reason!, + amount: amount + ) + } +} diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion index 3c34865ab3..98752d5115 100644 --- a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - SubstrateDataModel29.xcdatamodel + SubstrateDataModel30.xcdatamodel diff --git a/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel30.xcdatamodel/contents b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel30.xcdatamodel/contents new file mode 100644 index 0000000000..4494a01529 --- /dev/null +++ b/novawallet/Common/Storage/SubstrateDataModel.xcdatamodeld/SubstrateDataModel30.xcdatamodel/contents @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/novawallet/Common/Storage/SubstrateDataStorageFacade.swift b/novawallet/Common/Storage/SubstrateDataStorageFacade.swift index ddb9e47b80..2a1c26a431 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 = .version29 + static let modelVersion: SubstrateStorageVersion = .version30 static let deprecatedStorageDirectoryURL: URL = { let baseURL = FileManager.default.urls( diff --git a/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolsMigrateCall.swift b/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolsMigrateCall.swift new file mode 100644 index 0000000000..7faace2a08 --- /dev/null +++ b/novawallet/Common/Substrate/Calls/NominationPools/NominationPoolsMigrateCall.swift @@ -0,0 +1,21 @@ +import Foundation +import SubstrateSdk + +extension NominationPools { + struct MigrateCall: Codable { + enum CodingKeys: String, CodingKey { + case memberAccount = "member_account" + } + + let memberAccount: MultiAddress + + static var codingPath: CallCodingPath { + .init(moduleName: NominationPools.module, callName: "migrate_delegation") + } + + func runtimeCall() -> RuntimeCall { + let codingPath = Self.codingPath + return RuntimeCall(moduleName: codingPath.moduleName, callName: codingPath.callName, args: self) + } + } +} diff --git a/novawallet/Common/Substrate/Extension/AvailSignedExtension.swift b/novawallet/Common/Substrate/Extension/AvailSignedExtension.swift index 8210511e0b..ff763caec4 100644 --- a/novawallet/Common/Substrate/Extension/AvailSignedExtension.swift +++ b/novawallet/Common/Substrate/Extension/AvailSignedExtension.swift @@ -27,50 +27,19 @@ enum AvailSignedExtension { } } -extension AvailSignedExtension { - final class Factory { - private func getBaseSignedExtensions() -> [ExtrinsicSignedExtending] { - [] - } - - private func getMainSignedExtensions() -> [ExtrinsicSignedExtending] { - [ - CheckAppId(appId: 0) - ] - } - - private func getBaseCoders(for metadata: RuntimeMetadataProtocol) -> [ExtrinsicSignedExtensionCoding] { - DefaultSignedExtensionCoders.createDefaultCoders(for: metadata) - } - - private func getMainCoders(for metadata: RuntimeMetadataProtocol) -> [ExtrinsicSignedExtensionCoding] { - let extensionId = AvailSignedExtension.checkAppId - - guard let extraType = metadata.getSignedExtensionType(for: extensionId) else { - return [] - } +enum AvailSignedExtensionCoders { + static func getCoders(for metadata: RuntimeMetadataProtocol) -> [ExtrinsicSignedExtensionCoding] { + let extensionId = AvailSignedExtension.checkAppId - return [ - DefaultExtrinsicSignedExtensionCoder( - signedExtensionId: extensionId, - extraType: extraType - ) - ] + guard let extraType = metadata.getSignedExtensionType(for: extensionId) else { + return [] } - } -} - -extension AvailSignedExtension.Factory: ExtrinsicSignedExtensionFactoryProtocol { - func createExtensions() -> [ExtrinsicSignedExtending] { - getBaseSignedExtensions() + getMainSignedExtensions() - } - - func createExtensions(payingFeeIn _: AssetConversionPallet.AssetId) -> [ExtrinsicSignedExtending] { - // Avail doesn't support fee customization via signed extensions - ignore parameter - createExtensions() - } - func createCoders(for metadata: RuntimeMetadataProtocol) -> [ExtrinsicSignedExtensionCoding] { - getBaseCoders(for: metadata) + getMainCoders(for: metadata) + return [ + DefaultExtrinsicSignedExtensionCoder( + signedExtensionId: extensionId, + extraType: extraType + ) + ] } } diff --git a/novawallet/Common/Substrate/Extension/ExtrinsicSignedExtensionFacade.swift b/novawallet/Common/Substrate/Extension/ExtrinsicSignedExtensionFacade.swift index 617aedfa8e..7d5abeafc6 100644 --- a/novawallet/Common/Substrate/Extension/ExtrinsicSignedExtensionFacade.swift +++ b/novawallet/Common/Substrate/Extension/ExtrinsicSignedExtensionFacade.swift @@ -7,12 +7,7 @@ protocol ExtrinsicSignedExtensionFacadeProtocol { final class ExtrinsicSignedExtensionFacade {} extension ExtrinsicSignedExtensionFacade: ExtrinsicSignedExtensionFacadeProtocol { - func createFactory(for chainId: ChainModel.Id) -> ExtrinsicSignedExtensionFactoryProtocol { - switch chainId { - case KnowChainId.avail, KnowChainId.availTuringTestnet: - AvailSignedExtension.Factory() - default: - ExtrinsicSignedExtensionFactory() - } + func createFactory(for _: ChainModel.Id) -> ExtrinsicSignedExtensionFactoryProtocol { + ExtrinsicSignedExtensionFactory() } } diff --git a/novawallet/Common/Substrate/Extension/ExtrinsicSignedExtensionFactory.swift b/novawallet/Common/Substrate/Extension/ExtrinsicSignedExtensionFactory.swift index bf03bdcab5..eb14723c2c 100644 --- a/novawallet/Common/Substrate/Extension/ExtrinsicSignedExtensionFactory.swift +++ b/novawallet/Common/Substrate/Extension/ExtrinsicSignedExtensionFactory.swift @@ -19,7 +19,8 @@ final class ExtrinsicSignedExtensionFactory {} extension ExtrinsicSignedExtensionFactory: ExtrinsicSignedExtensionFactoryProtocol { func createExtensions() -> [ExtrinsicSignedExtending] { [ - ExtrinsicSignedExtension.ChargeAssetTxPayment() + ExtrinsicSignedExtension.ChargeAssetTxPayment(), + AvailSignedExtension.CheckAppId(appId: 0) ] } @@ -30,7 +31,10 @@ extension ExtrinsicSignedExtensionFactory: ExtrinsicSignedExtensionFactoryProtoc } func createCoders(for metadata: RuntimeMetadataProtocol) -> [ExtrinsicSignedExtensionCoding] { - DefaultSignedExtensionCoders.createDefaultCoders(for: metadata) + let baseCoders = DefaultSignedExtensionCoders.createDefaultCoders(for: metadata) + let availCoders = AvailSignedExtensionCoders.getCoders(for: metadata) + + return baseCoders + availCoders } } diff --git a/novawallet/Common/Substrate/Types/BalancesPallet/BalancePallet+Holds.swift b/novawallet/Common/Substrate/Types/BalancesPallet/BalancePallet+Holds.swift new file mode 100644 index 0000000000..bdf33ffa87 --- /dev/null +++ b/novawallet/Common/Substrate/Types/BalancesPallet/BalancePallet+Holds.swift @@ -0,0 +1,30 @@ +import Foundation +import SubstrateSdk +import BigInt + +extension BalancesPallet { + struct HoldId: Decodable { + let module: String + let reason: String + + init(from decoder: Decoder) throws { + var unkeyedContainer = try decoder.unkeyedContainer() + + module = try unkeyedContainer.decode(String.self) + + var reasonContainer = try unkeyedContainer.nestedUnkeyedContainer() + + reason = try reasonContainer.decode(String.self) + } + } + + struct Hold: Decodable { + enum CodingKeys: String, CodingKey { + case holdId = "id" + case amount + } + + let holdId: HoldId + @StringCodable var amount: BigUInt + } +} diff --git a/novawallet/Common/Substrate/Types/BalancesPallet/BalancesPallet+StoragePath.swift b/novawallet/Common/Substrate/Types/BalancesPallet/BalancesPallet+StoragePath.swift new file mode 100644 index 0000000000..5dd9f31d54 --- /dev/null +++ b/novawallet/Common/Substrate/Types/BalancesPallet/BalancesPallet+StoragePath.swift @@ -0,0 +1,8 @@ +import Foundation +import SubstrateSdk + +extension BalancesPallet { + static var holdsPath: StorageCodingPath { + StorageCodingPath(moduleName: Self.name, itemName: "Holds") + } +} diff --git a/novawallet/Common/Substrate/Types/DelegatedStaking/DelegatedStakingPallet+Path.swift b/novawallet/Common/Substrate/Types/DelegatedStaking/DelegatedStakingPallet+Path.swift new file mode 100644 index 0000000000..8ceaf057a9 --- /dev/null +++ b/novawallet/Common/Substrate/Types/DelegatedStaking/DelegatedStakingPallet+Path.swift @@ -0,0 +1,8 @@ +import Foundation +import SubstrateSdk + +extension DelegatedStakingPallet { + static var delegatorsPath: StorageCodingPath { + .init(moduleName: Self.name, itemName: "Delegators") + } +} diff --git a/novawallet/Common/Substrate/Types/DelegatedStaking/DelegatedStakingPallet.swift b/novawallet/Common/Substrate/Types/DelegatedStaking/DelegatedStakingPallet.swift new file mode 100644 index 0000000000..f37cf5cb0a --- /dev/null +++ b/novawallet/Common/Substrate/Types/DelegatedStaking/DelegatedStakingPallet.swift @@ -0,0 +1,12 @@ +import Foundation +import SubstrateSdk +import BigInt + +enum DelegatedStakingPallet { + static let name = "DelegatedStaking" + + struct Delegation: Decodable, Equatable { + @BytesCodable var agent: AccountId + @StringCodable var amount: BigUInt + } +} diff --git a/novawallet/Common/Validation/Validators/AsyncWarningConditionViolation.swift b/novawallet/Common/Validation/Validators/AsyncWarningConditionViolation.swift index d8a621b9bd..d508a00e95 100644 --- a/novawallet/Common/Validation/Validators/AsyncWarningConditionViolation.swift +++ b/novawallet/Common/Validation/Validators/AsyncWarningConditionViolation.swift @@ -14,10 +14,12 @@ final class AsyncWarningConditionViolation: DataValidating { func validate(notifying delegate: DataValidatingDelegate) -> DataValidationProblem? { preservesCondition { [weak self] result in - if result { - delegate.didCompleteAsyncHandling() - } else { - self?.onWarning(delegate) + DispatchQueue.main.async { + if result { + delegate.didCompleteAsyncHandling() + } else { + self?.onWarning(delegate) + } } } diff --git a/novawallet/Common/Validation/Validators/ErrorConditionViolation.swift b/novawallet/Common/Validation/Validators/ErrorConditionViolation.swift index 98b5f2e900..bc1c7a84d1 100644 --- a/novawallet/Common/Validation/Validators/ErrorConditionViolation.swift +++ b/novawallet/Common/Validation/Validators/ErrorConditionViolation.swift @@ -22,3 +22,47 @@ final class ErrorConditionViolation: DataValidating { return .error } } + +final class AsyncValidationOnProgress { + let willStart: (() -> Void)? + let didComplete: ((Bool) -> Void)? + + init(willStart: (() -> Void)?, didComplete: ((Bool) -> Void)?) { + self.willStart = willStart + self.didComplete = didComplete + } +} + +final class AsyncErrorConditionViolation: DataValidating { + let preservesCondition: (@escaping (Bool) -> Void) -> Void + let onError: () -> Void + let onProgress: AsyncValidationOnProgress? + + init( + onError: @escaping () -> Void, + preservesCondition: @escaping (@escaping (Bool) -> Void) -> Void, + onProgress: AsyncValidationOnProgress? = nil + ) { + self.preservesCondition = preservesCondition + self.onError = onError + self.onProgress = onProgress + } + + func validate(notifying delegate: DataValidatingDelegate) -> DataValidationProblem? { + onProgress?.willStart?() + + preservesCondition { [weak self] result in + DispatchQueue.main.async { + self?.onProgress?.didComplete?(result) + + if result { + delegate.didCompleteAsyncHandling() + } else { + self?.onError() + } + } + } + + return .asyncProcess + } +} diff --git a/novawallet/Common/View/StackTable/StackAddressCell.swift b/novawallet/Common/View/StackTable/StackAddressCell.swift index 7c267e472e..7801d03e1c 100644 --- a/novawallet/Common/View/StackTable/StackAddressCell.swift +++ b/novawallet/Common/View/StackTable/StackAddressCell.swift @@ -1,9 +1,8 @@ import Foundation import SoraUI -final class StackAddressCell: RowView> { - var titleView: LoadableIconDetailsView { rowContentView.titleView } - var indicatorView: UIImageView { rowContentView.valueView } +final class StackAddressCell: RowView { + var titleView: LoadableIconDetailsView { rowContentView } var skeletonView: SkrullableView? @@ -17,6 +16,7 @@ final class StackAddressCell: RowView [Skeletonable] { diff --git a/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift b/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift index 33cbb6cdea..b8c9ff01c6 100644 --- a/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift +++ b/novawallet/Common/ViewController/ModalPicker/ModalInfoFactory.swift @@ -1,6 +1,7 @@ import UIKit import SoraFoundation import SoraUI +import BigInt struct ModalInfoFactory { static let rowHeight: CGFloat = 50.0 @@ -167,7 +168,8 @@ struct ModalInfoFactory { let reserved: LocksSortingViewModel = createReservedViewModel( balanceContext: balanceContext, amountFormatter: amountFormatter, - priceFormatter: priceFormatter + priceFormatter: priceFormatter, + precision: precision ) let externalBalances = createExternalBalancesViewModel( @@ -192,7 +194,15 @@ struct ModalInfoFactory { precision: precision ) - return (balanceLockKnownModels + balanceLockUnknownModels + externalBalances + reserved) + let balanceHolds = createHoldsViewModel( + from: balanceContext.balanceHolds, + balanceContext: balanceContext, + amountFormatter: amountFormatter, + priceFormatter: priceFormatter, + precision: precision + ) + + return (balanceLockKnownModels + balanceLockUnknownModels + balanceHolds + externalBalances + reserved) .sorted { viewModel1, viewModel2 in viewModel1.value >= viewModel2.value }.map(\.viewModel) @@ -239,6 +249,31 @@ struct ModalInfoFactory { } } + private static func createHoldsViewModel( + from holds: [AssetHold], + balanceContext: BalanceContext, + amountFormatter: LocalizableResource, + priceFormatter: LocalizableResource, + precision: Int16 + ) -> LocksSortingViewModel { + holds.map { hold in + let holdAmount = Decimal.fromSubstrateAmount( + hold.amount, + precision: precision + ) ?? 0.0 + + return createLockFieldViewModel( + amount: holdAmount, + price: balanceContext.price, + localizedTitle: LocalizableResource { locale in + hold.displayTitle(for: locale) + }, + amountFormatter: amountFormatter, + priceFormatter: priceFormatter + ) + }.flatMap { $0 } + } + private static func createExternalBalancesViewModel( balanceContext: BalanceContext, amountFormatter: LocalizableResource, @@ -267,14 +302,27 @@ struct ModalInfoFactory { private static func createReservedViewModel( balanceContext: BalanceContext, amountFormatter: LocalizableResource, - priceFormatter: LocalizableResource + priceFormatter: LocalizableResource, + precision: Int16 ) -> LocksSortingViewModel { let title = LocalizableResource { locale in R.string.localizable.walletBalanceReserved(preferredLanguages: locale.rLanguages) } + let totalHolds = balanceContext.balanceHolds.reduce(BigUInt(0)) { $0 + $1.amount } + let totalHoldsDecimal = Decimal.fromSubstrateAmount( + totalHolds, + precision: precision + ) ?? 0.0 + + let totalAmount = max(balanceContext.reserved - totalHoldsDecimal, 0) + + guard totalAmount > 0 else { + return [] + } + return createLockFieldViewModel( - amount: balanceContext.reserved, + amount: totalAmount, price: balanceContext.price, localizedTitle: title, amountFormatter: amountFormatter, diff --git a/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift b/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift index 4ca493776d..05b2099b9a 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsInteractor.swift @@ -17,6 +17,7 @@ final class AssetDetailsInteractor: AnyCancellableCleaning { private var priceSubscription: StreamableProvider? private var assetBalanceSubscription: StreamableProvider? private var externalBalanceSubscription: StreamableProvider? + private var assetHoldsSubscription: StreamableProvider? private var swapsCall = CancellableCallStore() private var accountId: AccountId? { @@ -125,18 +126,27 @@ extension AssetDetailsInteractor: AssetDetailsInteractorInputProtocol { guard let accountId = accountId else { return } + subscribePrice() + assetBalanceSubscription = subscribeToAssetBalanceProvider( for: accountId, chainId: chainAsset.chain.chainId, assetId: chainAsset.asset.assetId ) + assetLocksSubscription = subscribeToLocksProvider( for: accountId, chainId: chainAsset.chain.chainId, assetId: chainAsset.asset.assetId ) + assetHoldsSubscription = subscribeToHoldsProvider( + for: accountId, + chainId: chainAsset.chain.chainId, + assetId: chainAsset.asset.assetId + ) + if chainAsset.chain.chainAssetIdsWithExternalBalances().contains(chainAsset.chainAssetId) { externalBalanceSubscription = subscribeToExternalAssetBalancesProvider( for: accountId, @@ -197,6 +207,26 @@ extension AssetDetailsInteractor: WalletLocalStorageSubscriber, WalletLocalSubsc presenter?.didReceive(lockChanges: changes) } } + + func handleAccountHolds( + result: Result<[DataProviderChange], Error>, + accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) { + guard chainId == chainAsset.chain.chainId, + assetId == chainAsset.asset.assetId, + accountId == self.accountId else { + return + } + + switch result { + case let .failure(error): + presenter?.didReceive(error: .holds(error)) + case let .success(changes): + presenter?.didReceive(holdsChanges: changes) + } + } } extension AssetDetailsInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler { diff --git a/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift b/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift index 196ce77269..540b64d960 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsPresenter.swift @@ -15,6 +15,7 @@ final class AssetDetailsPresenter: PurchaseFlowManaging { private var priceData: PriceData? private var balance: AssetBalance? private var locks: [AssetLock] = [] + private var holds: [AssetHold] = [] private var externalAssetBalances: [ExternalAssetBalance] = [] private var purchaseActions: [PurchaseAction] = [] private var availableOperations: AssetDetailsOperation = [] @@ -185,7 +186,8 @@ extension AssetDetailsPresenter: AssetDetailsPresenterProtocol { price: priceData.map { Decimal(string: $0.price) ?? 0 } ?? 0, priceChange: priceData?.dayChange ?? 0, priceId: priceData?.currencyId, - balanceLocks: locks + balanceLocks: locks, + balanceHolds: holds ) let model = AssetDetailsLocksViewModel( balanceContext: balanceContext, @@ -212,6 +214,11 @@ extension AssetDetailsPresenter: AssetDetailsInteractorOutputProtocol { updateView() } + func didReceive(holdsChanges: [DataProviderChange]) { + holds = holds.applying(changes: holdsChanges) + updateView() + } + func didReceive(price: PriceData?) { priceData = price updateView() diff --git a/novawallet/Modules/AssetDetails/AssetDetailsProtocols.swift b/novawallet/Modules/AssetDetails/AssetDetailsProtocols.swift index adf56cfed7..86060fb37d 100644 --- a/novawallet/Modules/AssetDetails/AssetDetailsProtocols.swift +++ b/novawallet/Modules/AssetDetails/AssetDetailsProtocols.swift @@ -24,6 +24,7 @@ protocol AssetDetailsInteractorInputProtocol: AnyObject { protocol AssetDetailsInteractorOutputProtocol: AnyObject { func didReceive(balance: AssetBalance?) func didReceive(lockChanges: [DataProviderChange]) + func didReceive(holdsChanges: [DataProviderChange]) func didReceive(externalBalanceChanges: [DataProviderChange]) func didReceive(price: PriceData?) func didReceive(error: AssetDetailsError) @@ -50,4 +51,5 @@ enum AssetDetailsError: Error { case locks(Error) case externalBalances(Error) case swaps(Error) + case holds(Error) } diff --git a/novawallet/Modules/AssetDetails/Model/BalanceContext.swift b/novawallet/Modules/AssetDetails/Model/BalanceContext.swift index a05fc632cf..e77b3bd90a 100644 --- a/novawallet/Modules/AssetDetails/Model/BalanceContext.swift +++ b/novawallet/Modules/AssetDetails/Model/BalanceContext.swift @@ -9,6 +9,7 @@ struct BalanceContext { let priceChange: Decimal let priceId: Int? let balanceLocks: [AssetLock] + let balanceHolds: [AssetHold] } extension BalanceContext { diff --git a/novawallet/Modules/AssetList/AssetListInteractor.swift b/novawallet/Modules/AssetList/AssetListInteractor.swift index 05e2958eb3..6aec17c0ee 100644 --- a/novawallet/Modules/AssetList/AssetListInteractor.swift +++ b/novawallet/Modules/AssetList/AssetListInteractor.swift @@ -30,6 +30,9 @@ final class AssetListInteractor: AssetListBaseInteractor { private var assetLocksSubscriptions: [AccountId: StreamableProvider] = [:] private var locks: [ChainAssetId: [AssetLock]] = [:] + private var assetHoldsSubscriptions: [AccountId: StreamableProvider] = [:] + private var holds: [ChainAssetId: [AssetHold]] = [:] + init( selectedWalletSettings: SelectedWalletSettings, chainRegistry: ChainRegistryProtocol, @@ -65,6 +68,7 @@ final class AssetListInteractor: AssetListBaseInteractor { override func resetWallet() { clearNftSubscription() clearLocksSubscription() + clearHoldsSubscription() providerWalletInfo() provideWalletConnectSessionsCount() @@ -80,6 +84,7 @@ final class AssetListInteractor: AssetListBaseInteractor { setupNftSubscription(from: Array(availableChains.values)) updateLocksSubscription(from: enabledChainChanges) + updateHoldsSubscription(from: enabledChainChanges) } private func providePolkadotStakingPromotion() { @@ -92,6 +97,12 @@ final class AssetListInteractor: AssetListBaseInteractor { locks = [:] } + private func clearHoldsSubscription() { + assetHoldsSubscriptions.values.forEach { $0.removeObserver(self) } + assetHoldsSubscriptions = [:] + holds = [:] + } + private func providerWalletInfo() { guard let selectedMetaAccount = selectedWalletSettings.value else { return @@ -127,6 +138,7 @@ final class AssetListInteractor: AssetListBaseInteractor { setupNftSubscription(from: Array(availableChains.values)) updateLocksSubscription(from: enabledChainChanges) + updateHoldsSubscription(from: enabledChainChanges) } private func setupNftSubscription(from allChains: [ChainModel]) { @@ -188,6 +200,19 @@ final class AssetListInteractor: AssetListBaseInteractor { } } + private func updateHoldsSubscription(from changes: [DataProviderChange]) { + guard let selectedMetaAccount = selectedWalletSettings.value else { + return + } + + assetHoldsSubscriptions = changes.reduce( + intitial: assetHoldsSubscriptions, + selectedMetaAccount: selectedMetaAccount + ) { [weak self] in + self?.subscribeToAllHoldsProvider(for: $0) + } + } + override func handleAccountLocks(result: Result<[DataProviderChange], Error>, accountId: AccountId) { switch result { case let .success(changes): @@ -197,6 +222,15 @@ final class AssetListInteractor: AssetListBaseInteractor { } } + override func handleAccountHolds(result: Result<[DataProviderChange], Error>, accountId: AccountId) { + switch result { + case let .success(changes): + handleAccountHoldsChanges(changes, accountId: accountId) + case let .failure(error): + modelBuilder?.applyHolds(.failure(error)) + } + } + override func handlePriceChanges(_ result: Result<[ChainAssetId: DataProviderChange], Error>) { super.handlePriceChanges(result) @@ -303,6 +337,43 @@ extension AssetListInteractor { modelBuilder?.applyLocks(.success(Array(locks.values.flatMap { $0 }))) } + + private func handleAccountHoldsChanges( + _ changes: [DataProviderChange], + accountId: AccountId + ) { + holds = changes.reduce( + into: holds + ) { accum, change in + switch change { + case let .insert(hold), let .update(hold): + let groupIdentifier = AssetBalance.createIdentifier( + for: hold.chainAssetId, + accountId: hold.accountId + ) + guard + let assetBalanceId = assetBalanceIdMapping[groupIdentifier], + assetBalanceId.accountId == accountId else { + return + } + + let chainAssetId = ChainAssetId( + chainId: assetBalanceId.chainId, + assetId: assetBalanceId.assetId + ) + + var items = accum[chainAssetId] ?? [] + items.addOrReplaceSingle(hold) + accum[chainAssetId] = items + case let .delete(deletedIdentifier): + for chainHolds in accum { + accum[chainHolds.key] = chainHolds.value.filter { $0.identifier != deletedIdentifier } + } + } + } + + modelBuilder?.applyHolds(.success(Array(holds.values.flatMap { $0 }))) + } } extension AssetListInteractor: EventVisitorProtocol { diff --git a/novawallet/Modules/AssetList/AssetListPresenter.swift b/novawallet/Modules/AssetList/AssetListPresenter.swift index d311545f68..3ec872dca6 100644 --- a/novawallet/Modules/AssetList/AssetListPresenter.swift +++ b/novawallet/Modules/AssetList/AssetListPresenter.swift @@ -412,6 +412,7 @@ extension AssetListPresenter: AssetListPresenterProtocol { let priceResult = model.priceResult, let prices = try? priceResult.get(), let locks = try? model.locksResult?.get(), + let holds = try? model.holdsResult?.get(), let externalBalances = try? model.externalBalanceResult?.get() else { return } @@ -421,6 +422,7 @@ extension AssetListPresenter: AssetListPresenterProtocol { balances: model.balances.values.compactMap { try? $0.get() }, chains: model.allChains, locks: locks, + holds: holds, externalBalances: externalBalances ) diff --git a/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift b/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift index 8c330e2cff..8f49ccc6ba 100644 --- a/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift +++ b/novawallet/Modules/AssetList/Base/AssetListBaseInteractor.swift @@ -369,6 +369,8 @@ class AssetListBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscrip } func handleAccountLocks(result _: Result<[DataProviderChange], Error>, accountId _: AccountId) {} + + func handleAccountHolds(result _: Result<[DataProviderChange], Error>, accountId _: AccountId) {} } extension AssetListBaseInteractor { diff --git a/novawallet/Modules/AssetList/Models/AssetListBuilder.swift b/novawallet/Modules/AssetList/Models/AssetListBuilder.swift index 46bcc0cb30..4ec13d53dd 100644 --- a/novawallet/Modules/AssetList/Models/AssetListBuilder.swift +++ b/novawallet/Modules/AssetList/Models/AssetListBuilder.swift @@ -6,6 +6,7 @@ final class AssetListBuilder: AssetListBaseBuilder { private(set) var nftList: ListDifferenceCalculator private(set) var locksResult: Result<[AssetLock], Error>? + private(set) var holdsResult: Result<[AssetHold], Error>? private var currentModel: AssetListBuilderResult.Model = .init() @@ -34,7 +35,8 @@ final class AssetListBuilder: AssetListBaseBuilder { balances: balances, externalBalanceResult: externalBalancesResult, nfts: nftList.allItems, - locksResult: locksResult + locksResult: locksResult, + holdsResult: holdsResult ) currentModel = model @@ -98,4 +100,12 @@ extension AssetListBuilder { self?.scheduleRebuildModel() } } + + func applyHolds(_ result: Result<[AssetHold], Error>) { + workingQueue.async { [weak self] in + self?.holdsResult = result + + self?.scheduleRebuildModel() + } + } } diff --git a/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift b/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift index 6d9c62a161..3964603b6e 100644 --- a/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift +++ b/novawallet/Modules/AssetList/Models/AssetListBuilderResult.swift @@ -12,6 +12,7 @@ struct AssetListBuilderResult { let externalBalanceResult: Result<[ChainAssetId: [ExternalAssetBalance]], Error>? let nfts: [NftModel] let locksResult: Result<[AssetLock], Error>? + let holdsResult: Result<[AssetHold], Error>? init( groups: [AssetListGroupModel] = [], @@ -22,7 +23,8 @@ struct AssetListBuilderResult { balances: [ChainAssetId: Result] = [:], externalBalanceResult: Result<[ChainAssetId: [ExternalAssetBalance]], Error>? = nil, nfts: [NftModel] = [], - locksResult: Result<[AssetLock], Error>? = nil + locksResult: Result<[AssetLock], Error>? = nil, + holdsResult: Result<[AssetHold], Error>? = nil ) { self.groups = groups self.groupLists = groupLists @@ -33,6 +35,7 @@ struct AssetListBuilderResult { self.externalBalanceResult = externalBalanceResult self.nfts = nfts self.locksResult = locksResult + self.holdsResult = holdsResult } func replacing(nfts: [NftModel]) -> Model { diff --git a/novawallet/Modules/DApp/DAppSearch/DAppSearchPresenter.swift b/novawallet/Modules/DApp/DAppSearch/DAppSearchPresenter.swift index e1d45f10ec..18ed8d76dd 100644 --- a/novawallet/Modules/DApp/DAppSearch/DAppSearchPresenter.swift +++ b/novawallet/Modules/DApp/DAppSearch/DAppSearchPresenter.swift @@ -1,10 +1,12 @@ import Foundation +import SoraFoundation import Operation_iOS final class DAppSearchPresenter { weak var view: DAppSearchViewProtocol? let wireframe: DAppSearchWireframeProtocol let interactor: DAppSearchInteractorInputProtocol + let localizationManager: LocalizationManagerProtocol private var dAppList: DAppList? private var favorites: [String: DAppFavorite]? @@ -14,6 +16,7 @@ final class DAppSearchPresenter { weak var delegate: DAppSearchDelegate? let viewModelFactory: DAppListViewModelFactoryProtocol + let applicationConfig: ApplicationConfigProtocol let logger: LoggerProtocol? @@ -23,6 +26,8 @@ final class DAppSearchPresenter { viewModelFactory: DAppListViewModelFactoryProtocol, initialQuery: String?, delegate: DAppSearchDelegate, + applicationConfig: ApplicationConfigProtocol, + localizationManager: LocalizationManagerProtocol, logger: LoggerProtocol? = nil ) { self.interactor = interactor @@ -30,6 +35,8 @@ final class DAppSearchPresenter { self.viewModelFactory = viewModelFactory query = initialQuery self.delegate = delegate + self.applicationConfig = applicationConfig + self.localizationManager = localizationManager self.logger = logger } @@ -80,8 +87,17 @@ extension DAppSearchPresenter: DAppSearchPresenterProtocol { } func selectSearchQuery() { - delegate?.didCompleteDAppSearchResult(.query(string: query ?? "")) - wireframe.close(from: view) + wireframe.showUnknownDappWarning( + from: view, + email: applicationConfig.supportEmail, + locale: localizationManager.selectedLocale, + handler: { [weak self] in + self?.delegate?.didCompleteDAppSearchResult( + .query(string: self?.query ?? "") + ) + self?.wireframe.close(from: self?.view) + } + ) } func cancel() { diff --git a/novawallet/Modules/DApp/DAppSearch/DAppSearchProtocols.swift b/novawallet/Modules/DApp/DAppSearch/DAppSearchProtocols.swift index 5716231d9d..45e7a077e9 100644 --- a/novawallet/Modules/DApp/DAppSearch/DAppSearchProtocols.swift +++ b/novawallet/Modules/DApp/DAppSearch/DAppSearchProtocols.swift @@ -22,7 +22,7 @@ protocol DAppSearchInteractorOutputProtocol: AnyObject { func didReceiveFavorite(changes: [DataProviderChange]) } -protocol DAppSearchWireframeProtocol: AnyObject { +protocol DAppSearchWireframeProtocol: DAppAlertPresentable { func close(from view: DAppSearchViewProtocol?) } diff --git a/novawallet/Modules/DApp/DAppSearch/DAppSearchViewFactory.swift b/novawallet/Modules/DApp/DAppSearch/DAppSearchViewFactory.swift index b9b46e3007..e896477c3e 100644 --- a/novawallet/Modules/DApp/DAppSearch/DAppSearchViewFactory.swift +++ b/novawallet/Modules/DApp/DAppSearch/DAppSearchViewFactory.swift @@ -25,6 +25,8 @@ struct DAppSearchViewFactory { viewModelFactory: DAppListViewModelFactory(), initialQuery: initialQuery, delegate: delegate, + applicationConfig: ApplicationConfig.shared, + localizationManager: LocalizationManager.shared, logger: Logger.shared ) diff --git a/novawallet/Modules/DApp/DAppSearch/DAppSearchWireframe.swift b/novawallet/Modules/DApp/DAppSearch/DAppSearchWireframe.swift index 716f4074e5..a1c866b986 100644 --- a/novawallet/Modules/DApp/DAppSearch/DAppSearchWireframe.swift +++ b/novawallet/Modules/DApp/DAppSearch/DAppSearchWireframe.swift @@ -2,6 +2,6 @@ import Foundation final class DAppSearchWireframe: DAppSearchWireframeProtocol { func close(from view: DAppSearchViewProtocol?) { - view?.controller.presentingViewController?.dismiss(animated: true, completion: nil) + view?.controller.presentingViewController?.dismiss(animated: true) } } diff --git a/novawallet/Modules/DApp/Protocols/DAppAlertPresentable.swift b/novawallet/Modules/DApp/Protocols/DAppAlertPresentable.swift index 2289f0a2be..cc1e58f074 100644 --- a/novawallet/Modules/DApp/Protocols/DAppAlertPresentable.swift +++ b/novawallet/Modules/DApp/Protocols/DAppAlertPresentable.swift @@ -15,6 +15,13 @@ protocol DAppAlertPresentable: AlertPresentable { locale: Locale, handler: @escaping () -> Void ) + + func showUnknownDappWarning( + from view: ControllerBackedProtocol?, + email: String, + locale: Locale, + handler: @escaping () -> Void + ) } extension DAppAlertPresentable { @@ -48,6 +55,34 @@ extension DAppAlertPresentable { showRemoval(from: view, title: title, message: message, locale: locale, handler: handler) } + func showUnknownDappWarning( + from view: ControllerBackedProtocol?, + email: String, + locale: Locale, + handler: @escaping () -> Void + ) { + let action = AlertPresentableAction( + title: R.string.localizable.dappUnknownWarningOpen(preferredLanguages: locale.rLanguages), + style: .destructive, + handler: handler + ) + let viewModel = AlertPresentableViewModel( + title: R.string.localizable.dappUnknownWarningTitle(preferredLanguages: locale.rLanguages), + message: R.string.localizable.dappUnknownWarningMessage( + email, + preferredLanguages: locale.rLanguages + ), + actions: [action], + closeAction: R.string.localizable.commonClose(preferredLanguages: locale.rLanguages) + ) + + present( + viewModel: viewModel, + style: .alert, + from: view + ) + } + private func showRemoval( from view: ControllerBackedProtocol?, title: String, diff --git a/novawallet/Modules/Locks/LocksPresenter.swift b/novawallet/Modules/Locks/LocksPresenter.swift index c7291b41cc..ba2c5443dc 100644 --- a/novawallet/Modules/Locks/LocksPresenter.swift +++ b/novawallet/Modules/Locks/LocksPresenter.swift @@ -93,16 +93,9 @@ final class LocksPresenter { ) } - let reservedCells: [LocksViewSectionModel.CellViewModel] = input.balances.compactMap { - createCell( - amountInPlank: $0.reservedInPlank, - chainAssetId: $0.chainAssetId, - title: R.string.localizable.walletBalanceReserved( - preferredLanguages: selectedLocale.rLanguages - ), - identifier: $0.identifier - ) - } + let holdsCells = createHoldReserves() + + let reservedCells = createNonHoldReserves() let groupedExternalBalances = input.externalBalances .values.flatMap { $0.filter { $0.amount > 0 } } @@ -120,7 +113,37 @@ final class LocksPresenter { ) } - return locksCells + reservedCells + externalBalanceCells + return locksCells + holdsCells + reservedCells + externalBalanceCells + } + + private func createNonHoldReserves() -> [LocksViewSectionModel.CellViewModel] { + input.balances.compactMap { balance in + let totalHolds = input.holds + .filter { $0.chainAssetId == balance.chainAssetId } + .reduce(BigUInt(0)) { $0 + $1.amount } + + let reservesNotInHolds = balance.reservedInPlank.subtractOrZero(totalHolds) + + return createCell( + amountInPlank: reservesNotInHolds, + chainAssetId: balance.chainAssetId, + title: R.string.localizable.walletBalanceReserved( + preferredLanguages: selectedLocale.rLanguages + ), + identifier: balance.identifier + ) + } + } + + private func createHoldReserves() -> [LocksViewSectionModel.CellViewModel] { + input.holds.compactMap { hold in + createCell( + amountInPlank: hold.amount, + chainAssetId: hold.chainAssetId, + title: hold.displayTitle(for: selectedLocale), + identifier: hold.identifier + ) + } } private func createCell( diff --git a/novawallet/Modules/Locks/LocksViewInput.swift b/novawallet/Modules/Locks/LocksViewInput.swift index 66507af3ca..fbfcd66a73 100644 --- a/novawallet/Modules/Locks/LocksViewInput.swift +++ b/novawallet/Modules/Locks/LocksViewInput.swift @@ -3,5 +3,6 @@ struct LocksViewInput { let balances: [AssetBalance] let chains: [ChainModel.Id: ChainModel] let locks: [AssetLock] + let holds: [AssetHold] let externalBalances: [ChainAssetId: [ExternalAssetBalance]] } diff --git a/novawallet/Modules/Locks/Model/AssetHold+Display.swift b/novawallet/Modules/Locks/Model/AssetHold+Display.swift new file mode 100644 index 0000000000..6c9795957c --- /dev/null +++ b/novawallet/Modules/Locks/Model/AssetHold+Display.swift @@ -0,0 +1,12 @@ +import Foundation + +extension AssetHold { + func displayTitle(for locale: Locale) -> String { + switch (module, reason) { + case ("DelegatedStaking", "StakingDelegation"): + return R.string.localizable.stakingTypeNominationPool(preferredLanguages: locale.rLanguages) + default: + return "\(module): \(reason)" + } + } +} diff --git a/novawallet/Modules/Staking/Model/StakingConstants.swift b/novawallet/Modules/Staking/Model/StakingConstants.swift index f41292e234..08e79903f1 100644 --- a/novawallet/Modules/Staking/Model/StakingConstants.swift +++ b/novawallet/Modules/Staking/Model/StakingConstants.swift @@ -9,6 +9,7 @@ struct StakingConstants { KnowChainId.polkadot: 54, KnowChainId.kusama: 160, KnowChainId.alephZero: 74, - KnowChainId.vara: 65 + KnowChainId.vara: 65, + KnowChainId.avail: 3 ] } diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseInteractor.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseInteractor.swift index d17c2f1455..50e30c2560 100644 --- a/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseInteractor.swift +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseInteractor.swift @@ -1,9 +1,10 @@ import UIKit import Operation_iOS import BigInt +import SubstrateSdk class NominationPoolBondMoreBaseInteractor: AnyProviderAutoCleaning, AnyCancellableCleaning, - NominationPoolsDataProviding { + NominationPoolsDataProviding, NominationPoolStakingMigrating { weak var basePresenter: NominationPoolBondMoreBaseInteractorOutputProtocol? let chainAsset: ChainAsset let selectedAccount: MetaChainAccountResponse @@ -15,7 +16,6 @@ class NominationPoolBondMoreBaseInteractor: AnyProviderAutoCleaning, AnyCancella let npoolsOperationFactory: NominationPoolsOperationFactoryProtocol let runtimeService: RuntimeCodingServiceProtocol let npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol - let stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol let assetStorageInfoFactory: AssetStorageInfoOperationFactoryProtocol private var operationQueue: OperationQueue @@ -25,18 +25,20 @@ class NominationPoolBondMoreBaseInteractor: AnyProviderAutoCleaning, AnyCancella private var balanceProvider: StreamableProvider? private var poolMemberProvider: AnyDataProvider? private var bondedPoolProvider: AnyDataProvider? + private var delegatedStakingProvider: AnyDataProvider? private var claimableRewardProvider: AnySingleValueProvider? private var rewardPoolProvider: AnyDataProvider? + private var cancellableNeedsMigration = CancellableCallStore() 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 accountId: AccountId { selectedAccount.chainAccount.accountId } var chainId: ChainModel.Id { chainAsset.chain.chainId } init( @@ -50,7 +52,6 @@ class NominationPoolBondMoreBaseInteractor: AnyProviderAutoCleaning, AnyCancella extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, npoolsOperationFactory: NominationPoolsOperationFactoryProtocol, npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, - stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol, assetStorageInfoFactory: AssetStorageInfoOperationFactoryProtocol, operationQueue: OperationQueue, currencyManager: CurrencyManagerProtocol @@ -65,7 +66,6 @@ class NominationPoolBondMoreBaseInteractor: AnyProviderAutoCleaning, AnyCancella self.npoolsOperationFactory = npoolsOperationFactory self.runtimeService = runtimeService self.npoolsLocalSubscriptionFactory = npoolsLocalSubscriptionFactory - self.stakingLocalSubscriptionFactory = stakingLocalSubscriptionFactory self.assetStorageInfoFactory = assetStorageInfoFactory extrinsicService = extrinsicServiceFactory.createService( @@ -80,7 +80,7 @@ class NominationPoolBondMoreBaseInteractor: AnyProviderAutoCleaning, AnyCancella clear(streamableProvider: &balanceProvider) balanceProvider = subscribeToAssetBalanceProvider( - for: selectedAccount.chainAccount.accountId, + for: accountId, chainId: chainAsset.chain.chainId, assetId: chainAsset.asset.assetId ) @@ -96,10 +96,29 @@ class NominationPoolBondMoreBaseInteractor: AnyProviderAutoCleaning, AnyCancella } } - func createExtrinsicClosure(for points: BigUInt) -> ExtrinsicBuilderClosure { + func subscribeDelegatedStaking() { + clear(dataProvider: &delegatedStakingProvider) + + delegatedStakingProvider = subscribeDelegatedStaking( + for: accountId, + chainId: chainId + ) + } + + func createExtrinsicClosure( + for points: BigUInt, + accountId: AccountId, + needsMigration: Bool + ) -> ExtrinsicBuilderClosure { { builder in - let call = NominationPools.BondExtraCall(extra: .freeBalance(points)) - return try builder.adding(call: call.runtimeCall()) + let currentBuilder = try NominationPools.migrateIfNeeded( + needsMigration, + accountId: accountId, + builder: builder + ) + + let bondExtraCall = NominationPools.BondExtraCall(extra: .freeBalance(points)) + return try currentBuilder.adding(call: bondExtraCall.runtimeCall()) } } @@ -135,6 +154,24 @@ class NominationPoolBondMoreBaseInteractor: AnyProviderAutoCleaning, AnyCancella poolMemberProvider = subscribePoolMember(for: accountId, chainId: chainId) } + private func provideNeedsMigration(for delegation: DelegatedStakingPallet.Delegation?) { + cancellableNeedsMigration.cancel() + + needsPoolStakingMigration( + for: delegation, + runtimeProvider: runtimeService, + cancellableStore: cancellableNeedsMigration, + operationQueue: operationQueue + ) { [weak self] result in + switch result { + case let .success(needsMigration): + self?.basePresenter?.didReceive(needsMigration: needsMigration) + case let .failure(error): + self?.basePresenter?.didReceive(error: .subscription(error, "Unexpected delegated staking error")) + } + } + } + func provideAssetExistence() { let assetInfoWrapper = assetStorageInfoFactory.createStorageInfoWrapper( from: chainAsset.asset, @@ -193,15 +230,20 @@ extension NominationPoolBondMoreBaseInteractor: NominationPoolBondMoreBaseIntera subscribeAccountBalance() subscribePoolMember() subscribePrice() + subscribeDelegatedStaking() provideAssetExistence() } - func estimateFee(for amount: BigUInt) { - let reuseIdentifier = String(amount) + func estimateFee(for amount: BigUInt, needsMigration: Bool) { + let reuseIdentifier = String(amount) + "-" + "\(needsMigration)" feeProxy.estimateFee( using: extrinsicService, reuseIdentifier: reuseIdentifier, - setupBy: createExtrinsicClosure(for: amount) + setupBy: createExtrinsicClosure( + for: amount, + accountId: accountId, + needsMigration: needsMigration + ) ) } @@ -209,6 +251,7 @@ extension NominationPoolBondMoreBaseInteractor: NominationPoolBondMoreBaseIntera subscribeAccountBalance() subscribePoolMember() subscribePrice() + subscribeDelegatedStaking() } func retryClaimableRewards() { @@ -279,6 +322,19 @@ extension NominationPoolBondMoreBaseInteractor: SelectedCurrencyDepending { } extension NominationPoolBondMoreBaseInteractor: NPoolsLocalStorageSubscriber, NPoolsLocalSubscriptionHandler { + func handleDelegatedStaking( + result: Result, + accountId _: AccountId, + chainId _: ChainModel.Id + ) { + switch result { + case let .success(delegation): + provideNeedsMigration(for: delegation) + case let .failure(error): + basePresenter?.didReceive(error: .subscription(error, "Delegated staking failed")) + } + } + func handlePoolMember( result: Result, accountId _: AccountId, chainId _: ChainModel.Id diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBasePresenter.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBasePresenter.swift index f6d9ac6685..28d7f68a8e 100644 --- a/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBasePresenter.swift +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBasePresenter.swift @@ -12,6 +12,7 @@ class NominationPoolBondMoreBasePresenter: NominationPoolBondMoreBaseInteractorO let logger: LoggerProtocol let balanceViewModelFactory: BalanceViewModelFactoryProtocol let dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol + let stakingActivity: StakingActivityForValidating var assetBalance: AssetBalance? var poolMember: NominationPools.PoolMember? @@ -20,6 +21,7 @@ class NominationPoolBondMoreBasePresenter: NominationPoolBondMoreBaseInteractorO var fee: ExtrinsicFeeProtocol? var claimableRewards: BigUInt? var assetBalanceExistance: AssetBalanceExistence? + var needsMigration: Bool? init( interactor: NominationPoolBondMoreBaseInteractorInputProtocol, @@ -28,6 +30,7 @@ class NominationPoolBondMoreBasePresenter: NominationPoolBondMoreBaseInteractorO hintsViewModelFactory: NominationPoolsBondMoreHintsFactoryProtocol, balanceViewModelFactory: BalanceViewModelFactoryProtocol, dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol, + stakingActivity: StakingActivityForValidating, localizationManager: LocalizationManagerProtocol, logger: LoggerProtocol ) { @@ -38,6 +41,7 @@ class NominationPoolBondMoreBasePresenter: NominationPoolBondMoreBaseInteractorO self.hintsViewModelFactory = hintsViewModelFactory self.balanceViewModelFactory = balanceViewModelFactory self.dataValidatorFactory = dataValidatorFactory + self.stakingActivity = stakingActivity self.localizationManager = localizationManager } @@ -67,13 +71,17 @@ class NominationPoolBondMoreBasePresenter: NominationPoolBondMoreBaseInteractorO } func refreshFee() { + guard let needsMigration else { + return + } + let inputAmount = getInputAmountInPlank() ?? 0 fee = nil provideFee() - baseInteractor.estimateFee(for: inputAmount) + baseInteractor.estimateFee(for: inputAmount, needsMigration: needsMigration) } func getValidations() -> [DataValidating] { @@ -81,6 +89,19 @@ class NominationPoolBondMoreBasePresenter: NominationPoolBondMoreBaseInteractorO dataValidatorFactory.has(fee: fee, locale: selectedLocale) { [weak self] in self?.refreshFee() }, + dataValidatorFactory.canMigrateIfNeeded( + needsMigration: needsMigration, + stakingActivity: stakingActivity, + onProgress: .init( + willStart: { [weak self] in + self?.baseView?.didStartLoading() + }, + didComplete: { [weak self] _ in + self?.baseView?.didStopLoading() + } + ), + locale: selectedLocale + ), dataValidatorFactory.canSpendAmountInPlank( balance: assetBalance?.transferable, spendingAmount: getInputAmount(), @@ -145,7 +166,7 @@ class NominationPoolBondMoreBasePresenter: NominationPoolBondMoreBaseInteractorO } func didReceive(error: NominationPoolBondMoreError) { - logger.error(error.localizedDescription) + logger.error("Error: \(error)") switch error { case .fetchFeeFailed: @@ -166,6 +187,14 @@ class NominationPoolBondMoreBasePresenter: NominationPoolBondMoreBaseInteractorO } } } + + func didReceive(needsMigration: Bool) { + logger.debug("Needs migration: \(needsMigration)") + + self.needsMigration = needsMigration + + refreshFee() + } } extension NominationPoolBondMoreBasePresenter: Localizable { diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseProtocols.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseProtocols.swift index b26074255b..83bf22b6a4 100644 --- a/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseProtocols.swift +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Base/NominationPoolBondMoreBaseProtocols.swift @@ -1,12 +1,12 @@ import BigInt -protocol NominationPoolBondMoreBaseViewProtocol: ControllerBackedProtocol { +protocol NominationPoolBondMoreBaseViewProtocol: SCLoadableControllerProtocol { func didReceiveHints(viewModel: [String]) } protocol NominationPoolBondMoreBaseInteractorInputProtocol: AnyObject { func setup() - func estimateFee(for amount: BigUInt) + func estimateFee(for amount: BigUInt, needsMigration: Bool) func retrySubscriptions() func retryClaimableRewards() func retryAssetExistance() @@ -21,6 +21,7 @@ protocol NominationPoolBondMoreBaseInteractorOutputProtocol: AnyObject { func didReceive(bondedPool: NominationPools.BondedPool?) func didReceive(claimableRewards: BigUInt?) func didReceive(assetBalanceExistance: AssetBalanceExistence?) + func didReceive(needsMigration: Bool) } protocol NominationPoolBondMoreBaseWireframeProtocol: ErrorPresentable, AlertPresentable, CommonRetryable, diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmInteractor.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmInteractor.swift index 1976b4885d..47aa50ac23 100644 --- a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmInteractor.swift +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmInteractor.swift @@ -1,5 +1,6 @@ import UIKit import BigInt +import SubstrateSdk final class NominationPoolBondMoreConfirmInteractor: NominationPoolBondMoreBaseInteractor { weak var presenter: NominationPoolBondMoreConfirmInteractorOutputProtocol? { @@ -19,7 +20,6 @@ final class NominationPoolBondMoreConfirmInteractor: NominationPoolBondMoreBaseI extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol, npoolsOperationFactory: NominationPoolsOperationFactoryProtocol, npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, - stakingLocalSubscriptionFactory: StakingLocalSubscriptionFactoryProtocol, assetStorageInfoFactory: AssetStorageInfoOperationFactoryProtocol, operationQueue: OperationQueue, currencyManager: CurrencyManagerProtocol, @@ -37,7 +37,6 @@ final class NominationPoolBondMoreConfirmInteractor: NominationPoolBondMoreBaseI extrinsicServiceFactory: extrinsicServiceFactory, npoolsOperationFactory: npoolsOperationFactory, npoolsLocalSubscriptionFactory: npoolsLocalSubscriptionFactory, - stakingLocalSubscriptionFactory: stakingLocalSubscriptionFactory, assetStorageInfoFactory: assetStorageInfoFactory, operationQueue: operationQueue, currencyManager: currencyManager @@ -46,9 +45,9 @@ final class NominationPoolBondMoreConfirmInteractor: NominationPoolBondMoreBaseI } extension NominationPoolBondMoreConfirmInteractor: NominationPoolBondMoreConfirmInteractorInputProtocol { - func submit(amount: BigUInt) { + func submit(amount: BigUInt, needsMigration: Bool) { extrinsicService.submit( - createExtrinsicClosure(for: amount), + createExtrinsicClosure(for: amount, accountId: accountId, needsMigration: needsMigration), signer: signingWrapper, runningIn: .main ) { [weak self] result in diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmPresenter.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmPresenter.swift index 65505d7abf..25a3fed86c 100644 --- a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmPresenter.swift +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmPresenter.swift @@ -30,6 +30,7 @@ final class NominationPoolBondMoreConfirmPresenter: NominationPoolBondMoreBasePr hintsViewModelFactory: NominationPoolsBondMoreHintsFactoryProtocol, balanceViewModelFactory: BalanceViewModelFactoryProtocol, dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol, + stakingActivity: StakingActivityForValidating, localizationManager: LocalizationManagerProtocol, logger: LoggerProtocol ) { @@ -42,6 +43,7 @@ final class NominationPoolBondMoreConfirmPresenter: NominationPoolBondMoreBasePr hintsViewModelFactory: hintsViewModelFactory, balanceViewModelFactory: balanceViewModelFactory, dataValidatorFactory: dataValidatorFactory, + stakingActivity: stakingActivity, localizationManager: localizationManager, logger: logger ) @@ -129,12 +131,12 @@ extension NominationPoolBondMoreConfirmPresenter: NominationPoolBondMoreConfirmP DataValidationRunner( validators: validators ).runValidation { [weak self] in - guard let amount = self?.getInputAmountInPlank() else { + guard let amount = self?.getInputAmountInPlank(), let needsMigration = self?.needsMigration else { return } self?.view?.didStartLoading() - self?.interactor?.submit(amount: amount) + self?.interactor?.submit(amount: amount, needsMigration: needsMigration) } } diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmProtocols.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmProtocols.swift index 92a477e7e3..97bd33063e 100644 --- a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmProtocols.swift +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmProtocols.swift @@ -5,8 +5,6 @@ protocol NominationPoolBondMoreConfirmViewProtocol: NominationPoolBondMoreBaseVi func didReceiveWallet(viewModel: DisplayWalletViewModel) func didReceiveAccount(viewModel: DisplayAddressViewModel) func didReceiveFee(viewModel: BalanceViewModelProtocol?) - func didStartLoading() - func didStopLoading() } protocol NominationPoolBondMoreConfirmPresenterProtocol: AnyObject { @@ -16,7 +14,7 @@ protocol NominationPoolBondMoreConfirmPresenterProtocol: AnyObject { } protocol NominationPoolBondMoreConfirmInteractorInputProtocol: NominationPoolBondMoreBaseInteractorInputProtocol { - func submit(amount: BigUInt) + func submit(amount: BigUInt, needsMigration: Bool) } protocol NominationPoolBondMoreConfirmInteractorOutputProtocol: NominationPoolBondMoreBaseInteractorOutputProtocol { diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewController.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewController.swift index 3668c7eb6e..9f31b37396 100644 --- a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewController.swift +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewController.swift @@ -96,14 +96,6 @@ extension NominationPoolBondMoreConfirmViewController: NominationPoolBondMoreCon func didReceiveHints(viewModel: [String]) { rootView.hintListView.bind(texts: viewModel) } - - func didStartLoading() { - rootView.loadingView.startLoading() - } - - func didStopLoading() { - rootView.loadingView.stopLoading() - } } extension NominationPoolBondMoreConfirmViewController: Localizable { diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewFactory.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewFactory.swift index 183e22d559..d5eaf17078 100644 --- a/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewFactory.swift +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Confirm/NominationPoolBondMoreConfirmViewFactory.swift @@ -9,7 +9,13 @@ struct NominationPoolBondMoreConfirmViewFactory { 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 { + let selectedAccount = wallet.fetchMetaChainAccount(for: state.chainAsset.chain.accountRequest()), + let stakingActivity = StakingActivityForValidation( + wallet: SelectedWalletSettings.shared.value, + chain: state.chainAsset.chain, + chainRegistry: ChainRegistryFacade.sharedRegistry, + operationQueue: OperationManagerFacade.sharedDefaultQueue + ) else { return nil } let wireframe = NominationPoolBondMoreConfirmWireframe() @@ -37,6 +43,7 @@ struct NominationPoolBondMoreConfirmViewFactory { hintsViewModelFactory: hintsViewModelFactory, balanceViewModelFactory: balanceViewModelFactory, dataValidatorFactory: dataValidatorFactory, + stakingActivity: stakingActivity, localizationManager: localizationManager, logger: Logger.shared ) @@ -93,7 +100,6 @@ struct NominationPoolBondMoreConfirmViewFactory { 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/NominationPoolBondMoreSetupPresenter.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupPresenter.swift index c6878adab8..d11c4bdd40 100644 --- a/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupPresenter.swift +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupPresenter.swift @@ -16,29 +16,6 @@ final class NominationPoolBondMoreSetupPresenter: NominationPoolBondMoreBasePres 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) diff --git a/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupViewFactory.swift b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupViewFactory.swift index a8a8a65e33..e41e0c14db 100644 --- a/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupViewFactory.swift +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupViewFactory.swift @@ -4,7 +4,13 @@ import SoraFoundation struct NominationPoolBondMoreSetupViewFactory { static func createView(state: NPoolsStakingSharedStateProtocol) -> NominationPoolBondMoreSetupViewProtocol? { guard let currencyManager = CurrencyManager.shared, - let interactor = createInteractor(state: state) else { + let interactor = createInteractor(state: state), + let stakingActivity = StakingActivityForValidation( + wallet: SelectedWalletSettings.shared.value, + chain: state.chainAsset.chain, + chainRegistry: ChainRegistryFacade.sharedRegistry, + operationQueue: OperationManagerFacade.sharedDefaultQueue + ) else { return nil } let wireframe = NominationPoolBondMoreSetupWireframe(state: state) @@ -30,6 +36,7 @@ struct NominationPoolBondMoreSetupViewFactory { hintsViewModelFactory: hintsViewModelFactory, balanceViewModelFactory: balanceViewModelFactory, dataValidatorFactory: dataValidatorFactory, + stakingActivity: stakingActivity, localizationManager: localizationManager, logger: Logger.shared ) @@ -84,7 +91,6 @@ struct NominationPoolBondMoreSetupViewFactory { 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 index aa4f2f2741..12eb4664c7 100644 --- a/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupViewLayout.swift +++ b/novawallet/Modules/Staking/NominationPools/BondMore/Setup/NominationPoolBondMoreSetupViewLayout.swift @@ -1,6 +1,6 @@ import UIKit -final class NominationPoolBondMoreSetupViewLayout: SCSingleActionLayoutView { +final class NominationPoolBondMoreSetupViewLayout: SCLoadableActionLayoutView { let amountView = TitleHorizontalMultiValueView() let amountInputView = NewAmountInputView() @@ -10,6 +10,10 @@ final class NominationPoolBondMoreSetupViewLayout: SCSingleActionLayoutView { let hintListView = HintListView() var actionButton: TriangularedButton { + genericActionView.actionButton + } + + var loadingView: LoadableActionView { genericActionView } diff --git a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsInteractor.swift b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsInteractor.swift index e893ea39e0..6d09b11742 100644 --- a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsInteractor.swift +++ b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsInteractor.swift @@ -3,7 +3,8 @@ import Operation_iOS import SubstrateSdk import BigInt -final class NPoolsClaimRewardsInteractor: RuntimeConstantFetching { +final class NPoolsClaimRewardsInteractor: RuntimeConstantFetching, AnyProviderAutoCleaning, + NominationPoolStakingMigrating { weak var presenter: NPoolsClaimRewardsInteractorOutputProtocol? let selectedAccount: MetaChainAccountResponse @@ -28,6 +29,8 @@ final class NPoolsClaimRewardsInteractor: RuntimeConstantFetching { private var priceProvider: StreamableProvider? private var rewardPoolProvider: AnyDataProvider? private var claimableRewardProvider: AnySingleValueProvider? + private var delegatedStakingProvider: AnyDataProvider? + private var cancellableNeedsMigration = CancellableCallStore() private var currentPoolId: NominationPools.PoolId? private var currentPoolRewardCounter: BigUInt? @@ -101,20 +104,31 @@ final class NPoolsClaimRewardsInteractor: RuntimeConstantFetching { poolMemberProvider = subscribePoolMember(for: accountId, chainId: chainId) balanceProvider = subscribeToAssetBalanceProvider(for: accountId, chainId: chainId, assetId: assetId) + clear(dataProvider: &delegatedStakingProvider) + delegatedStakingProvider = subscribeDelegatedStaking(for: accountId, chainId: chainId) + setupCurrencyProvider() } func createExtrinsicBuilderClosure( - for strategy: NominationPools.ClaimRewardsStrategy + for strategy: NominationPools.ClaimRewardsStrategy, + accountId: AccountId, + needsMigration: Bool ) -> ExtrinsicBuilderClosure { { builder in + let currentBuilder = try NominationPools.migrateIfNeeded( + needsMigration, + accountId: accountId, + builder: builder + ) + switch strategy { case .restake: let bondExtra = NominationPools.BondExtraCall(extra: .rewards) - return try builder.adding(call: bondExtra.runtimeCall()) + return try currentBuilder.adding(call: bondExtra.runtimeCall()) case .freeBalance: let claimRewards = NominationPools.ClaimRewardsCall() - return try builder.adding(call: claimRewards.runtimeCall()) + return try currentBuilder.adding(call: claimRewards.runtimeCall()) } } } @@ -151,17 +165,27 @@ extension NPoolsClaimRewardsInteractor: NPoolsClaimRewardsInteractorInputProtoco provideExistentialDeposit() } - func estimateFee(for strategy: NominationPools.ClaimRewardsStrategy) { + func estimateFee(for strategy: NominationPools.ClaimRewardsStrategy, needsMigration: Bool) { + let identifier = strategy.rawValue + "-" + "\(needsMigration)" + feeProxy.estimateFee( using: extrinsicService, - reuseIdentifier: strategy.rawValue, - setupBy: createExtrinsicBuilderClosure(for: strategy) + reuseIdentifier: identifier, + setupBy: createExtrinsicBuilderClosure( + for: strategy, + accountId: accountId, + needsMigration: needsMigration + ) ) } - func submit(for strategy: NominationPools.ClaimRewardsStrategy) { + func submit(for strategy: NominationPools.ClaimRewardsStrategy, needsMigration: Bool) { extrinsicService.submit( - createExtrinsicBuilderClosure(for: strategy), + createExtrinsicBuilderClosure( + for: strategy, + accountId: accountId, + needsMigration: needsMigration + ), signer: signingWrapper, runningIn: .main ) { [weak self] result in @@ -233,6 +257,33 @@ extension NPoolsClaimRewardsInteractor: NPoolsLocalStorageSubscriber, NPoolsLoca claimableRewardProvider?.refresh() } } + + func handleDelegatedStaking( + result: Result, + accountId _: AccountId, + chainId _: ChainModel.Id + ) { + switch result { + case let .success(delegation): + cancellableNeedsMigration.cancel() + + needsPoolStakingMigration( + for: delegation, + runtimeProvider: runtimeService, + cancellableStore: cancellableNeedsMigration, + operationQueue: operationQueue + ) { [weak self] result in + switch result { + case let .success(needsMigration): + self?.presenter?.didReceive(needsMigration: needsMigration) + case let .failure(error): + self?.presenter?.didReceive(error: .subscription(error, "Needs Migration")) + } + } + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "Delegated Staking")) + } + } } extension NPoolsClaimRewardsInteractor: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler { diff --git a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsPresenter.swift b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsPresenter.swift index 38f5d29b59..d2fec8c6c3 100644 --- a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsPresenter.swift +++ b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsPresenter.swift @@ -8,6 +8,7 @@ final class NPoolsClaimRewardsPresenter { let interactor: NPoolsClaimRewardsInteractorInputProtocol let chainAsset: ChainAsset let dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol + let stakingActivity: StakingActivityForValidating let balanceViewModelFactory: BalanceViewModelFactoryProtocol let selectedAccount: MetaChainAccountResponse let logger: LoggerProtocol @@ -22,6 +23,7 @@ final class NPoolsClaimRewardsPresenter { var price: PriceData? var existentialDeposit: BigUInt? var fee: ExtrinsicFeeProtocol? + var needsMigration: Bool? init( interactor: NPoolsClaimRewardsInteractorInputProtocol, @@ -30,6 +32,7 @@ final class NPoolsClaimRewardsPresenter { chainAsset: ChainAsset, balanceViewModelFactory: BalanceViewModelFactoryProtocol, dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol, + stakingActivity: StakingActivityForValidating, localizationManager: LocalizationManagerProtocol, logger: LoggerProtocol ) { @@ -39,6 +42,7 @@ final class NPoolsClaimRewardsPresenter { self.chainAsset = chainAsset self.balanceViewModelFactory = balanceViewModelFactory self.dataValidatorFactory = dataValidatorFactory + self.stakingActivity = stakingActivity self.logger = logger self.localizationManager = localizationManager } @@ -105,10 +109,14 @@ final class NPoolsClaimRewardsPresenter { } private func refreshFee() { + guard let needsMigration else { + return + } + fee = nil provideFee() - interactor.estimateFee(for: claimRewardsStrategy) + interactor.estimateFee(for: claimRewardsStrategy, needsMigration: needsMigration) } } @@ -126,6 +134,19 @@ extension NPoolsClaimRewardsPresenter: NPoolsClaimRewardsPresenterProtocol { dataValidatorFactory.has(fee: fee, locale: selectedLocale) { [weak self] in self?.refreshFee() }, + dataValidatorFactory.canMigrateIfNeeded( + needsMigration: needsMigration, + stakingActivity: stakingActivity, + onProgress: .init( + willStart: { [weak self] in + self?.view?.didStartLoading() + }, + didComplete: { [weak self] _ in + self?.view?.didStopLoading() + } + ), + locale: selectedLocale + ), dataValidatorFactory.canPayFeeInPlank( balance: assetBalance?.transferable, fee: fee, @@ -146,13 +167,15 @@ extension NPoolsClaimRewardsPresenter: NPoolsClaimRewardsPresenterProtocol { locale: selectedLocale ) ]).runValidation { [weak self] in - guard let claimRewardsStrategy = self?.claimRewardsStrategy else { + guard + let claimRewardsStrategy = self?.claimRewardsStrategy, + let needsMigration = self?.needsMigration else { return } self?.view?.didStartLoading() - self?.interactor.submit(for: claimRewardsStrategy) + self?.interactor.submit(for: claimRewardsStrategy, needsMigration: needsMigration) } } @@ -242,6 +265,14 @@ extension NPoolsClaimRewardsPresenter: NPoolsClaimRewardsInteractorOutputProtoco } } + func didReceive(needsMigration: Bool) { + logger.debug("Needs migration: \(needsMigration)") + + self.needsMigration = needsMigration + + refreshFee() + } + func didReceive(error: NPoolsClaimRewardsError) { logger.error("Error: \(error)") diff --git a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsProtocols.swift b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsProtocols.swift index 72678c2081..470c122f69 100644 --- a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsProtocols.swift +++ b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsProtocols.swift @@ -1,6 +1,6 @@ import BigInt -protocol NPoolsClaimRewardsViewProtocol: ControllerBackedProtocol, LoadableViewProtocol { +protocol NPoolsClaimRewardsViewProtocol: SCLoadableControllerProtocol { func didReceiveAmount(viewModel: BalanceViewModelProtocol) func didReceiveWallet(viewModel: DisplayWalletViewModel) func didReceiveAccount(viewModel: DisplayAddressViewModel) @@ -19,8 +19,8 @@ protocol NPoolsClaimRewardsInteractorInputProtocol: AnyObject { func setup() func remakeSubscriptions() func retryExistentialDeposit() - func estimateFee(for strategy: NominationPools.ClaimRewardsStrategy) - func submit(for strategy: NominationPools.ClaimRewardsStrategy) + func estimateFee(for strategy: NominationPools.ClaimRewardsStrategy, needsMigration: Bool) + func submit(for strategy: NominationPools.ClaimRewardsStrategy, needsMigration: Bool) } protocol NPoolsClaimRewardsInteractorOutputProtocol: AnyObject { @@ -30,6 +30,7 @@ protocol NPoolsClaimRewardsInteractorOutputProtocol: AnyObject { func didReceive(price: PriceData?) func didReceive(fee: ExtrinsicFeeProtocol) func didReceive(submissionResult: Result) + func didReceive(needsMigration: Bool) func didReceive(error: NPoolsClaimRewardsError) } diff --git a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewController.swift b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewController.swift index afdf8b499c..8b9f014e23 100644 --- a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewController.swift +++ b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewController.swift @@ -113,14 +113,6 @@ extension NPoolsClaimRewardsViewController: NPoolsClaimRewardsViewProtocol { let shouldRestake = viewModel == .restake rootView.restakeCell.switchControl.setOn(shouldRestake, animated: false) } - - func didStartLoading() { - rootView.loadingView.startLoading() - } - - func didStopLoading() { - rootView.loadingView.stopLoading() - } } extension NPoolsClaimRewardsViewController: Localizable { diff --git a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewFactory.swift b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewFactory.swift index f54675cb4a..f9ebc47c02 100644 --- a/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewFactory.swift +++ b/novawallet/Modules/Staking/NominationPools/ClaimRewards/NPoolsClaimRewardsViewFactory.swift @@ -8,7 +8,13 @@ struct NPoolsClaimRewardsViewFactory { let interactor = createInteractor(for: state), let wallet = SelectedWalletSettings.shared.value, let selectedAccount = wallet.fetchMetaChainAccount(for: state.chainAsset.chain.accountRequest()), - let currencyManager = CurrencyManager.shared else { + let currencyManager = CurrencyManager.shared, + let stakingActivity = StakingActivityForValidation( + wallet: SelectedWalletSettings.shared.value, + chain: state.chainAsset.chain, + chainRegistry: ChainRegistryFacade.sharedRegistry, + operationQueue: OperationManagerFacade.sharedDefaultQueue + ) else { return nil } @@ -31,6 +37,7 @@ struct NPoolsClaimRewardsViewFactory { chainAsset: state.chainAsset, balanceViewModelFactory: balanceViewModelFactory, dataValidatorFactory: dataValidatingFactory, + stakingActivity: stakingActivity, localizationManager: LocalizationManager.shared, logger: Logger.shared ) diff --git a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemInteractor.swift b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemInteractor.swift index 6fecf49eaa..f9aa7995ea 100644 --- a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemInteractor.swift +++ b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemInteractor.swift @@ -3,7 +3,8 @@ import Operation_iOS import SubstrateSdk import BigInt -final class NPoolsRedeemInteractor: RuntimeConstantFetching { +final class NPoolsRedeemInteractor: RuntimeConstantFetching, NominationPoolStakingMigrating, + AnyProviderAutoCleaning, AnyCancellableCleaning { weak var presenter: NPoolsRedeemInteractorOutputProtocol? let selectedAccount: MetaChainAccountResponse @@ -32,6 +33,8 @@ final class NPoolsRedeemInteractor: RuntimeConstantFetching { private var balanceProvider: StreamableProvider? private var priceProvider: StreamableProvider? private var activeEraProvider: AnyDataProvider? + private var delegatedStakingProvider: AnyDataProvider? + private var cancellableNeedsMigration = CancellableCallStore() private var currentPoolId: NominationPools.PoolId? @@ -93,6 +96,9 @@ final class NPoolsRedeemInteractor: RuntimeConstantFetching { balanceProvider = subscribeToAssetBalanceProvider(for: accountId, chainId: chainId, assetId: assetId) activeEraProvider = subscribeActiveEra(for: chainId) + clear(dataProvider: &delegatedStakingProvider) + delegatedStakingProvider = subscribeDelegatedStaking(for: accountId, chainId: chainId) + setupCurrencyProvider() } @@ -140,29 +146,46 @@ final class NPoolsRedeemInteractor: RuntimeConstantFetching { func createExtrinsicBuilderClosure( for accountId: AccountId, - numOfSlashingSpans: UInt32 + numOfSlashingSpans: UInt32, + needsMigration: Bool ) -> ExtrinsicBuilderClosure { { builder in + let currentBuilder = try NominationPools.migrateIfNeeded( + needsMigration, + accountId: accountId, + builder: builder + ) + let redeemCall = NominationPools.RedeemCall( memberAccount: .accoundId(accountId), numberOfSlashingSpans: numOfSlashingSpans ) - return try builder.adding(call: redeemCall.runtimeCall()) + return try currentBuilder.adding(call: redeemCall.runtimeCall()) } } - func estimateFee(for numOfSlashingSpans: UInt32) { + func estimateFee(for numOfSlashingSpans: UInt32, needsMigration: Bool) { + let identifier = "\(numOfSlashingSpans)" + "-" + "\(needsMigration)" + feeProxy.estimateFee( using: extrinsicService, - reuseIdentifier: TransactionFeeId(numOfSlashingSpans), - setupBy: createExtrinsicBuilderClosure(for: accountId, numOfSlashingSpans: numOfSlashingSpans) + reuseIdentifier: identifier, + setupBy: createExtrinsicBuilderClosure( + for: accountId, + numOfSlashingSpans: numOfSlashingSpans, + needsMigration: needsMigration + ) ) } - func submit(for numberOfSlashingSpans: UInt32) { + func submit(for numberOfSlashingSpans: UInt32, needsMigration: Bool) { extrinsicService.submit( - createExtrinsicBuilderClosure(for: accountId, numOfSlashingSpans: numberOfSlashingSpans), + createExtrinsicBuilderClosure( + for: accountId, + numOfSlashingSpans: numberOfSlashingSpans, + needsMigration: needsMigration + ), signer: signingWrapper, runningIn: .main ) { [weak self] result in @@ -202,7 +225,7 @@ extension NPoolsRedeemInteractor: NPoolsRedeemInteractorInputProtocol { provideExistentialDeposit() } - func estimateFee() { + func estimateFee(needsMigration: Bool) { guard let poolId = currentPoolId else { return } @@ -210,14 +233,17 @@ extension NPoolsRedeemInteractor: NPoolsRedeemInteractorInputProtocol { fetchSlashingSpansForStash(poolId: poolId) { [weak self] result in switch result { case let .success(optSlashingSpans): - self?.estimateFee(for: optSlashingSpans?.numOfSlashingSpans ?? 0) + self?.estimateFee( + for: optSlashingSpans?.numOfSlashingSpans ?? 0, + needsMigration: needsMigration + ) case let .failure(error): self?.presenter?.didReceive(error: .fee(error)) } } } - func submit() { + func submit(needsMigration: Bool) { guard let poolId = currentPoolId else { presenter?.didReceive(submissionResult: .failure(CommonError.dataCorruption)) return @@ -226,7 +252,10 @@ extension NPoolsRedeemInteractor: NPoolsRedeemInteractorInputProtocol { fetchSlashingSpansForStash(poolId: poolId) { [weak self] result in switch result { case let .success(optSlashingSpans): - self?.submit(for: optSlashingSpans?.numOfSlashingSpans ?? 0) + self?.submit( + for: optSlashingSpans?.numOfSlashingSpans ?? 0, + needsMigration: needsMigration + ) case let .failure(error): self?.presenter?.didReceive(submissionResult: .failure(error)) } @@ -256,8 +285,6 @@ extension NPoolsRedeemInteractor: NPoolsLocalStorageSubscriber, NPoolsLocalSubsc currentPoolId = optPoolMember?.poolId setupPoolProviders() - - estimateFee() } presenter?.didReceive(poolMember: optPoolMember) @@ -278,6 +305,33 @@ extension NPoolsRedeemInteractor: NPoolsLocalStorageSubscriber, NPoolsLocalSubsc presenter?.didReceive(error: .subscription(error, "sub pools")) } } + + func handleDelegatedStaking( + result: Result, + accountId _: AccountId, + chainId _: ChainModel.Id + ) { + switch result { + case let .success(delegation): + cancellableNeedsMigration.cancel() + + needsPoolStakingMigration( + for: delegation, + runtimeProvider: runtimeService, + cancellableStore: cancellableNeedsMigration, + operationQueue: operationQueue + ) { [weak self] result in + switch result { + case let .success(needsMigration): + self?.presenter?.didReceive(needsMigration: needsMigration) + case let .failure(error): + self?.presenter?.didReceive(error: .subscription(error, "Needs Migration")) + } + } + case let .failure(error): + presenter?.didReceive(error: .subscription(error, "Delegated Staking")) + } + } } extension NPoolsRedeemInteractor: StakingLocalStorageSubscriber, StakingLocalSubscriptionHandler { diff --git a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemPresenter.swift b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemPresenter.swift index bc477fe61f..82adb6a363 100644 --- a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemPresenter.swift +++ b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemPresenter.swift @@ -8,6 +8,8 @@ final class NPoolsRedeemPresenter { let interactor: NPoolsRedeemInteractorInputProtocol let chainAsset: ChainAsset let dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol + let stakingActivity: StakingActivityForValidating + let balanceViewModelFactory: BalanceViewModelFactoryProtocol let selectedAccount: MetaChainAccountResponse let logger: LoggerProtocol @@ -22,6 +24,7 @@ final class NPoolsRedeemPresenter { var existentialDeposit: BigUInt? var price: PriceData? var fee: ExtrinsicFeeProtocol? + var needsMigration: Bool? init( interactor: NPoolsRedeemInteractorInputProtocol, @@ -30,6 +33,7 @@ final class NPoolsRedeemPresenter { chainAsset: ChainAsset, balanceViewModelFactory: BalanceViewModelFactoryProtocol, dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol, + stakingActivity: StakingActivityForValidating, localizationManager: LocalizationManagerProtocol, logger: LoggerProtocol ) { @@ -39,6 +43,7 @@ final class NPoolsRedeemPresenter { self.chainAsset = chainAsset self.balanceViewModelFactory = balanceViewModelFactory self.dataValidatorFactory = dataValidatorFactory + self.stakingActivity = stakingActivity self.logger = logger self.localizationManager = localizationManager } @@ -103,6 +108,14 @@ final class NPoolsRedeemPresenter { view?.didReceiveFee(viewModel: viewModel) } + private func refreshFee() { + guard let needsMigration else { + return + } + + interactor.estimateFee(needsMigration: needsMigration) + } + func updateView() { provideAmountViewModel() provideWalletViewModel() @@ -124,8 +137,21 @@ extension NPoolsRedeemPresenter: NPoolsRedeemPresenterProtocol { fee: fee, locale: selectedLocale ) { [weak self] in - self?.interactor.estimateFee() + self?.refreshFee() }, + dataValidatorFactory.canMigrateIfNeeded( + needsMigration: needsMigration, + stakingActivity: stakingActivity, + onProgress: .init( + willStart: { [weak self] in + self?.view?.didStartLoading() + }, + didComplete: { [weak self] _ in + self?.view?.didStopLoading() + } + ), + locale: selectedLocale + ), dataValidatorFactory.canPayFeeInPlank( balance: assetBalance?.transferable, fee: fee, @@ -140,8 +166,12 @@ extension NPoolsRedeemPresenter: NPoolsRedeemPresenterProtocol { locale: selectedLocale ) ]).runValidation { [weak self] in + guard let needsMigration = self?.needsMigration else { + return + } + self?.view?.didStartLoading() - self?.interactor.submit() + self?.interactor.submit(needsMigration: needsMigration) } } @@ -240,6 +270,14 @@ extension NPoolsRedeemPresenter: NPoolsRedeemInteractorOutputProtocol { } } + func didReceive(needsMigration: Bool) { + logger.debug("Needs migration: \(needsMigration)") + + self.needsMigration = needsMigration + + refreshFee() + } + func didReceive(error: NPoolsRedeemError) { logger.error("Error: \(error)") @@ -250,7 +288,7 @@ extension NPoolsRedeemPresenter: NPoolsRedeemInteractorOutputProtocol { } case .fee: wireframe.presentFeeStatus(on: view, locale: selectedLocale) { [weak self] in - self?.interactor.estimateFee() + self?.refreshFee() } case .existentialDeposit: wireframe.presentRequestStatus(on: view, locale: selectedLocale) { [weak self] in diff --git a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemProtocols.swift b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemProtocols.swift index 5b075c6f3e..9070a3bd04 100644 --- a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemProtocols.swift +++ b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemProtocols.swift @@ -1,6 +1,6 @@ import BigInt -protocol NPoolsRedeemViewProtocol: ControllerBackedProtocol, LoadableViewProtocol { +protocol NPoolsRedeemViewProtocol: SCLoadableControllerProtocol { func didReceiveAmount(viewModel: BalanceViewModelProtocol) func didReceiveWallet(viewModel: DisplayWalletViewModel) func didReceiveAccount(viewModel: DisplayAddressViewModel) @@ -17,8 +17,8 @@ protocol NPoolsRedeemInteractorInputProtocol: AnyObject { func setup() func remakeSubscriptions() func retryExistentialDeposit() - func estimateFee() - func submit() + func estimateFee(needsMigration: Bool) + func submit(needsMigration: Bool) } protocol NPoolsRedeemInteractorOutputProtocol: AnyObject { @@ -30,6 +30,7 @@ protocol NPoolsRedeemInteractorOutputProtocol: AnyObject { func didReceive(existentialDeposit: BigUInt?) func didReceive(fee: ExtrinsicFeeProtocol) func didReceive(submissionResult: Result) + func didReceive(needsMigration: Bool) func didReceive(error: NPoolsRedeemError) } diff --git a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewController.swift b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewController.swift index 904eaa4f6e..d2fa5cfbb9 100644 --- a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewController.swift +++ b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewController.swift @@ -89,14 +89,6 @@ extension NPoolsRedeemViewController: NPoolsRedeemViewProtocol { func didReceiveFee(viewModel: BalanceViewModelProtocol?) { rootView.networkFeeCell.rowContentView.bind(viewModel: viewModel) } - - func didStartLoading() { - rootView.loadingView.startLoading() - } - - func didStopLoading() { - rootView.loadingView.stopLoading() - } } extension NPoolsRedeemViewController: Localizable { diff --git a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewFactory.swift b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewFactory.swift index 71089f2075..472e5aad0d 100644 --- a/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewFactory.swift +++ b/novawallet/Modules/Staking/NominationPools/Redeem/NPoolsRedeemViewFactory.swift @@ -9,7 +9,13 @@ struct NPoolsRedeemViewFactory { let interactor = createInteractor(for: state), let wallet = SelectedWalletSettings.shared.value, let selectedAccount = wallet.fetchMetaChainAccount(for: state.chainAsset.chain.accountRequest()), - let currencyManager = CurrencyManager.shared else { + let currencyManager = CurrencyManager.shared, + let stakingActivity = StakingActivityForValidation( + wallet: SelectedWalletSettings.shared.value, + chain: state.chainAsset.chain, + chainRegistry: ChainRegistryFacade.sharedRegistry, + operationQueue: OperationManagerFacade.sharedDefaultQueue + ) else { return nil } @@ -32,6 +38,7 @@ struct NPoolsRedeemViewFactory { chainAsset: state.chainAsset, balanceViewModelFactory: balanceViewModelFactory, dataValidatorFactory: dataValidatingFactory, + stakingActivity: stakingActivity, localizationManager: LocalizationManager.shared, logger: Logger.shared ) diff --git a/novawallet/Modules/Staking/NominationPools/Shared/NominationPool+MigrateWrapping.swift b/novawallet/Modules/Staking/NominationPools/Shared/NominationPool+MigrateWrapping.swift new file mode 100644 index 0000000000..256abdf1f4 --- /dev/null +++ b/novawallet/Modules/Staking/NominationPools/Shared/NominationPool+MigrateWrapping.swift @@ -0,0 +1,20 @@ +import Foundation +import SubstrateSdk + +extension NominationPools { + static func migrateIfNeeded( + _ needsMigration: Bool, + accountId: AccountId, + builder: ExtrinsicBuilderProtocol + ) throws -> ExtrinsicBuilderProtocol { + if needsMigration { + return try builder.adding( + call: NominationPools.MigrateCall( + memberAccount: .accoundId(accountId) + ).runtimeCall() + ).with(batchType: .ignoreFails) + } else { + return builder + } + } +} diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseInteractor.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseInteractor.swift index d6841f536e..e0e5fd8276 100644 --- a/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseInteractor.swift +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseInteractor.swift @@ -3,7 +3,8 @@ import Operation_iOS import BigInt import SubstrateSdk -class NPoolsUnstakeBaseInteractor: AnyCancellableCleaning, NominationPoolsDataProviding, RuntimeConstantFetching { +class NPoolsUnstakeBaseInteractor: AnyCancellableCleaning, NominationPoolsDataProviding, RuntimeConstantFetching, + NominationPoolStakingMigrating, AnyProviderAutoCleaning { weak var basePresenter: NPoolsUnstakeBaseInteractorOutputProtocol? let selectedAccount: MetaChainAccountResponse @@ -37,6 +38,8 @@ class NPoolsUnstakeBaseInteractor: AnyCancellableCleaning, NominationPoolsDataPr private var rewardPoolProvider: AnyDataProvider? private var claimableRewardProvider: AnySingleValueProvider? private var minStakeProvider: AnyDataProvider? + private var delegatedStakingProvider: AnyDataProvider? + private var cancellableNeedsMigration = CancellableCallStore() private var bondedAccountIdCancellable: CancellableCall? private var eraCountdownCancellable: CancellableCall? @@ -181,6 +184,10 @@ class NPoolsUnstakeBaseInteractor: AnyCancellableCleaning, NominationPoolsDataPr balanceProvider = subscribeToAssetBalanceProvider(for: accountId, chainId: chainId, assetId: assetId) minStakeProvider = subscribeMinJoinBond(for: chainId) + clear(dataProvider: &delegatedStakingProvider) + + delegatedStakingProvider = subscribeDelegatedStaking(for: accountId, chainId: chainId) + setupCurrencyProvider() } @@ -271,13 +278,23 @@ class NPoolsUnstakeBaseInteractor: AnyCancellableCleaning, NominationPoolsDataPr operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false) } - func createExtrinsicClosure(for points: BigUInt, accountId: AccountId) -> ExtrinsicBuilderClosure { + func createExtrinsicClosure( + for points: BigUInt, + accountId: AccountId, + needsMigration: Bool + ) -> ExtrinsicBuilderClosure { { builder in + let currentBuilder = try NominationPools.migrateIfNeeded( + needsMigration, + accountId: accountId, + builder: builder + ) + let call = NominationPools.UnbondCall( memberAccount: .accoundId(accountId), unbondingPoints: points ) - return try builder.adding(call: call.runtimeCall()) + return try currentBuilder.adding(call: call.runtimeCall()) } } @@ -333,13 +350,17 @@ extension NPoolsUnstakeBaseInteractor: NPoolsUnstakeBaseInteractorInputProtocol provideExistentialDeposit() } - func estimateFee(for points: BigUInt) { - let identifier = String(points) + func estimateFee(for points: BigUInt, needsMigration: Bool) { + let identifier = String(points) + "-" + "\(needsMigration)" feeProxy.estimateFee( using: extrinsicService, reuseIdentifier: identifier, - setupBy: createExtrinsicClosure(for: points, accountId: accountId) + setupBy: createExtrinsicClosure( + for: points, + accountId: accountId, + needsMigration: needsMigration + ) ) } } @@ -432,6 +453,33 @@ extension NPoolsUnstakeBaseInteractor: NPoolsLocalStorageSubscriber, NPoolsLocal basePresenter?.didReceive(error: .subscription(error, "min stake")) } } + + func handleDelegatedStaking( + result: Result, + accountId _: AccountId, + chainId _: ChainModel.Id + ) { + switch result { + case let .success(delegation): + cancellableNeedsMigration.cancel() + + needsPoolStakingMigration( + for: delegation, + runtimeProvider: runtimeService, + cancellableStore: cancellableNeedsMigration, + operationQueue: operationQueue + ) { [weak self] result in + switch result { + case let .success(needsMigration): + self?.basePresenter?.didReceive(needsMigration: needsMigration) + case let .failure(error): + self?.basePresenter?.didReceive(error: .subscription(error, "Needs Migration")) + } + } + case let .failure(error): + basePresenter?.didReceive(error: .subscription(error, "Delegated Staking")) + } + } } extension NPoolsUnstakeBaseInteractor: StakingLocalStorageSubscriber, StakingLocalSubscriptionHandler { diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBasePresenter.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBasePresenter.swift index ab598b838d..c2153563e1 100644 --- a/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBasePresenter.swift +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBasePresenter.swift @@ -3,13 +3,14 @@ import BigInt import SoraFoundation class NPoolsUnstakeBasePresenter: NPoolsUnstakeBaseInteractorOutputProtocol { - weak var baseView: ControllerBackedProtocol? + weak var baseView: NPoolsUnstakeBaseViewProtocol? let baseWireframe: NPoolsUnstakeBaseWireframeProtocol let baseInteractor: NPoolsUnstakeBaseInteractorInputProtocol let chainAsset: ChainAsset let hintsViewModelFactory: NPoolsUnstakeHintsFactoryProtocol let dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol + let stakingActivity: StakingActivityForValidating let balanceViewModelFactory: BalanceViewModelFactoryProtocol let logger: LoggerProtocol @@ -26,6 +27,8 @@ class NPoolsUnstakeBasePresenter: NPoolsUnstakeBaseInteractorOutputProtocol { var unstakingLimits: NominationPools.UnstakeLimits? var fee: ExtrinsicFeeProtocol? + var needsMigration: Bool? + init( baseInteractor: NPoolsUnstakeBaseInteractorInputProtocol, baseWireframe: NPoolsUnstakeBaseWireframeProtocol, @@ -33,6 +36,7 @@ class NPoolsUnstakeBasePresenter: NPoolsUnstakeBaseInteractorOutputProtocol { hintsViewModelFactory: NPoolsUnstakeHintsFactoryProtocol, balanceViewModelFactory: BalanceViewModelFactoryProtocol, dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol, + stakingActivity: StakingActivityForValidating, localizationManager: LocalizationManagerProtocol, logger: LoggerProtocol ) { @@ -42,6 +46,7 @@ class NPoolsUnstakeBasePresenter: NPoolsUnstakeBaseInteractorOutputProtocol { self.hintsViewModelFactory = hintsViewModelFactory self.balanceViewModelFactory = balanceViewModelFactory self.dataValidatorFactory = dataValidatorFactory + self.stakingActivity = stakingActivity self.logger = logger self.localizationManager = localizationManager } @@ -90,6 +95,19 @@ class NPoolsUnstakeBasePresenter: NPoolsUnstakeBaseInteractorOutputProtocol { dataValidatorFactory.has(fee: fee, locale: selectedLocale) { [weak self] in self?.refreshFee() }, + dataValidatorFactory.canMigrateIfNeeded( + needsMigration: needsMigration, + stakingActivity: stakingActivity, + onProgress: .init( + willStart: { [weak self] in + self?.baseView?.didStartLoading() + }, + didComplete: { [weak self] _ in + self?.baseView?.didStopLoading() + } + ), + locale: selectedLocale + ), dataValidatorFactory.canUnstake( for: getInputAmount() ?? 0, stakedAmountInPlank: getStakedAmountInPlank(), @@ -143,7 +161,7 @@ class NPoolsUnstakeBasePresenter: NPoolsUnstakeBaseInteractorOutputProtocol { } func refreshFee() { - guard let unstakingPoints = getUnstakingPoints() else { + guard let unstakingPoints = getUnstakingPoints(), let needsMigration else { return } @@ -151,7 +169,7 @@ class NPoolsUnstakeBasePresenter: NPoolsUnstakeBaseInteractorOutputProtocol { provideFee() - baseInteractor.estimateFee(for: unstakingPoints) + baseInteractor.estimateFee(for: unstakingPoints, needsMigration: needsMigration) } // MARK: Unstake Base Interactor Output @@ -253,6 +271,13 @@ class NPoolsUnstakeBasePresenter: NPoolsUnstakeBaseInteractorOutputProtocol { provideFee() } + func didReceive(needsMigration: Bool) { + logger.debug("Needs migration: \(needsMigration)") + + self.needsMigration = needsMigration + refreshFee() + } + func didReceive(error: NPoolsUnstakeBaseError) { logger.error("Error: \(error)") diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseProtocols.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseProtocols.swift index 5f029472ec..ae4b19a60d 100644 --- a/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseProtocols.swift +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Base/NPoolsUnstakeBaseProtocols.swift @@ -1,5 +1,7 @@ import BigInt +protocol NPoolsUnstakeBaseViewProtocol: SCLoadableControllerProtocol {} + protocol NPoolsUnstakeBaseInteractorInputProtocol: AnyObject { func setup() func retrySubscriptions() @@ -8,7 +10,7 @@ protocol NPoolsUnstakeBaseInteractorInputProtocol: AnyObject { func retryClaimableRewards() func retryUnstakeLimits() func retryExistentialDeposit() - func estimateFee(for points: BigUInt) + func estimateFee(for points: BigUInt, needsMigration: Bool) } protocol NPoolsUnstakeBaseInteractorOutputProtocol: AnyObject { @@ -25,6 +27,7 @@ protocol NPoolsUnstakeBaseInteractorOutputProtocol: AnyObject { func didReceive(unstakingLimits: NominationPools.UnstakeLimits) func didReceive(fee: ExtrinsicFeeProtocol) func didReceive(error: NPoolsUnstakeBaseError) + func didReceive(needsMigration: Bool) } protocol NPoolsUnstakeBaseWireframeProtocol: ErrorPresentable, AlertPresentable, CommonRetryable, FeeRetryable, diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmInteractor.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmInteractor.swift index 978db3de0d..0db15bac2c 100644 --- a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmInteractor.swift +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmInteractor.swift @@ -60,9 +60,9 @@ final class NPoolsUnstakeConfirmInteractor: NPoolsUnstakeBaseInteractor { } extension NPoolsUnstakeConfirmInteractor: NPoolsUnstakeConfirmInteractorInputProtocol { - func submit(unstakingPoints: BigUInt) { + func submit(unstakingPoints: BigUInt, needsMigration: Bool) { extrinsicService.submit( - createExtrinsicClosure(for: unstakingPoints, accountId: accountId), + createExtrinsicClosure(for: unstakingPoints, accountId: accountId, needsMigration: needsMigration), signer: signingWrapper, runningIn: .main ) { [weak self] result in diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmPresenter.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmPresenter.swift index d9dd7d9da5..4a62013cbd 100644 --- a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmPresenter.swift +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmPresenter.swift @@ -3,7 +3,9 @@ import SoraFoundation import BigInt final class NPoolsUnstakeConfirmPresenter: NPoolsUnstakeBasePresenter { - weak var view: NPoolsUnstakeConfirmViewProtocol? + var view: NPoolsUnstakeConfirmViewProtocol? { + baseView as? NPoolsUnstakeConfirmViewProtocol + } var wireframe: NPoolsUnstakeConfirmWireframeProtocol? { baseWireframe as? NPoolsUnstakeConfirmWireframeProtocol @@ -28,6 +30,7 @@ final class NPoolsUnstakeConfirmPresenter: NPoolsUnstakeBasePresenter { hintsViewModelFactory: NPoolsUnstakeHintsFactoryProtocol, balanceViewModelFactory: BalanceViewModelFactoryProtocol, dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol, + stakingActivity: StakingActivityForValidating, localizationManager: LocalizationManagerProtocol, logger: LoggerProtocol ) { @@ -41,6 +44,7 @@ final class NPoolsUnstakeConfirmPresenter: NPoolsUnstakeBasePresenter { hintsViewModelFactory: hintsViewModelFactory, balanceViewModelFactory: balanceViewModelFactory, dataValidatorFactory: dataValidatorFactory, + stakingActivity: stakingActivity, localizationManager: localizationManager, logger: logger ) @@ -140,12 +144,14 @@ extension NPoolsUnstakeConfirmPresenter: NPoolsUnstakeConfirmPresenterProtocol { DataValidationRunner( validators: validators ).runValidation { [weak self] in - guard let unstakingPoints = self?.getUnstakingPoints() else { + guard + let unstakingPoints = self?.getUnstakingPoints(), + let needsMigration = self?.needsMigration else { return } self?.view?.didStartLoading() - self?.interactor?.submit(unstakingPoints: unstakingPoints) + self?.interactor?.submit(unstakingPoints: unstakingPoints, needsMigration: needsMigration) } } diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmProtocols.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmProtocols.swift index e484790f8c..c9cc2c8285 100644 --- a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmProtocols.swift +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmProtocols.swift @@ -1,6 +1,6 @@ import BigInt -protocol NPoolsUnstakeConfirmViewProtocol: ControllerBackedProtocol, LoadableViewProtocol { +protocol NPoolsUnstakeConfirmViewProtocol: NPoolsUnstakeBaseViewProtocol { func didReceiveAmount(viewModel: BalanceViewModelProtocol) func didReceiveWallet(viewModel: DisplayWalletViewModel) func didReceiveAccount(viewModel: DisplayAddressViewModel) @@ -15,7 +15,7 @@ protocol NPoolsUnstakeConfirmPresenterProtocol: AnyObject { } protocol NPoolsUnstakeConfirmInteractorInputProtocol: NPoolsUnstakeBaseInteractorInputProtocol { - func submit(unstakingPoints: BigUInt) + func submit(unstakingPoints: BigUInt, needsMigration: Bool) } protocol NPoolsUnstakeConfirmInteractorOutputProtocol: NPoolsUnstakeBaseInteractorOutputProtocol { diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewController.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewController.swift index 5d4105e879..3d8db589b9 100644 --- a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewController.swift +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewController.swift @@ -93,14 +93,6 @@ extension NPoolsUnstakeConfirmViewController: NPoolsUnstakeConfirmViewProtocol { func didReceiveHints(viewModel: [String]) { rootView.hintListView.bind(texts: viewModel) } - - func didStartLoading() { - rootView.loadingView.startLoading() - } - - func didStopLoading() { - rootView.loadingView.stopLoading() - } } extension NPoolsUnstakeConfirmViewController: Localizable { diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewFactory.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewFactory.swift index fa7d917911..9a75c8b423 100644 --- a/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewFactory.swift +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Confirm/NPoolsUnstakeConfirmViewFactory.swift @@ -11,7 +11,13 @@ struct NPoolsUnstakeConfirmViewFactory { let interactor = createInteractor(for: state), let currencyManager = CurrencyManager.shared, let wallet = SelectedWalletSettings.shared.value, - let selectedAccount = wallet.fetchMetaChainAccount(for: state.chainAsset.chain.accountRequest()) else { + let selectedAccount = wallet.fetchMetaChainAccount(for: state.chainAsset.chain.accountRequest()), + let stakingActivity = StakingActivityForValidation( + wallet: SelectedWalletSettings.shared.value, + chain: state.chainAsset.chain, + chainRegistry: ChainRegistryFacade.sharedRegistry, + operationQueue: OperationManagerFacade.sharedDefaultQueue + ) else { return nil } @@ -41,6 +47,7 @@ struct NPoolsUnstakeConfirmViewFactory { hintsViewModelFactory: hintsViewModelFactory, balanceViewModelFactory: balanceViewModelFactory, dataValidatorFactory: dataValidatingFactory, + stakingActivity: stakingActivity, localizationManager: LocalizationManager.shared, logger: Logger.shared ) @@ -50,7 +57,7 @@ struct NPoolsUnstakeConfirmViewFactory { localizationManager: LocalizationManager.shared ) - presenter.view = view + presenter.baseView = view interactor.presenter = presenter dataValidatingFactory.view = view diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupPresenter.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupPresenter.swift index 47b8b94860..31ce2e273a 100644 --- a/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupPresenter.swift +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupPresenter.swift @@ -3,7 +3,9 @@ import SoraFoundation import BigInt final class NPoolsUnstakeSetupPresenter: NPoolsUnstakeBasePresenter { - weak var view: NPoolsUnstakeSetupViewProtocol? + var view: NPoolsUnstakeSetupViewProtocol? { + baseView as? NPoolsUnstakeSetupViewProtocol + } var wireframe: NPoolsUnstakeSetupWireframeProtocol? { baseWireframe as? NPoolsUnstakeSetupWireframeProtocol @@ -22,6 +24,7 @@ final class NPoolsUnstakeSetupPresenter: NPoolsUnstakeBasePresenter { hintsViewModelFactory: NPoolsUnstakeHintsFactoryProtocol, balanceViewModelFactory: BalanceViewModelFactoryProtocol, dataValidatorFactory: NominationPoolDataValidatorFactoryProtocol, + stakingActivity: StakingActivityForValidating, localizationManager: LocalizationManagerProtocol, logger: LoggerProtocol ) { @@ -32,6 +35,7 @@ final class NPoolsUnstakeSetupPresenter: NPoolsUnstakeBasePresenter { hintsViewModelFactory: hintsViewModelFactory, balanceViewModelFactory: balanceViewModelFactory, dataValidatorFactory: dataValidatorFactory, + stakingActivity: stakingActivity, localizationManager: localizationManager, logger: logger ) diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupProtocols.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupProtocols.swift index 390c35d1eb..bc7fff5971 100644 --- a/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupProtocols.swift +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupProtocols.swift @@ -1,6 +1,6 @@ import Foundation -protocol NPoolsUnstakeSetupViewProtocol: ControllerBackedProtocol { +protocol NPoolsUnstakeSetupViewProtocol: NPoolsUnstakeBaseViewProtocol { func didReceiveAssetBalance(viewModel: AssetBalanceViewModelProtocol) func didReceiveInput(viewModel: AmountInputViewModelProtocol) func didReceiveFee(viewModel: BalanceViewModelProtocol?) diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewFactory.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewFactory.swift index e8a5273676..93ec6693ab 100644 --- a/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewFactory.swift +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewFactory.swift @@ -6,7 +6,13 @@ struct NPoolsUnstakeSetupViewFactory { static func createView(for state: NPoolsStakingSharedStateProtocol) -> NPoolsUnstakeSetupViewProtocol? { guard let interactor = createInteractor(for: state), - let currencyManager = CurrencyManager.shared else { + let currencyManager = CurrencyManager.shared, + let stakingActivity = StakingActivityForValidation( + wallet: SelectedWalletSettings.shared.value, + chain: state.chainAsset.chain, + chainRegistry: ChainRegistryFacade.sharedRegistry, + operationQueue: OperationManagerFacade.sharedDefaultQueue + ) else { return nil } @@ -34,6 +40,7 @@ struct NPoolsUnstakeSetupViewFactory { hintsViewModelFactory: hintsViewModelFactory, balanceViewModelFactory: balanceViewModelFactory, dataValidatorFactory: dataValidatingFactory, + stakingActivity: stakingActivity, localizationManager: LocalizationManager.shared, logger: Logger.shared ) @@ -43,7 +50,7 @@ struct NPoolsUnstakeSetupViewFactory { localizationManager: LocalizationManager.shared ) - presenter.view = view + presenter.baseView = view interactor.presenter = presenter dataValidatingFactory.view = view diff --git a/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewLayout.swift b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewLayout.swift index c9d68fa089..49564dd81a 100644 --- a/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewLayout.swift +++ b/novawallet/Modules/Staking/NominationPools/Unstake/Setup/NPoolsUnstakeSetupViewLayout.swift @@ -1,6 +1,6 @@ import UIKit -final class NPoolsUnstakeSetupViewLayout: SCSingleActionLayoutView { +final class NPoolsUnstakeSetupViewLayout: SCLoadableActionLayoutView { let amountView = TitleHorizontalMultiValueView() let amountInputView = NewAmountInputView() @@ -12,6 +12,10 @@ final class NPoolsUnstakeSetupViewLayout: SCSingleActionLayoutView { let hintListView = HintListView() var actionButton: TriangularedButton { + genericActionView.actionButton + } + + var loadingView: LoadableActionView { genericActionView } diff --git a/novawallet/Modules/Staking/Protocols/NominationPoolErrorPresentable.swift b/novawallet/Modules/Staking/Protocols/NominationPoolErrorPresentable.swift index d30b59c1e3..aebe809794 100644 --- a/novawallet/Modules/Staking/Protocols/NominationPoolErrorPresentable.swift +++ b/novawallet/Modules/Staking/Protocols/NominationPoolErrorPresentable.swift @@ -1,5 +1,12 @@ import Foundation +struct NPoolsEDViolationErrorParams { + let availableBalance: String + let minimumBalance: String + let fee: String + let maxStake: String +} + protocol NominationPoolErrorPresentable: StakingBaseErrorPresentable { func presentNominationPoolHasNoApy( from view: ControllerBackedProtocol, @@ -45,6 +52,11 @@ protocol NominationPoolErrorPresentable: StakingBaseErrorPresentable { action: (() -> Void)?, locale: Locale ) + + func presentDirectStakingNotAllowedForMigration( + from view: ControllerBackedProtocol, + locale: Locale + ) } extension NominationPoolErrorPresentable where Self: AlertPresentable & ErrorPresentable { @@ -208,4 +220,19 @@ extension NominationPoolErrorPresentable where Self: AlertPresentable & ErrorPre present(viewModel: viewModel, style: .alert, from: view) } + + func presentDirectStakingNotAllowedForMigration( + from view: ControllerBackedProtocol, + locale: Locale + ) { + let title = R.string.localizable.nominationPoolsConflictTitle(preferredLanguages: locale.rLanguages) + let message = R.string.localizable.nominationPoolsConflictMessage(preferredLanguages: locale.rLanguages) + + present( + message: message, + title: title, + closeAction: R.string.localizable.commonClose(preferredLanguages: locale.rLanguages), + from: view + ) + } } diff --git a/novawallet/Modules/Staking/Protocols/NominationPoolStakingMigrating.swift b/novawallet/Modules/Staking/Protocols/NominationPoolStakingMigrating.swift new file mode 100644 index 0000000000..1387f31de6 --- /dev/null +++ b/novawallet/Modules/Staking/Protocols/NominationPoolStakingMigrating.swift @@ -0,0 +1,52 @@ +import Foundation +import Operation_iOS +import SubstrateSdk + +protocol NominationPoolStakingMigrating { + func needsPoolStakingMigration( + for stakingDelegation: DelegatedStakingPallet.Delegation?, + runtimeProvider: RuntimeCodingServiceProtocol, + cancellableStore: CancellableCallStore, + operationQueue: OperationQueue, + completion: @escaping (Result) -> Void + ) +} + +extension NominationPoolStakingMigrating { + func needsPoolStakingMigration( + for stakingDelegation: DelegatedStakingPallet.Delegation?, + runtimeProvider: RuntimeCodingServiceProtocol, + cancellableStore: CancellableCallStore, + operationQueue: OperationQueue, + completion: @escaping (Result) -> Void + ) { + guard stakingDelegation == nil else { + completion(.success(false)) + return + } + + let codingFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() + + let checkDelegatedStakingOperation = ClosureOperation { + let codingFactory = try codingFactoryOperation.extractNoCancellableResultData() + + return codingFactory.hasCall(for: NominationPools.MigrateCall.codingPath) && + codingFactory.hasStorage(for: DelegatedStakingPallet.delegatorsPath) + } + + checkDelegatedStakingOperation.addDependency(codingFactoryOperation) + + let wrapper = CompoundOperationWrapper( + targetOperation: checkDelegatedStakingOperation, + dependencies: [codingFactoryOperation] + ) + + executeCancellable( + wrapper: wrapper, + inOperationQueue: operationQueue, + backingCallIn: cancellableStore, + runningCallbackIn: .main, + callbackClosure: completion + ) + } +} diff --git a/novawallet/Modules/Staking/Protocols/StakingActivityProviding.swift b/novawallet/Modules/Staking/Protocols/StakingActivityProviding.swift new file mode 100644 index 0000000000..6d4ba4e017 --- /dev/null +++ b/novawallet/Modules/Staking/Protocols/StakingActivityProviding.swift @@ -0,0 +1,120 @@ +import Foundation +import SubstrateSdk +import Operation_iOS + +protocol StakingActivityProviding { + func hasDirectStaking( + for accountId: AccountId, + connection: JSONRPCEngine, + runtimeProvider: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue, + completion: @escaping (Result) -> Void + ) + + func hasPoolStaking( + for accountId: AccountId, + connection: JSONRPCEngine, + runtimeProvider: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue, + completion: @escaping (Result) -> Void + ) +} + +private struct StakingAvailabilityParams { + let accountId: AccountId + let storagePath: StorageCodingPath +} + +extension StakingActivityProviding { + private func hasStorageValue( + for params: StakingAvailabilityParams, + connection: JSONRPCEngine, + runtimeProvider: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue, + completion: @escaping (Result) -> Void + ) { + let coderFactoryOperation = runtimeProvider.fetchCoderFactoryOperation() + + let checkWrapper = OperationCombiningService.compoundNonOptionalWrapper( + operationManager: OperationManager(operationQueue: operationQueue) + ) { + let coderFactory = try coderFactoryOperation.extractNoCancellableResultData() + + guard coderFactory.hasStorage(for: params.storagePath) else { + return CompoundOperationWrapper.createWithResult(false) + } + + let requestFactory = StorageRequestFactory( + remoteFactory: StorageKeyFactory(), + operationManager: OperationManager(operationQueue: operationQueue) + ) + + let fetchWrapper: CompoundOperationWrapper<[StorageResponse]> = requestFactory.queryItems( + engine: connection, + keyParams: { [params.accountId] }, + factory: { coderFactory }, + storagePath: params.storagePath + ) + + let mapOperation = ClosureOperation { + let response = try fetchWrapper.targetOperation.extractNoCancellableResultData() + let hasValue = response.first?.value != nil + + return hasValue + } + + mapOperation.addDependency(fetchWrapper.targetOperation) + + return fetchWrapper.insertingTail(operation: mapOperation) + } + + checkWrapper.addDependency(operations: [coderFactoryOperation]) + + let totalWrapper = checkWrapper.insertingHead(operations: [coderFactoryOperation]) + + execute( + wrapper: totalWrapper, + inOperationQueue: operationQueue, + runningCallbackIn: .main + ) { result in + switch result { + case let .success(hasValue): + completion(.success(hasValue)) + case let .failure(error): + completion(.failure(error)) + } + } + } + + func hasPoolStaking( + for accountId: AccountId, + connection: JSONRPCEngine, + runtimeProvider: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue, + completion: @escaping (Result) -> Void + ) { + hasStorageValue( + for: .init(accountId: accountId, storagePath: NominationPools.poolMembersPath), + connection: connection, + runtimeProvider: runtimeProvider, + operationQueue: operationQueue, + completion: completion + ) + } + + func hasDirectStaking( + for accountId: AccountId, + connection: JSONRPCEngine, + runtimeProvider: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue, + completion: @escaping (Result) -> Void + ) { + hasStorageValue( + for: .init(accountId: accountId, storagePath: Staking.stakingLedger), + connection: connection, + runtimeProvider: runtimeProvider, + operationQueue: operationQueue, + completion: completion + ) + } +} diff --git a/novawallet/Modules/Staking/Protocols/StakingErrorPresentable.swift b/novawallet/Modules/Staking/Protocols/StakingErrorPresentable.swift index 2002120716..15bd5b36c2 100644 --- a/novawallet/Modules/Staking/Protocols/StakingErrorPresentable.swift +++ b/novawallet/Modules/Staking/Protocols/StakingErrorPresentable.swift @@ -1,12 +1,5 @@ import Foundation -struct NPoolsEDViolationErrorParams { - let availableBalance: String - let minimumBalance: String - let fee: String - let maxStake: String -} - protocol StakingErrorPresentable: StakingBaseErrorPresentable { func presentAmountTooLow(value: String, from view: ControllerBackedProtocol, locale: Locale?) @@ -74,6 +67,11 @@ protocol StakingErrorPresentable: StakingBaseErrorPresentable { onClose: @escaping () -> Void, locale: Locale? ) + + func presentDirectAndPoolStakingConflict( + from view: ControllerBackedProtocol?, + locale: Locale? + ) } extension StakingErrorPresentable where Self: AlertPresentable & ErrorPresentable { @@ -300,4 +298,20 @@ extension StakingErrorPresentable where Self: AlertPresentable & ErrorPresentabl present(viewModel: viewModel, style: .alert, from: view) } + + func presentDirectAndPoolStakingConflict( + from view: ControllerBackedProtocol?, + locale: Locale? + ) { + let message = R.string.localizable.stakingSetupConflictMessage( + preferredLanguages: locale?.rLanguages + ) + + let title = R.string.localizable.stakingSetupConflictTitle( + 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/StakingMain/NominationPools/StakingNPoolsPresenter.swift b/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsPresenter.swift index 760769210b..f83ecee44b 100644 --- a/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsPresenter.swift +++ b/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsPresenter.swift @@ -159,21 +159,6 @@ extension StakingNPoolsPresenter: StakingMainChildPresenterProtocol { 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: diff --git a/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsProtocols.swift b/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsProtocols.swift index 5150bd4f7f..188aba9eb0 100644 --- a/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsProtocols.swift +++ b/novawallet/Modules/Staking/StakingMain/NominationPools/StakingNPoolsProtocols.swift @@ -32,7 +32,7 @@ protocol StakingNPoolsInteractorOutputProtocol: AnyObject { func didReceive(error: StakingNPoolsError) } -protocol StakingNPoolsWireframeProtocol: AlertPresentable, ErrorPresentable, AddressOptionsPresentable, +protocol StakingNPoolsWireframeProtocol: AlertPresentable, ErrorPresentable, CommonRetryable { func showStakeMore(from view: StakingMainViewProtocol?) func showUnstake(from view: StakingMainViewProtocol?) diff --git a/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainPresenter.swift b/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainPresenter.swift index e2baecfb42..8d085e802c 100644 --- a/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainPresenter.swift +++ b/novawallet/Modules/Staking/StakingMain/Parachain/StakingParachainPresenter.swift @@ -148,10 +148,6 @@ extension StakingParachainPresenter: StakingMainChildPresenterProtocol { // not needed action for parachain staking } - func performSelectedEntityAction() { - // no support for selected entity - } - func performRebondAction() { guard let delegator = stateMachine.viewState( diff --git a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainPresenter.swift b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainPresenter.swift index d70d4336ef..614dfbc989 100644 --- a/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainPresenter.swift +++ b/novawallet/Modules/Staking/StakingMain/Relaychain/StakingRelaychainPresenter.swift @@ -402,10 +402,6 @@ extension StakingRelaychainPresenter: StakingMainChildPresenterProtocol { } } - 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/StakingMainPresenter.swift b/novawallet/Modules/Staking/StakingMain/StakingMainPresenter.swift index 872062555f..2cf19944c5 100644 --- a/novawallet/Modules/Staking/StakingMain/StakingMainPresenter.swift +++ b/novawallet/Modules/Staking/StakingMain/StakingMainPresenter.swift @@ -83,10 +83,6 @@ extension StakingMainPresenter: StakingMainPresenterProtocol { childPresenter?.performAlertAction(alert) } - func performSelectedEntityAction() { - childPresenter?.performSelectedEntityAction() - } - func selectPeriod() { wireframe.showPeriodSelection( from: view, diff --git a/novawallet/Modules/Staking/StakingMain/StakingMainProtocols.swift b/novawallet/Modules/Staking/StakingMain/StakingMainProtocols.swift index b137430064..7d462b6155 100644 --- a/novawallet/Modules/Staking/StakingMain/StakingMainProtocols.swift +++ b/novawallet/Modules/Staking/StakingMain/StakingMainProtocols.swift @@ -21,7 +21,6 @@ protocol StakingMainPresenterProtocol: AnyObject { func networkInfoViewDidChangeExpansion(isExpanded: Bool) func performManageAction(_ action: StakingManageOption) func performAlertAction(_ alert: StakingAlert) - func performSelectedEntityAction() func selectPeriod() } @@ -53,6 +52,5 @@ protocol StakingMainChildPresenterProtocol: AnyObject { 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 a5b487a9d0..fe4014bb5a 100644 --- a/novawallet/Modules/Staking/StakingMain/StakingMainViewController.swift +++ b/novawallet/Modules/Staking/StakingMain/StakingMainViewController.swift @@ -122,10 +122,6 @@ final class StakingMainViewController: UIViewController, AdaptiveDesignable, Vie presenter.performClaimRewards() } - @objc private func selectedEntityAction() { - presenter.performSelectedEntityAction() - } - private func setupScrollView() { scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 8, right: 0) } @@ -176,8 +172,6 @@ final class StakingMainViewController: UIViewController, AdaptiveDesignable, Vie selectedEntityCell = addressCell addressCell.bind(viewModel: viewModel.loadingAddress) - - addressCell.addTarget(self, action: #selector(selectedEntityAction), for: .touchUpInside) } private func setupNetworkInfoView() { diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/DirectStakingRecommendationMediator.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/DirectStakingRecommendationMediator.swift index 9b056dd3b6..af1aad9469 100644 --- a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/DirectStakingRecommendationMediator.swift +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/DirectStakingRecommendationMediator.swift @@ -10,14 +10,18 @@ class DirectStakingRecommendationMediator: BaseStakingRecommendationMediator { var restrictions: RelaychainStakingRestrictions? + let validationFactory: StakingRecommendationValidationFactoryProtocol + init( recommendationFactory: DirectStakingRecommendationFactoryProtocol, restrictionsBuilder: RelaychainStakingRestrictionsBuilding, + validationFactory: StakingRecommendationValidationFactoryProtocol, operationQueue: OperationQueue, logger: LoggerProtocol ) { self.recommendationFactory = recommendationFactory self.restrictionsBuilder = restrictionsBuilder + self.validationFactory = validationFactory self.operationQueue = operationQueue super.init(logger: logger) @@ -31,7 +35,7 @@ class DirectStakingRecommendationMediator: BaseStakingRecommendationMediator { let recommendation = RelaychainStakingRecommendation( staking: .direct(validators), restrictions: restrictions, - validationFactory: nil + validationFactory: validationFactory ) didReceive(recommendation: recommendation, for: amount) diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/DirectStkRecommendingValidationFactory.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/DirectStkRecommendingValidationFactory.swift new file mode 100644 index 0000000000..0dee4dc275 --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/DirectStkRecommendingValidationFactory.swift @@ -0,0 +1,62 @@ +import Foundation +import SubstrateSdk + +final class DirectStkRecommendingValidationFactory: StakingActivityProviding { + let connection: JSONRPCEngine + let runtimeProvider: RuntimeCodingServiceProtocol + let operationQueue: OperationQueue + + init( + connection: JSONRPCEngine, + runtimeProvider: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue + ) { + self.connection = connection + self.runtimeProvider = runtimeProvider + self.operationQueue = operationQueue + } + + private func createNoPoolStaking( + for params: StakingRecommendationValidationParams, + view: ControllerBackedProtocol?, + presentable: StakingErrorPresentable, + locale: Locale + ) -> DataValidating { + AsyncErrorConditionViolation(onError: { + presentable.presentDirectAndPoolStakingConflict(from: view, locale: locale) + }, preservesCondition: { completion in + self.hasPoolStaking( + for: params.accountId, + connection: self.connection, + runtimeProvider: self.runtimeProvider, + operationQueue: self.operationQueue + ) { result in + switch result { + case let .success(hasDirectStaking): + completion(!hasDirectStaking) + case .failure: + completion(false) + } + } + }, onProgress: params.onAsyncProgress) + } +} + +extension DirectStkRecommendingValidationFactory: StakingRecommendationValidationFactoryProtocol { + func createValidations( + for params: StakingRecommendationValidationParams, + controller: ControllerBackedProtocol?, + balanceViewModelFactory _: BalanceViewModelFactoryProtocol, + presentable: StakingErrorPresentable, + locale: Locale + ) -> [DataValidating] { + let noDirectStaking = createNoPoolStaking( + for: params, + view: controller, + presentable: presentable, + locale: locale + ) + + return [noDirectStaking] + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/PoolStakingRecommendationMediator.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/PoolStakingRecommendationMediator.swift index aeaf7db863..98ba4dd2ad 100644 --- a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/PoolStakingRecommendationMediator.swift +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/PoolStakingRecommendationMediator.swift @@ -9,6 +9,7 @@ final class PoolStakingRecommendationMediator: BaseStakingRecommendationMediator let chainAsset: ChainAsset let npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol + let validationFactory: StakingRecommendationValidationFactoryProtocol var restrictions: RelaychainStakingRestrictions? @@ -20,12 +21,14 @@ final class PoolStakingRecommendationMediator: BaseStakingRecommendationMediator npoolsLocalSubscriptionFactory: NPoolsLocalSubscriptionFactoryProtocol, restrictionsBuilder: RelaychainStakingRestrictionsBuilding, operationFactory: NominationPoolRecommendationFactoryProtocol, + validationFactory: StakingRecommendationValidationFactoryProtocol, operationQueue: OperationQueue, logger: LoggerProtocol ) { self.chainAsset = chainAsset self.restrictionsBuilder = restrictionsBuilder self.npoolsLocalSubscriptionFactory = npoolsLocalSubscriptionFactory + self.validationFactory = validationFactory self.operationFactory = operationFactory self.operationQueue = operationQueue @@ -44,7 +47,7 @@ final class PoolStakingRecommendationMediator: BaseStakingRecommendationMediator let recommendation = RelaychainStakingRecommendation( staking: .pool(pool), restrictions: restrictions, - validationFactory: nil + validationFactory: validationFactory ) didReceive(recommendation: recommendation, for: amount) diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/PoolStakingRecommendingValidationFactory.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/PoolStakingRecommendingValidationFactory.swift new file mode 100644 index 0000000000..df36a1a1bb --- /dev/null +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/PoolStakingRecommendingValidationFactory.swift @@ -0,0 +1,62 @@ +import Foundation +import SubstrateSdk + +final class PoolStakingRecommendingValidationFactory: StakingActivityProviding { + let connection: JSONRPCEngine + let runtimeProvider: RuntimeCodingServiceProtocol + let operationQueue: OperationQueue + + init( + connection: JSONRPCEngine, + runtimeProvider: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue + ) { + self.connection = connection + self.runtimeProvider = runtimeProvider + self.operationQueue = operationQueue + } + + private func createNoDirectStaking( + for params: StakingRecommendationValidationParams, + view: ControllerBackedProtocol?, + presentable: StakingErrorPresentable, + locale: Locale + ) -> DataValidating { + AsyncErrorConditionViolation(onError: { + presentable.presentDirectAndPoolStakingConflict(from: view, locale: locale) + }, preservesCondition: { completion in + self.hasDirectStaking( + for: params.accountId, + connection: self.connection, + runtimeProvider: self.runtimeProvider, + operationQueue: self.operationQueue + ) { result in + switch result { + case let .success(hasDirectStaking): + completion(!hasDirectStaking) + case .failure: + completion(false) + } + } + }, onProgress: params.onAsyncProgress) + } +} + +extension PoolStakingRecommendingValidationFactory: StakingRecommendationValidationFactoryProtocol { + func createValidations( + for params: StakingRecommendationValidationParams, + controller: ControllerBackedProtocol?, + balanceViewModelFactory _: BalanceViewModelFactoryProtocol, + presentable: StakingErrorPresentable, + locale: Locale + ) -> [DataValidating] { + let noDirectStaking = createNoDirectStaking( + for: params, + view: controller, + presentable: presentable, + locale: locale + ) + + return [noDirectStaking] + } +} diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/StakingRecommendationMediatorFactory.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/StakingRecommendationMediatorFactory.swift index 10364058e9..9b505c8aa2 100644 --- a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/StakingRecommendationMediatorFactory.swift +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/StakingRecommendationMediatorFactory.swift @@ -95,15 +95,26 @@ extension StakingRecommendationMediatorFactory: StakingRecommendationMediatorFac func createDirectStakingMediator( for state: RelaychainStartStakingStateProtocol ) -> RelaychainStakingRecommendationMediating? { + let chainId = state.chainAsset.chain.chainId + guard let recommendationFactory = createDirectStakingRecommendationFactory(for: state), - let restrictionsBuilder = createDirectStakingRestrictionsBuilder(for: state) else { + let restrictionsBuilder = createDirectStakingRestrictionsBuilder(for: state), + let connection = chainRegistry.getConnection(for: chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chainId) else { return nil } + let validationFactory = DirectStkRecommendingValidationFactory( + connection: connection, + runtimeProvider: runtimeService, + operationQueue: operationQueue + ) + return DirectStakingRecommendationMediator( recommendationFactory: recommendationFactory, restrictionsBuilder: restrictionsBuilder, + validationFactory: validationFactory, operationQueue: operationQueue, logger: logger ) @@ -138,11 +149,18 @@ extension StakingRecommendationMediatorFactory: StakingRecommendationMediatorFac storageOperationFactory: poolsOperationFactory ) + let validationFactory = PoolStakingRecommendingValidationFactory( + connection: connection, + runtimeProvider: runtimeService, + operationQueue: operationQueue + ) + return PoolStakingRecommendationMediator( chainAsset: state.chainAsset, npoolsLocalSubscriptionFactory: state.npLocalSubscriptionFactory, restrictionsBuilder: restrictionsBuilder, operationFactory: operationFactory, + validationFactory: validationFactory, operationQueue: operationQueue, logger: logger ) diff --git a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/StakingRecommendationValidationFactory.swift b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/StakingRecommendationValidationFactory.swift index b37aea47f8..fbdbcdb5fd 100644 --- a/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/StakingRecommendationValidationFactory.swift +++ b/novawallet/Modules/Staking/StakingSetupAmount/Recommendation/StakingRecommendationValidationFactory.swift @@ -2,12 +2,14 @@ import Foundation import BigInt struct StakingRecommendationValidationParams { + let accountId: AccountId let stakingAmount: Decimal? let assetBalance: AssetBalance? let assetLocks: AssetLocks? let fee: ExtrinsicFeeProtocol? let existentialDeposit: BigUInt? let stakeUpdateClosure: (Decimal) -> Void + let onAsyncProgress: AsyncValidationOnProgress? } protocol StakingRecommendationValidationFactoryProtocol: AnyObject { diff --git a/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountPresenter.swift b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountPresenter.swift index d3b54e70ea..b9d9931d60 100644 --- a/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountPresenter.swift +++ b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountPresenter.swift @@ -14,6 +14,7 @@ final class StakingSetupAmountPresenter { let balanceDerivationFactory: StakingTypeBalanceFactoryProtocol let recommendsMultipleStakings: Bool let chainAsset: ChainAsset + let accountId: AccountId let logger: LoggerProtocol private var setupMethod: StakingSelectionMethod = .recommendation(nil) @@ -48,6 +49,7 @@ final class StakingSetupAmountPresenter { balanceViewModelFactory: BalanceViewModelFactoryProtocol, balanceDerivationFactory: StakingTypeBalanceFactoryProtocol, dataValidatingFactory: RelaychainStakingValidatorFacadeProtocol, + accountId: AccountId, chainAsset: ChainAsset, recommendsMultipleStakings: Bool, localizationManager: LocalizationManagerProtocol, @@ -61,6 +63,7 @@ final class StakingSetupAmountPresenter { self.balanceViewModelFactory = balanceViewModelFactory self.balanceDerivationFactory = balanceDerivationFactory self.dataValidatingFactory = dataValidatingFactory + self.accountId = accountId self.chainAsset = chainAsset self.recommendsMultipleStakings = recommendsMultipleStakings self.logger = logger @@ -357,6 +360,7 @@ extension StakingSetupAmountPresenter: StakingSetupAmountPresenterProtocol { let recommendedValidations = setupMethod.recommendation?.validationFactory?.createValidations( for: .init( + accountId: accountId, stakingAmount: currentInputAmount, assetBalance: assetBalance, assetLocks: assetLocks, @@ -364,7 +368,15 @@ extension StakingSetupAmountPresenter: StakingSetupAmountPresenterProtocol { existentialDeposit: existentialDeposit, stakeUpdateClosure: { newStake in currentInputAmount = newStake - } + }, + onAsyncProgress: .init( + willStart: { [weak self] in + self?.view?.didStartLoading() + }, + didComplete: { [weak self] _ in + self?.view?.didStopLoading() + } + ) ), controller: view, balanceViewModelFactory: balanceViewModelFactory, diff --git a/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountProtocols.swift b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountProtocols.swift index 770afad122..9e2192d46b 100644 --- a/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountProtocols.swift +++ b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountProtocols.swift @@ -1,7 +1,7 @@ import Foundation import BigInt -protocol StakingSetupAmountViewProtocol: ControllerBackedProtocol { +protocol StakingSetupAmountViewProtocol: SCLoadableControllerProtocol { func didReceive(balance: TitleHorizontalMultiValueView.Model) func didReceive(title: String) func didReceiveButtonState(title: String, enabled: Bool) diff --git a/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewFactory.swift b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewFactory.swift index 657da6942b..717b403387 100644 --- a/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewFactory.swift +++ b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewFactory.swift @@ -6,9 +6,12 @@ struct StakingSetupAmountViewFactory { static func createView( for state: RelaychainStartStakingStateProtocol ) -> StakingSetupAmountViewProtocol? { + let accountRequest = state.chainAsset.chain.accountRequest() + guard let currencyManager = CurrencyManager.shared, - let interactor = createInteractor(for: state) else { + let selectedAccount = SelectedWalletSettings.shared.value?.fetch(for: accountRequest), + let interactor = createInteractor(for: state, selectedAccount: selectedAccount) else { return nil } @@ -41,18 +44,17 @@ struct StakingSetupAmountViewFactory { balanceViewModelFactory: balanceViewModelFactory, balanceDerivationFactory: balanceDerivationFactory, dataValidatingFactory: dataValidatingFactory, + accountId: selectedAccount.accountId, chainAsset: state.chainAsset, recommendsMultipleStakings: state.recommendsMultipleStakings, localizationManager: LocalizationManager.shared, logger: Logger.shared ) + let keyboardStrategy = EventDrivenKeyboardStrategy(events: [.viewDidAppear], triggersOnes: true) let view = StakingSetupAmountViewController( presenter: presenter, - keyboardAppearanceStrategy: EventDrivenKeyboardStrategy( - events: [.viewDidAppear], - triggersOnes: true - ), + keyboardAppearanceStrategy: keyboardStrategy, localizationManager: LocalizationManager.shared ) @@ -64,15 +66,11 @@ struct StakingSetupAmountViewFactory { } private static func createInteractor( - for state: RelaychainStartStakingStateProtocol + for state: RelaychainStartStakingStateProtocol, + selectedAccount: ChainAccountResponse ) -> 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 diff --git a/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewLayout.swift b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewLayout.swift index 68ff29f56b..564f1b0a00 100644 --- a/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewLayout.swift +++ b/novawallet/Modules/Staking/StakingSetupAmount/StakingSetupAmountViewLayout.swift @@ -1,7 +1,7 @@ import UIKit import SoraUI -final class StakingSetupAmountViewLayout: ScrollableContainerLayoutView { +final class StakingSetupAmountViewLayout: SCLoadableActionLayoutView { let amountView: TitleHorizontalMultiValueView = .create { $0.titleView.apply(style: .footnoteSecondary) $0.detailsTitleLabel.apply(style: .footnoteSecondary) @@ -17,8 +17,8 @@ final class StakingSetupAmountViewLayout: ScrollableContainerLayoutView { var stakingTypeView: BackgroundedContentControl = StakingTypeAccountView(frame: .zero) - let actionButton: TriangularedButton = .create { - $0.applyDefaultStyle() + var actionButton: TriangularedButton { + genericActionView.actionButton } override init(frame: CGRect) { @@ -38,13 +38,6 @@ final class StakingSetupAmountViewLayout: ScrollableContainerLayoutView { 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) diff --git a/novawallet/Modules/Staking/Validation/NominationPoolDataValidatorFactory.swift b/novawallet/Modules/Staking/Validation/NominationPoolDataValidatorFactory.swift index aead83ff30..43f75206d9 100644 --- a/novawallet/Modules/Staking/Validation/NominationPoolDataValidatorFactory.swift +++ b/novawallet/Modules/Staking/Validation/NominationPoolDataValidatorFactory.swift @@ -54,6 +54,13 @@ protocol NominationPoolDataValidatorFactoryProtocol: StakingBaseDataValidatingFa locale: Locale ) -> DataValidating + func canMigrateIfNeeded( + needsMigration: Bool?, + stakingActivity: StakingActivityForValidating, + onProgress: AsyncValidationOnProgress?, + locale: Locale + ) -> DataValidating + func poolStakingNotViolatingExistentialDeposit( for params: ExistentialDepositValidationParams, chainAsset: ChainAsset, @@ -308,6 +315,44 @@ extension NominationPoolDataValidatorFactory: NominationPoolDataValidatorFactory }) } + func canMigrateIfNeeded( + needsMigration: Bool?, + stakingActivity: StakingActivityForValidating, + onProgress: AsyncValidationOnProgress?, + locale: Locale + ) -> DataValidating { + AsyncErrorConditionViolation(onError: { [weak self] in + guard let view = self?.view else { + return + } + + self?.presentable.presentDirectStakingNotAllowedForMigration( + from: view, + locale: locale + ) + + }, preservesCondition: { completion in + guard let needsMigration else { + completion(false) + return + } + + guard needsMigration else { + completion(true) + return + } + + stakingActivity.hasDirectStaking { result in + switch result { + case let .success(hasDirectStaking): + completion(!hasDirectStaking) + case .failure: + completion(false) + } + } + }, onProgress: onProgress) + } + // swiftlint:disable:next function_body_length func poolStakingNotViolatingExistentialDeposit( for params: ExistentialDepositValidationParams, diff --git a/novawallet/Modules/Staking/Validation/StakingActivityForValidating.swift b/novawallet/Modules/Staking/Validation/StakingActivityForValidating.swift new file mode 100644 index 0000000000..d608745e18 --- /dev/null +++ b/novawallet/Modules/Staking/Validation/StakingActivityForValidating.swift @@ -0,0 +1,60 @@ +import Foundation +import SubstrateSdk + +protocol StakingActivityForValidating { + func hasDirectStaking(for completion: @escaping (Result) -> Void) +} + +final class StakingActivityForValidation { + let accountId: AccountId + let connection: JSONRPCEngine + let runtimeService: RuntimeCodingServiceProtocol + let operationQueue: OperationQueue + + init( + accountId: AccountId, + connection: JSONRPCEngine, + runtimeService: RuntimeCodingServiceProtocol, + operationQueue: OperationQueue + ) { + self.accountId = accountId + self.connection = connection + self.runtimeService = runtimeService + self.operationQueue = operationQueue + } +} + +extension StakingActivityForValidation: StakingActivityProviding { + func hasDirectStaking(for completion: @escaping (Result) -> Void) { + hasDirectStaking( + for: accountId, + connection: connection, + runtimeProvider: runtimeService, + operationQueue: operationQueue, + completion: completion + ) + } +} + +extension StakingActivityForValidation: StakingActivityForValidating { + convenience init?( + wallet: MetaAccountModel, + chain: ChainModel, + chainRegistry: ChainRegistryProtocol, + operationQueue: OperationQueue + ) { + guard + let connection = chainRegistry.getConnection(for: chain.chainId), + let runtimeService = chainRegistry.getRuntimeProvider(for: chain.chainId), + let selectedAccount = wallet.fetch(for: chain.accountRequest()) else { + return nil + } + + self.init( + accountId: selectedAccount.accountId, + connection: connection, + runtimeService: runtimeService, + operationQueue: operationQueue + ) + } +} diff --git a/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift b/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift index 57be6c2f90..8817383c20 100644 --- a/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift +++ b/novawallet/Modules/Swaps/Base/Model/SwapMaxModel.swift @@ -34,12 +34,12 @@ struct SwapMaxModel { if shouldKeepMinBalance, !minBalanceCoveredByFrozen(in: balance) { let minBalance = payAssetExistense?.minBalance ?? 0 - maxAmount = maxAmount.saturatingSub(minBalance) + maxAmount = maxAmount.subtractOrZero(minBalance) } if let feeModel = feeModel { let fee = feeModel.totalFee.targetAmount - maxAmount = maxAmount.saturatingSub(fee) + maxAmount = maxAmount.subtractOrZero(fee) } return maxAmount.decimal(precision: payChainAsset.asset.precision) @@ -51,7 +51,7 @@ struct SwapMaxModel { } let fee = feeModel.totalFee.targetAmount - let maxAmount = balance.transferable.saturatingSub(fee) + let maxAmount = balance.transferable.subtractOrZero(fee) return maxAmount.decimal(precision: payChainAsset.asset.precision) } diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index cb000680e8..67f72a9a18 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -63,7 +63,6 @@ "common.secret.derivation.path.substrate" = "Substrate secret derivation path"; "common.secret.derivation.path.ethereum" = "Ethereum secret derivation path"; "common.choose.network" = "Choose network"; -"common.name" = "Name"; "sr25519.selection.subtitle" = "sr25519 (recommended)"; "ed25519.selection.subtitle" = "ed25519 (alternative)"; "ecdsa.selection.subtitle" = "(BTC/ETH compatible)"; @@ -123,7 +122,6 @@ "common.copied" = "Copied to clipboard"; "connections.add.connection" = "Add connection"; "connection.management.default.title" = "Default"; -"connection.management.custom.title" = "Added"; "wallet.delete.confirmation.title" = "Forget wallet?"; "account.delete.confirmation.description" = "Make sure you have exported your wallet before proceeding."; "wallet.delete.confirmation.description" = "Make sure you have exported your wallet before proceeding."; @@ -134,8 +132,6 @@ "wallet.asset.receive" = "Receive"; "wallet.assets.total.title" = "Assets value"; "network.info.node.title" = "Node Info"; -"network.info.node.name" = "Node name"; -"network.info.node.url" = "Node URL"; "network.info.node.address" = "Node address"; "common.add" = "Add"; "common.update" = "Update"; @@ -1086,8 +1082,9 @@ "add.token.title" = "Enter ERC-20 token details"; "common.contract.address" = "Contract address"; "common.token.symbol" = "Symbol"; -"add.token.price.title" = "Coingecko link for price info (Optional)"; +"add.token.price.title" = "Coingecko link for price info (Optional) "; "add.token.network.selection.title" = "Select network to add ERC-20 token"; +"common.disabled" = "Disabled"; "tokens.manage.title" = "Manage tokens"; "tokens.manage.all.selected" = "All networks"; "common.more.format" = "%@ (+%@ more)"; @@ -1517,7 +1514,6 @@ "notifications.error.disabled.in.settings.title" = "Enable Notifications for Nova Wallet"; "notifications.error.disabled.in.settings.message" = "Go to Settings → Tap \"Notifications\" → Turn on \"Allow Notifications\""; "notifications.wallet.list.limit.error.message" = "Unfortunately, there is currently a limit in push notifications for only %d of your wallets."; -"ledger.instructions.step4" = "%@ to add to wallet"; "legacy.ledger.migration.message" = "The Migration app will be unavailable in the near future. Use it to migrate your accounts to the new Ledger app to avoid losing your funds."; "legacy.ledger.notification.title" = "New Ledger app has been released"; "legacy.ledger.notification.message" = "To sign operations and migrate your accounts to the new Generic Ledger app install and open Migration app. Legacy Old and Migration Ledger apps will not be supported in the future."; @@ -1663,68 +1659,71 @@ "common.ledger.nano.generic" = "Ledger Nano X (Generic Polkadot app)"; "generic.ledger.instructions.step1.highlighted" = "Polkadot app is installed"; "generic.ledger.instructions.step2.highlighted" = "Open the Polkadot app"; -"ledger.instructions.step1" = "Make sure %@ to your Ledger device using Ledger Live app"; -"ledger.instructions.step1.highlighted" = "Network app is installed"; -"ledger.instructions.step2" = "%@ on your Ledger device"; -"ledger.instructions.step2.highlighted" = "Open the network app"; -"ledger.instructions.step3" = "Allow Nova Wallet to %@"; -"ledger.instructions.step3.highlighted" = "access Bluetooth"; "ledger.instructions.step4" = "%@ to add to wallet"; -"ledger.instructions.step4.highlighted" = "Select account"; -"common.disabled" = "Disabled"; -"common.testnet" = "Testnet"; "networks.list.add.network.button.title" = "Add network"; -"integrate.network.banner.title" = "Building for Polkadot?"; -"integrate.network.banner.message" = "Integrate all the features of the network you are building into Nova Wallet, making it accessible to everyone."; +"connection.management.custom.title" = "Added"; +"common.testnet" = "Testnet"; "integrate.network.banner.button.link" = "Integrate your network"; -"networks.list.placeholder.messsage" = "Added custom networks will appear here"; -"network.details.default.nodes.section.title" = "Default Nodes"; -"network.details.custom.nodes.section.title" = "Custom nodes"; -"network.details.enable.connection" = "Enable connection"; -"network.details.auto.balance" = "Auto-balance nodes"; +"integrate.network.banner.message" = "Integrate all the features of the network you are building into Nova Wallet, making it accessible to everyone."; +"integrate.network.banner.title" = "Building for Polkadot?"; +"network.details.default.nodes.section.title" = "default nodes"; +"network.details.custom.nodes.section.title" = "custom nodes"; "network.details.add.custom.node" = "Add custom node"; "network.details.ping.milliseconds" = "%d ms"; -"network.node.add.title" = "Add custom node"; "network.node.add.button.add" = "Add node"; "network.node.add.button.enter.details" = "Enter details"; +"common.name" = "Name"; +"network.info.node.name" = "Node name"; +"network.info.node.url" = "Node URL"; +"network.node.add.alert.node.error.message.wss" = "The Node URL you entered is either not responding or contains incorrect format. The URL format should start with \"wss://\"."; "network.node.add.alert.node.error.title" = "Node error"; -"network.node.add.alert.node.error.message.wss" = "The Node URL you entered is either not responding or contains incorrect format. The URL format should start with \"ws://\" or \"wss://\""; "network.node.add.alert.wrong.network.title" = "Wrong network"; -"network.node.add.alert.wrong.network.message" = "The URL you entered is not corresponding to Node for %@. -Please enter URL of the valid %@ node."; "network.node.add.alert.already.exists.title" = "This Node already exists"; -"network.node.add.alert.already.exists.message" = "The URL you entered already exists as the \"%@\" Node."; -"common.for" = "For"; -"network.manage.node.edit" = "Edit node"; -"network.manage.node.delete" = "Delete node"; -"network.manage.node.manage.added.node" = "Manage added node"; -"network.node.edit.title" = "Edit custom node"; -"common.token" = "Token"; -"network.add.title" = "Enter network details"; +"network.add.network.manually" = "Add network manually"; +"network.known.list.search.placeholder" = "Search by network name"; "network.add.rpc.url" = "RPC URL"; "network.add.name" = "Network name"; +"common.token" = "TOKEN"; "network.add.currencySymbol" = "Currency Symbol"; "network.add.chainId" = "Chain ID"; "network.add.blockExplorerUrl" = "Block explorer URL (Optional)"; +"common.modify" = "Modify"; +"network.add.alert.invalid.chain.id.message" = "The entered Chain ID does not match the network in the RPC URL."; +"network.add.alert.invalid.chain.id.title" = "Invalid Chain ID"; +"network.add.title" = "Enter network details"; +"common.delete" = "Delete"; +"network.manage.title" = "Manage added network"; +"network.manage.delete" = "Delete network"; +"network.manage.edit" = "Edit network"; +"network.manage.delete.alert.description" = "You will not be able to see your token balances on that network on Assets screen"; +"network.manage.delete.alert.title" = "Delete network?"; +"network.manage.node.delete" = "Delete node"; +"network.manage.node.edit" = "Edit node"; +"network.node.delete.alert.title" = "Delete node?"; +"network.manage.node.manage.added.node" = "Manage added node"; +"network.add.alert.invalid.symbol.title" = "Invalid Currency Symbol"; +"network.info.node.title" = "Node Info"; +"networks.list.placeholder.messsage" = "Added custom networks will appear here"; +"network.details.enable.connection" = "Enable connection"; +"network.details.auto.balance" = "Auto-balance nodes"; +"network.node.add.title" = "Add custom node"; +"network.node.add.alert.wrong.network.message" = "The URL you entered is not corresponding to Node for %@.\nPlease enter URL of the valid %@ node."; +"network.node.add.alert.already.exists.message" = "The URL you entered already exists as the \"%@\" Node."; +"common.for" = "For"; +"network.node.edit.title" = "Edit custom node"; "network.add.coingeckoUrl" = "Coingecko link for price info (Optional)"; "network.add.alert.already.exists.title" = "This network already exist"; "network.add.alert.already.exists.remote.message" = "The entered RPC URL is present in Nova as a %@."; "network.add.alert.already.exists.custom.message" = "The entered RPC URL is present in Nova as a %@. Are you sure you want to modify it?"; -"network.add.alert.invalid.chain.id.title" = "Invalid Chain ID"; -"network.add.alert.invalid.chain.id.message" = "The entered Chain ID does not match the network in the RPC URL."; "network.add.alert.invalid.network.type.title" = "Invalid network type"; -"network.add.alert.invalid.network.type.message" = "It seems you want to add a different type of network. Please select the “%@” to continue."; -"network.add.alert.invalid.symbol.title" = "Invalid Currency Symbol"; -"network.add.alert.invalid.symbol.message" = "The entered Currency Symbol (%@) does not match the network (%@). Do you want to use the correct currency symbol?"; -"network.manage.edit" = "Edit network"; -"network.manage.delete" = "Delete network"; -"network.manage.title" = "Manage added network"; -"network.manage.delete.alert.title" = "Delete network?"; -"network.manage.delete.alert.description" = "You will not be able to see your token balances on that network on Assets screen"; -"common.delete" = "Delete"; -"network.known.list.search.placeholder" = "Search by network name"; -"network.add.network.manually" = "Add network manually"; -"network.node.delete.alert.title" = "Delete node?"; +"network.add.alert.invalid.network.type.message" = "It seems you want to add a different type of network. Please select the \“%@\” to continue."; +"network.add.alert.invalid.symbol.message" = "The entered Currency Symbol (%@) does not match the network (%@). Do you want to use the correct currency symbol?"; "network.node.delete.alert.description" = "%@ node will be deleted"; "network.add.alert.success.title" = "Network added successfully"; -"common.modify" = "Modify"; +"dapp.unknown.warning.title" = "Warning! DApp is unknown"; +"dapp.unknown.warning.message" = "Malicious DApps can withdraw all your funds. Always do your own research before using a DApp, granting permission, or sending funds out.\n\nIf someone is urging you to visit this DApp, it is likely a scam. When in doubt, please contact Nova Wallet support: %@."; +"dapp.unknown.warning.open" = "Open anyway"; +"staking.setup.conflict.title" = "Already staking"; +"staking.setup.conflict.message" = "You cannot stake with Direct Staking and Nomination Pools at the same time"; +"nomination.pools.conflict.title" = "Pool operations are not available"; +"nomination.pools.conflict.message" = "You can no longer use both Direct Staking and Pool Staking from the same account. To manage your Pool Staking you first need to unstake your tokens from Direct Staking."; \ No newline at end of file diff --git a/novawallet/es.lproj/Localizable.strings b/novawallet/es.lproj/Localizable.strings index b309ed1521..3fd1b75e3c 100644 --- a/novawallet/es.lproj/Localizable.strings +++ b/novawallet/es.lproj/Localizable.strings @@ -1719,4 +1719,11 @@ "network.add.alert.invalid.network.type.message" = "Parece que desea agregar un tipo diferente de red. Por favor, seleccione la “%@” para continuar."; "network.add.alert.invalid.symbol.message" = "El símbolo de moneda ingresado (%@) no coincide con la red (%@). ¿Desea usar el símbolo de moneda correcto?"; "network.node.delete.alert.description" = "El nodo %@ será eliminado"; -"network.add.alert.success.title" = "Red agregada con éxito"; \ No newline at end of file +"network.add.alert.success.title" = "Red agregada con éxito"; +"dapp.unknown.warning.title" = "¡Advertencia! La DApp es desconocida"; +"dapp.unknown.warning.message" = "Las DApps maliciosas pueden retirar todos tus fondos. Siempre realiza tu propia investigación antes de usar una DApp, otorgar permiso o enviar fondos.\n\nSi alguien te está presionando para que visites esta DApp, es probable que sea una estafa. En caso de duda, por favor contacta con el soporte de Nova Wallet: %@."; +"dapp.unknown.warning.open" = "Abrir de todos modos"; +"staking.setup.conflict.title" = "Ya estás en staking"; +"staking.setup.conflict.message" = "No puedes hacer stake con Staking Directo y Pools de Nominación al mismo tiempo"; +"nomination.pools.conflict.title" = "Las operaciones de Pool no están disponibles"; +"nomination.pools.conflict.message" = "Ya no puedes usar tanto Staking Directo como Pool Staking desde la misma cuenta. Para gestionar tu Pool Staking, primero necesitas unstakear tus tokens del Staking Directo."; \ No newline at end of file diff --git a/novawallet/fr.lproj/Localizable.strings b/novawallet/fr.lproj/Localizable.strings index 7caa326200..a6d978031c 100644 --- a/novawallet/fr.lproj/Localizable.strings +++ b/novawallet/fr.lproj/Localizable.strings @@ -1719,4 +1719,11 @@ "network.add.alert.invalid.network.type.message" = "Il semble que vous souhaitiez ajouter un type de réseau différent. Veuillez sélectionner le "; "network.add.alert.invalid.symbol.message" = "Le symbole de monnaie entré (%@) ne correspond pas au réseau (%@). Voulez-vous utiliser le symbole de monnaie correct ?"; "network.node.delete.alert.description" = "Le nœud %@ sera supprimé"; -"network.add.alert.success.title" = "Réseau ajouté avec succès"; \ No newline at end of file +"network.add.alert.success.title" = "Réseau ajouté avec succès"; +"dapp.unknown.warning.title" = "Attention ! DApp inconnue"; +"dapp.unknown.warning.message" = "Les DApps malveillantes peuvent retirer tous vos fonds. Faites toujours vos propres recherches avant d'utiliser une DApp, d'accorder des permissions ou d'envoyer des fonds.\n\nSi quelqu'un vous pousse à visiter cette DApp, il est probable que ce soit une escroquerie. En cas de doute, veuillez contacter le support de Nova Wallet : %@."; +"dapp.unknown.warning.open" = "Ouvrir quand même"; +"staking.setup.conflict.title" = "Déjà staké"; +"staking.setup.conflict.message" = "Vous ne pouvez pas staker avec le Stake direct et les Pools de Nomination en même temps"; +"nomination.pools.conflict.title" = "Les opérations de Pool ne sont pas disponibles"; +"nomination.pools.conflict.message" = "Vous ne pouvez plus utiliser à la fois le Stake direct et le Pool Staking depuis le même compte. Pour gérer votre Pool Staking, vous devez d'abord unstake vos tokens du Stake direct."; \ No newline at end of file diff --git a/novawallet/id.lproj/Localizable.strings b/novawallet/id.lproj/Localizable.strings index 11ddb3db87..a90ecf7b31 100644 --- a/novawallet/id.lproj/Localizable.strings +++ b/novawallet/id.lproj/Localizable.strings @@ -1719,4 +1719,11 @@ "network.add.alert.invalid.network.type.message" = "Sepertinya Anda ingin menambahkan tipe jaringan yang berbeda. Harap pilih \"%@\" untuk melanjutkan."; "network.add.alert.invalid.symbol.message" = "Simbol Mata Uang yang dimasukkan (%@) tidak cocok dengan jaringan (%@). Apakah Anda ingin menggunakan simbol mata uang yang benar?"; "network.node.delete.alert.description" = "Node %@ akan dihapus"; -"network.add.alert.success.title" = "Jaringan berhasil ditambahkan"; \ No newline at end of file +"network.add.alert.success.title" = "Jaringan berhasil ditambahkan"; +"dapp.unknown.warning.title" = "Peringatan! Aplikasi desentralisasi tidak dikenal"; +"dapp.unknown.warning.message" = "Aplikasi desentralisasi yang berbahaya dapat menarik semua dana Anda. Selalu lakukan penelitian sendiri sebelum menggunakan aplikasi desentralisasi, memberikan izin, atau mengirim dana keluar.\n\nJika seseorang mendesak Anda untuk mengunjungi aplikasi desentralisasi ini, kemungkinan besar itu adalah penipuan. Jika ragu, harap hubungi dukungan Nova Wallet: %@."; +"dapp.unknown.warning.open" = "Buka saja"; +"staking.setup.conflict.title" = "Sudah melakukan staking"; +"staking.setup.conflict.message" = "Anda tidak dapat melakukan staking dengan Staking Langsung dan Pool Nominasi pada saat yang sama"; +"nomination.pools.conflict.title" = "Operasi pool tidak tersedia"; +"nomination.pools.conflict.message" = "Anda tidak dapat lagi menggunakan Staking Langsung dan Staking Pool dari akun yang sama. Untuk mengelola Staking Pool Anda, pertama-tama Anda perlu melepaskan token Anda dari Staking Langsung."; \ No newline at end of file diff --git a/novawallet/it.lproj/Localizable.strings b/novawallet/it.lproj/Localizable.strings index 80bc84b690..ecb3440938 100644 --- a/novawallet/it.lproj/Localizable.strings +++ b/novawallet/it.lproj/Localizable.strings @@ -1719,4 +1719,11 @@ "network.add.alert.invalid.network.type.message" = "Sembra che tu voglia aggiungere un tipo di rete diverso. Seleziona \\u201c%@\\u201d per continuare."; "network.add.alert.invalid.symbol.message" = "Il Simbolo della Moneta inserito (%@) non corrisponde alla rete (%@). Vuoi utilizzare il simbolo corretto?"; "network.node.delete.alert.description" = "Il nodo %@ verrà eliminato"; -"network.add.alert.success.title" = "Rete aggiunta con successo"; \ No newline at end of file +"network.add.alert.success.title" = "Rete aggiunta con successo"; +"dapp.unknown.warning.title" = "Attenzione! DApp sconosciuta"; +"dapp.unknown.warning.message" = "Le DApp dannose possono prelevare tutti i tuoi fondi. Esegui sempre una tua ricerca prima di utilizzare una DApp, di concedere autorizzazioni o di inviare fondi.\n\nSe qualcuno ti spinge a visitare questa DApp, si tratta probabilmente di una truffa. In caso di dubbio, contatta il supporto di Nova Wallet: %@."; +"dapp.unknown.warning.open" = "Apri comunque"; +"staking.setup.conflict.title" = "Già in staking"; +"staking.setup.conflict.message" = "Non puoi fare staking contemporaneamente con Direct Staking e con Nomination Pools"; +"nomination.pools.conflict.title" = "Operazioni del Pool non disponibili"; +"nomination.pools.conflict.message" = "Non puoi più utilizzare sia il Direct Staking che il Pool Staking dallo stesso account. Per gestire il tuo Pool Staking devi prima disattivare lo staking dei tuoi token dal Direct Staking."; \ No newline at end of file diff --git a/novawallet/ja.lproj/Localizable.strings b/novawallet/ja.lproj/Localizable.strings index 5a6d3c124a..d057c5ec06 100644 --- a/novawallet/ja.lproj/Localizable.strings +++ b/novawallet/ja.lproj/Localizable.strings @@ -1719,4 +1719,11 @@ "network.add.alert.invalid.network.type.message" = "異なる種類のネットワークを追加しようとしているようです。「%@」を選択して続行してください。"; "network.add.alert.invalid.symbol.message" = "入力された通貨シンボル(%@)がネットワーク(%@)と一致しません。正しい通貨シンボルを使用しますか?"; "network.node.delete.alert.description" = "%@ノードは削除されます"; -"network.add.alert.success.title" = "ネットワークが正常に追加されました"; \ No newline at end of file +"network.add.alert.success.title" = "ネットワークが正常に追加されました"; +"dapp.unknown.warning.title" = "警告! DAppは未知です"; +"dapp.unknown.warning.message" = "悪意のあるDAppsはすべての資金を引き出す可能性があります。DAppを使用する前に、許可を与えたり、資金を送金したりする前に、必ず自分で調査してください。\n\n誰かがこのDAppを訪問するよう促している場合、それは詐欺である可能性が高いです。疑わしい場合は、Nova Walletサポートに連絡してください: %@。"; +"dapp.unknown.warning.open" = "それでも開く"; +"staking.setup.conflict.title" = "すでにステーキング中"; +"staking.setup.conflict.message" = "Direct StakingとNomination Poolsを同時にステークすることはできません"; +"nomination.pools.conflict.title" = "プール操作は利用できません"; +"nomination.pools.conflict.message" = "同じアカウントでDirect StakingとPool Stakingの両方を使用することはできなくなりました。Pool Stakingを管理するには、まずDirect Stakingからトークンをアンステークする必要があります。"; \ No newline at end of file diff --git a/novawallet/ko.lproj/Localizable.strings b/novawallet/ko.lproj/Localizable.strings index 4b76679c27..5e222d96cc 100644 --- a/novawallet/ko.lproj/Localizable.strings +++ b/novawallet/ko.lproj/Localizable.strings @@ -1720,3 +1720,10 @@ "network.add.alert.invalid.symbol.message" = "입력된 통화 기호 (%@)가 네트워크 (%@)와 일치하지 않습니다. 올바른 통화 기호를 사용하시겠습니까?"; "network.node.delete.alert.description" = "%@ 노드가 삭제됩니다"; "network.add.alert.success.title" = "네트워크가 성공적으로 추가되었습니다"; +"dapp.unknown.warning.title" = "경고! DApp이 알 수 없습니다"; +"dapp.unknown.warning.message" = "악성 DApp은 모든 자금을 인출할 수 있습니다. DApp을 사용하기 전, 권한을 부여하기 전 또는 자금을 송금하기 전에 항상 철저한 조사를 하십시오.\n\n누군가 이 DApp을 방문하라고 재촉할 경우, 그것은 사기일 가능성이 큽니다. 의심스러울 경우, 노바 월렛 지원팀에 연락하십시오: %@."; +"dapp.unknown.warning.open" = "어쨌든 열기"; +"staking.setup.conflict.title" = "이미 Staking 중"; +"staking.setup.conflict.message" = "직접 Staking과 Nomination Pools를 동시에 사용할 수 없습니다"; +"nomination.pools.conflict.title" = "Pool 작업을 사용할 수 없습니다"; +"nomination.pools.conflict.message" = "더 이상 동일한 계정에서 직접 Staking과 Pool Staking을 동시에 사용할 수 없습니다. Pool Staking을 관리하려면 먼저 직접 Staking에서 토큰을 Unstake해야 합니다."; \ No newline at end of file diff --git a/novawallet/pl.lproj/Localizable.strings b/novawallet/pl.lproj/Localizable.strings index ffde98ab5a..35d20660ff 100644 --- a/novawallet/pl.lproj/Localizable.strings +++ b/novawallet/pl.lproj/Localizable.strings @@ -1719,4 +1719,11 @@ "network.add.alert.invalid.network.type.message" = "Wygląda na to, że chcesz dodać inny typ sieci. Wybierz “%@” aby kontynuować."; "network.add.alert.invalid.symbol.message" = "Wprowadzony symbol waluty (%@) nie pasuje do sieci (%@). Czy chcesz użyć poprawnego symbolu waluty?"; "network.node.delete.alert.description" = "Węzeł %@ zostanie usunięty"; -"network.add.alert.success.title" = "Sieć dodana pomyślnie"; \ No newline at end of file +"network.add.alert.success.title" = "Sieć dodana pomyślnie"; +"dapp.unknown.warning.title" = "Ostrzeżenie! DApp jest nieznany"; +"dapp.unknown.warning.message" = "Złośliwe DApps mogą wypłacić wszystkie Twoje środki. Zawsze prowadź własne badania przed użyciem DApp, nadaniem uprawnień lub wysłaniem środków.\n\nJeśli ktoś nalega, abyś odwiedził ten DApp, prawdopodobnie jest to oszustwo. W razie wątpliwości, skontaktuj się z pomocą techniczną Nova Wallet: %@."; +"dapp.unknown.warning.open" = "Otwórz mimo to"; +"staking.setup.conflict.title" = "Już staking"; +"staking.setup.conflict.message" = "Nie możesz stake z Direct Staking i Nomination Pools jednocześnie"; +"nomination.pools.conflict.title" = "Operacje Pool są niedostępne"; +"nomination.pools.conflict.message" = "Nie możesz już używać zarówno Direct Staking, jak i Pool Staking z tego samego konta. Aby zarządzać swoim Pool Staking, najpierw musisz unstake swoje tokeny z Direct Staking."; \ No newline at end of file diff --git a/novawallet/pt-PT.lproj/Localizable.strings b/novawallet/pt-PT.lproj/Localizable.strings index 543e7ea40d..2c55a7c27b 100644 --- a/novawallet/pt-PT.lproj/Localizable.strings +++ b/novawallet/pt-PT.lproj/Localizable.strings @@ -1719,4 +1719,11 @@ "network.add.alert.invalid.network.type.message" = "Parece que você quer adicionar um tipo diferente de rede. Por favor, selecione \"%@\" para continuar."; "network.add.alert.invalid.symbol.message" = "O Símbolo da Moeda inserido (%@) não corresponde à rede (%@). Deseja usar o símbolo correto da moeda?"; "network.node.delete.alert.description" = "O nodo %@ será excluído"; -"network.add.alert.success.title" = "Rede adicionada com sucesso"; \ No newline at end of file +"network.add.alert.success.title" = "Rede adicionada com sucesso"; +"dapp.unknown.warning.title" = "Aviso! DApp desconhecido"; +"dapp.unknown.warning.message" = "DApps maliciosos podem retirar todos os seus fundos. Sempre faça sua própria pesquisa antes de usar um DApp, conceder permissão ou enviar fundos.\n\nSe alguém estiver insistindo para você visitar este DApp, provavelmente é um golpe. Em caso de dúvida, por favor, entre em contato com o suporte da Nova Wallet: %@."; +"dapp.unknown.warning.open" = "Abrir de qualquer forma"; +"staking.setup.conflict.title" = "Já está em staking"; +"staking.setup.conflict.message" = "Você não pode fazer staking com Staking Direto e Pools de Nomeação ao mesmo tempo"; +"nomination.pools.conflict.title" = "Operações de Pool não disponíveis"; +"nomination.pools.conflict.message" = "Você não pode mais usar Staking Direto e Staking em Pool a partir da mesma conta. Para gerenciar seu Staking em Pool, você primeiro precisa retirar seus tokens do Staking Direto."; \ No newline at end of file diff --git a/novawallet/ru.lproj/Localizable.strings b/novawallet/ru.lproj/Localizable.strings index 16c13d2e05..27da15f751 100644 --- a/novawallet/ru.lproj/Localizable.strings +++ b/novawallet/ru.lproj/Localizable.strings @@ -1719,4 +1719,11 @@ "network.add.alert.invalid.network.type.message" = "Похоже, вы хотите добавить другой тип сети. Пожалуйста, выберите \\u201c%@\\u201d, чтобы продолжить."; "network.add.alert.invalid.symbol.message" = "Введенный символ валюты (%@) не совпадает с сетью (%@). Вы хотите использовать правильный символ валюты?"; "network.node.delete.alert.description" = "нода %@ будет удалена"; -"network.add.alert.success.title" = "Сеть успешно добавлена"; \ No newline at end of file +"network.add.alert.success.title" = "Сеть успешно добавлена"; +"dapp.unknown.warning.title" = "Внимание! DApp неизвестен"; +"dapp.unknown.warning.message" = "Вредоносные DApps могут вывести все ваши средства. Всегда проводите собственное исследование перед использованием DApp, предоставлением разрешения или отправкой средств.\n\nЕсли кто-то настаивает на посещении этого DApp, это, вероятно, мошенничество. Если у вас есть сомнения, пожалуйста, свяжитесь с поддержкой Nova Wallet: %@."; +"dapp.unknown.warning.open" = "Все равно открыть"; +"staking.setup.conflict.title" = "Уже застейкано"; +"staking.setup.conflict.message" = "Невозможно стейкать напрямую и в пуле одновременно"; +"nomination.pools.conflict.title" = "Действия в пуле недоступны"; +"nomination.pools.conflict.message" = "Вы больше не можете использовать одновременно стейкинг напрямую и в пуле с одного аккаунта. Для управления стейкингом в пуле вам сначала нужно вывести ваши токены из стейкинга напрямую."; \ No newline at end of file diff --git a/novawallet/tr.lproj/Localizable.strings b/novawallet/tr.lproj/Localizable.strings index 538eb2a3f1..d7abe4db4d 100644 --- a/novawallet/tr.lproj/Localizable.strings +++ b/novawallet/tr.lproj/Localizable.strings @@ -1719,4 +1719,11 @@ "network.add.alert.invalid.network.type.message" = "Farklı bir ağ tipi eklemek istediğiniz görünüyor. Devam etmek için lütfen \"%@\" seçin."; "network.add.alert.invalid.symbol.message" = "Girilen Para Birimi Sembolü (%@) ağ (%@) ile eşleşmiyor. Doğru para birimi sembolünü kullanmak istiyor musunuz?"; "network.node.delete.alert.description" = "%@ düğümü silinecek"; -"network.add.alert.success.title" = "Ağ başarıyla eklendi"; \ No newline at end of file +"network.add.alert.success.title" = "Ağ başarıyla eklendi"; +"dapp.unknown.warning.title" = "Uyarı! DApp bilinmiyor"; +"dapp.unknown.warning.message" = "Kötü niyetli DApp'ler tüm varlıklarınızı çekebilir. Bir DApp'i kullanmadan, izin vermeden veya para göndermeden önce her zaman kendi araştırmanızı yapın.\n\nBirisi sizi bu DApp'e girmeye zorluyorsa, muhtemelen bir dolandırıcılıktır. Şüphe duyduğunuzda, lütfen Nova Wallet desteği ile iletişime geçin: %@."; +"dapp.unknown.warning.open" = "Her neyse aç"; +"staking.setup.conflict.title" = "Zaten stake edilmekte"; +"staking.setup.conflict.message" = "Aynı anda hem Doğrudan Stake hem de Nomination Havuzlarını kullanarak stake yapamazsınız"; +"nomination.pools.conflict.title" = "Havuz işlemleri kullanılamıyor"; +"nomination.pools.conflict.message" = "Artık aynı hesaptan hem Doğrudan Stake hem de Havuz Stake kullanamazsınız. Havuz Stake'inizi yönetmek için öncelikle token'larınızı Doğrudan Stake'ten çözmeniz gerekmektedir."; \ No newline at end of file diff --git a/novawallet/vi.lproj/Localizable.strings b/novawallet/vi.lproj/Localizable.strings index 87bb77c0b3..d34eb74e04 100644 --- a/novawallet/vi.lproj/Localizable.strings +++ b/novawallet/vi.lproj/Localizable.strings @@ -1719,4 +1719,11 @@ "network.add.alert.invalid.network.type.message" = "Có vẻ bạn muốn thêm một loại mạng khác. Vui lòng chọn “%@” để tiếp tục."; "network.add.alert.invalid.symbol.message" = "Ký hiệu tiền tệ (%@) bạn nhập vào không khớp với mạng (%@). Bạn có muốn sử dụng ký hiệu tiền tệ đúng không?"; "network.node.delete.alert.description" = "Node %@ sẽ bị xóa"; -"network.add.alert.success.title" = "Mạng đã được thêm thành công"; \ No newline at end of file +"network.add.alert.success.title" = "Mạng đã được thêm thành công"; +"dapp.unknown.warning.title" = "Cảnh báo! DApp chưa biết"; +"dapp.unknown.warning.message" = "Các DApp độc hại có thể rút hết các khoản tiền của bạn. Luôn luôn tự nghiên cứu trước khi sử dụng một DApp, cấp quyền hoặc gửi tiền.\n\nNếu ai đó thúc giục bạn truy cập DApp này, rất có thể đó là một trò lừa đảo. Khi nghi ngờ, vui lòng liên hệ với bộ phận hỗ trợ của Nova Wallet: %@."; +"dapp.unknown.warning.open" = "Mở dù sao đi nữa"; +"staking.setup.conflict.title" = "Đã staking"; +"staking.setup.conflict.message" = "Bạn không thể Stake với Direct Staking và Nomination Pools cùng một lúc"; +"nomination.pools.conflict.title" = "Các thao tác trong Pool không khả dụng"; +"nomination.pools.conflict.message" = "Bạn không thể sử dụng cả Staking trực tiếp và Pool Staking từ cùng một tài khoản. Để quản lý Pool Staking bạn trước tiên cần unstake token của mình từ Staking trực tiếp."; \ No newline at end of file diff --git a/novawallet/zh-Hans.lproj/Localizable.strings b/novawallet/zh-Hans.lproj/Localizable.strings index ba63be2156..4af0bfa21b 100644 --- a/novawallet/zh-Hans.lproj/Localizable.strings +++ b/novawallet/zh-Hans.lproj/Localizable.strings @@ -1719,4 +1719,11 @@ "network.add.alert.invalid.network.type.message" = "看起来您想添加不同类型的网络。请选择“%@”以继续。"; "network.add.alert.invalid.symbol.message" = "输入的货币符号 (%@) 与网络 (%@) 不匹配。要使用正确的货币符号吗?"; "network.node.delete.alert.description" = "%@ 节点将被删除"; -"network.add.alert.success.title" = "网络添加成功"; \ No newline at end of file +"network.add.alert.success.title" = "网络添加成功"; +"dapp.unknown.warning.title" = "警告!DApp 未知"; +"dapp.unknown.warning.message" = "恶意的 DApps 可以提取你所有的资金。使用 DApp、授予权限或转出资金前请务必自行研究。\n\n如果有人迫使你访问这个 DApp,很可能是骗局。如有疑问,请联系 Nova Wallet 支持:%@。"; +"dapp.unknown.warning.open" = "仍然打开"; +"staking.setup.conflict.title" = "已质押"; +"staking.setup.conflict.message" = "不能同时使用直接质押和提名池进行质押"; +"nomination.pools.conflict.title" = "池操作不可用"; +"nomination.pools.conflict.message" = "你不能再同时使用同一个账号进行直接质押和池质押。要管理你的池质押,首先需要从直接质押中取消质押你的代币。"; \ No newline at end of file diff --git a/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift b/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift index 4bb0e58ed9..79e9149260 100644 --- a/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift +++ b/novawalletTests/Mocks/DataProviders/WalletLocalSubscriptionFactoryStub.swift @@ -82,4 +82,16 @@ final class WalletLocalSubscriptionFactoryStub: WalletLocalSubscriptionFactoryPr ) throws -> StreamableProvider { throw CommonError.undefined } + + func getHoldsProvider(for accountId: AccountId) throws -> StreamableProvider { + throw CommonError.undefined + } + + func getHoldsProvider( + for accountId: AccountId, + chainId: ChainModel.Id, + assetId: AssetModel.Id + ) throws -> StreamableProvider { + throw CommonError.undefined + } } diff --git a/novawalletTests/Mocks/ModuleMocks.swift b/novawalletTests/Mocks/ModuleMocks.swift index e246875c4d..7dc26f718e 100644 --- a/novawalletTests/Mocks/ModuleMocks.swift +++ b/novawalletTests/Mocks/ModuleMocks.swift @@ -39275,21 +39275,6 @@ import SoraFoundation - func performSelectedEntityAction() { - - return cuckoo_manager.call("performSelectedEntityAction()", - parameters: (), - escapingParameters: (), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.performSelectedEntityAction()) - - } - - - func selectPeriod() { return cuckoo_manager.call("selectPeriod()", @@ -39347,11 +39332,6 @@ import SoraFoundation 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)) @@ -39415,12 +39395,6 @@ import SoraFoundation 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] = [] @@ -39480,12 +39454,6 @@ import SoraFoundation - func performSelectedEntityAction() { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - func selectPeriod() { return DefaultValueRegistry.defaultValue(for: (Void).self) } @@ -40080,21 +40048,6 @@ import SoraFoundation - func performSelectedEntityAction() { - - return cuckoo_manager.call("performSelectedEntityAction()", - parameters: (), - escapingParameters: (), - superclassCall: - - Cuckoo.MockManager.crashOnProtocolSuperclassCall() - , - defaultCall: __defaultImplStub!.performSelectedEntityAction()) - - } - - - func selectPeriod(_ period: StakingRewardFiltersPeriod) { return cuckoo_manager.call("selectPeriod(_: StakingRewardFiltersPeriod)", @@ -40147,11 +40100,6 @@ import SoraFoundation 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)) @@ -40209,12 +40157,6 @@ import SoraFoundation 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 }] @@ -40268,12 +40210,6 @@ import SoraFoundation - func performSelectedEntityAction() { - return DefaultValueRegistry.defaultValue(for: (Void).self) - } - - - func selectPeriod(_ period: StakingRewardFiltersPeriod) { return DefaultValueRegistry.defaultValue(for: (Void).self) } diff --git a/novawalletTests/Modules/DApps/DAppSearch/DAppSearchTests.swift b/novawalletTests/Modules/DApps/DAppSearch/DAppSearchTests.swift index b3bcdb92bb..e3314fa74a 100644 --- a/novawalletTests/Modules/DApps/DAppSearch/DAppSearchTests.swift +++ b/novawalletTests/Modules/DApps/DAppSearch/DAppSearchTests.swift @@ -1,5 +1,6 @@ import XCTest @testable import novawallet +import SoraFoundation import Cuckoo class DAppSearchTests: XCTestCase { @@ -32,7 +33,9 @@ class DAppSearchTests: XCTestCase { wireframe: wireframe, viewModelFactory: DAppListViewModelFactory(), initialQuery: "", - delegate: delegate + delegate: delegate, + applicationConfig: ApplicationConfig.shared, + localizationManager: LocalizationManager.shared ) presenter.view = view @@ -110,30 +113,5 @@ class DAppSearchTests: XCTestCase { // then (dApp selection test) wait(for: [dAppSelectionExpectation, dAppSelectionCloseExpectation], timeout: 10.0) - - // when (query selection test) - - let querySelectionExpectation = XCTestExpectation() - let querySelectionCloseExpectation = XCTestExpectation() - - stub(delegate) { stub in - when(stub).didCompleteDAppSearchResult(any()).then { result in - if case .query = result { - querySelectionExpectation.fulfill() - } - } - } - - stub(wireframe) { stub in - when(stub).close(from: any()).then { _ in - querySelectionCloseExpectation.fulfill() - } - } - - presenter.selectSearchQuery() - - // then (dApp selection test) - - wait(for: [querySelectionExpectation, querySelectionCloseExpectation], timeout: 10.0) } }